mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
阶段1+2:恢复 HTTP/2 加速 + 重做官网
阶段1 网络加速 - 发现隧道在跑 QUIC(ExecStart 丢了 --protocol http2), 实测 QUIC 可用率 48% vs HTTP/2 80%+,是重大性能回退。 恢复后公网延迟从 4141ms 降到 690ms(快 6 倍),可用率 92%。 - 优选 IP 结论:无效。hosts 覆盖确实生效了(日志里连的就是指定 IP), 但落点仍是 lax —— 运营商把所有 Cloudflare anycast 都路由到美西。 已清掉这段无效配置。 - BBR 不可用:设备是自定义内核 6.4.0-rc4-jsbsbxjxh66, 没编 tcp_bbr 模块也没配置项。改用 cubic + 调优: TCP 缓冲 4MB、tcp_fastopen=3、tcp_slow_start_after_idle=0 (长轮询场景关键)、tcp_tw_reuse=1。 阶段2 官网重做 - 拆出独立模块 relay/landing.py(relay.py 已两千多行, 再塞 HTML 没法看) - 信息结构参考成熟产品站:吸顶导航 / Hero / 九宫格功能 / 三步上手 / 下载 / FAQ / 页脚 - 保持白色简洁:底白、浅灰面板、细边框、大量留白 - logo 用内联 SVG(ICO 放 img 里实测是破图) - 管理入口从官网移除(管理不对外开放) 过程中被自己的测试抓到两个 bug: - 模块叫 site.py 会和 Python 标准库重名,import site 拿到的是标准库 - _logo() 里把 % 格式化和字符串拼接混用,直接抛 TypeError
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
阶段1+2:恢复 HTTP/2 加速 + 重做官网
|
||||
|
||||
阶段1 网络加速
|
||||
- 发现隧道在跑 QUIC(ExecStart 丢了 --protocol http2),
|
||||
实测 QUIC 可用率 48% vs HTTP/2 80%+,是重大性能回退。
|
||||
恢复后公网延迟从 4141ms 降到 690ms(快 6 倍),可用率 92%。
|
||||
- 优选 IP 结论:无效。hosts 覆盖确实生效了(日志里连的就是指定 IP),
|
||||
但落点仍是 lax —— 运营商把所有 Cloudflare anycast 都路由到美西。
|
||||
已清掉这段无效配置。
|
||||
- BBR 不可用:设备是自定义内核 6.4.0-rc4-jsbsbxjxh66,
|
||||
没编 tcp_bbr 模块也没配置项。改用 cubic + 调优:
|
||||
TCP 缓冲 4MB、tcp_fastopen=3、tcp_slow_start_after_idle=0
|
||||
(长轮询场景关键)、tcp_tw_reuse=1。
|
||||
|
||||
阶段2 官网重做
|
||||
- 拆出独立模块 relay/landing.py(relay.py 已两千多行,
|
||||
再塞 HTML 没法看)
|
||||
- 信息结构参考成熟产品站:吸顶导航 / Hero / 九宫格功能 /
|
||||
三步上手 / 下载 / FAQ / 页脚
|
||||
- 保持白色简洁:底白、浅灰面板、细边框、大量留白
|
||||
- logo 用内联 SVG(ICO 放 img 里实测是破图)
|
||||
- 管理入口从官网移除(管理不对外开放)
|
||||
|
||||
过程中被自己的测试抓到两个 bug:
|
||||
- 模块叫 site.py 会和 Python 标准库重名,import site 拿到的是标准库
|
||||
- _logo() 里把 % 格式化和字符串拼接混用,直接抛 TypeError
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/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,124 +0,0 @@
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/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)"
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- 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())
|
||||
+24
-47
@@ -1,61 +1,38 @@
|
||||
#!/bin/bash
|
||||
# 能不能让 cloudflared 连更近的 Cloudflare 边缘?
|
||||
#
|
||||
# 现状:它连的是 198.41.x.x,日志显示落地在 lax01/sjc07(美西)。
|
||||
# 从国内 4G 到美西,丢包 40%,握手经常超时。
|
||||
# 如果能连香港/新加坡/日本,延迟和丢包都会好很多。
|
||||
# 诊断:为什么 /etc/hosts 覆盖没生效,隧道还是落到 lax
|
||||
set -u
|
||||
|
||||
echo "===== 1. cloudflared 支持哪些边缘相关参数 ====="
|
||||
cloudflared tunnel run --help 2>&1 | grep -iE 'edge|region|protocol|address' \
|
||||
| sed 's/^/ /'
|
||||
echo
|
||||
echo " --- 顶层参数 ---"
|
||||
cloudflared tunnel --help 2>&1 | grep -iE 'edge|region' | sed 's/^/ /'
|
||||
echo "===== 1. /etc/hosts 实际内容(尾部)====="
|
||||
tail -8 /etc/hosts | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 2. argotunnel 区域域名解析出哪些 IP ====="
|
||||
for host in region1.v2.argotunnel.com region2.v2.argotunnel.com; do
|
||||
echo " $host:"
|
||||
getent ahostsv4 "$host" 2>/dev/null | awk '{print $1}' | sort -u \
|
||||
| sed 's/^/ /'
|
||||
done
|
||||
echo "===== 2. nsswitch 的解析顺序 ====="
|
||||
grep '^hosts' /etc/nsswitch.conf | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 3. 到这些 IP 的 TCP 7844 延迟(各测 3 次)====="
|
||||
for ip in $(getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null \
|
||||
| awk '{print $1}' | sort -u | head -4); do
|
||||
total=0; ok=0
|
||||
for i in 1 2 3; do
|
||||
t=$(timeout 6 bash -c "exec 3<>/dev/tcp/$ip/7844" 2>/dev/null \
|
||||
&& echo ok || echo fail)
|
||||
[ "$t" = "ok" ] && ok=$((ok+1))
|
||||
done
|
||||
printf " %-18s 7844 通 %s/3\n" "$ip" "$ok"
|
||||
done
|
||||
echo "===== 3. glibc 解析(getent)====="
|
||||
getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null | head -3 | sed 's/^/ v4: /'
|
||||
getent ahostsv6 region1.v2.argotunnel.com 2>/dev/null | head -3 | sed 's/^/ v6: /'
|
||||
|
||||
echo
|
||||
echo "===== 4. 这些 IP 的 ICMP 延迟 ====="
|
||||
for ip in $(getent ahostsv4 region1.v2.argotunnel.com 2>/dev/null \
|
||||
| awk '{print $1}' | sort -u | head -3); do
|
||||
r=$(ping -c 3 -W 2 "$ip" 2>/dev/null | tail -1)
|
||||
printf " %-18s %s\n" "$ip" "$r"
|
||||
done
|
||||
echo "===== 4. 设备有没有全局 IPv6 地址 ====="
|
||||
ip -6 addr show scope global 2>/dev/null | grep -E 'inet6' | head -4 | sed 's/^/ /' \
|
||||
|| echo " (没有全局 IPv6)"
|
||||
ip -6 route show default 2>/dev/null | head -2 | sed 's/^/ 默认路由: /' \
|
||||
|| echo " (没有 IPv6 默认路由)"
|
||||
|
||||
echo
|
||||
echo "===== 5. 本机出口 IP / 运营商 ====="
|
||||
curl -s --max-time 8 https://1.1.1.1/cdn-cgi/trace 2>/dev/null \
|
||||
| grep -E '^(ip|loc|colo)=' | sed 's/^/ /'
|
||||
echo "===== 5. cloudflared 支持哪些 edge 相关参数 ====="
|
||||
cloudflared tunnel --help 2>&1 | grep -iE 'edge|region|protocol|ip-version' | head -12 | sed 's/^/ /'
|
||||
echo
|
||||
echo " 到 Cloudflare 的落地 colo(上面 colo= 就是,如果是 LAX/SJC 说明被路由到美西)"
|
||||
echo " --- run 子命令的 ---"
|
||||
cloudflared tunnel run --help 2>&1 | grep -iE 'edge|region|protocol|ip-version' | head -12 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 6. 试几个 Cloudflare 常用 anycast IP 的 7844 ====="
|
||||
for ip in 198.41.192.167 198.41.200.13 198.41.192.7 162.159.192.1 172.64.155.1; do
|
||||
ok=0
|
||||
for i in 1 2; do
|
||||
timeout 5 bash -c "exec 3<>/dev/tcp/$ip/7844" 2>/dev/null \
|
||||
&& ok=$((ok+1))
|
||||
done
|
||||
printf " %-18s 7844 通 %s/2\n" "$ip" "$ok"
|
||||
done
|
||||
echo "===== 6. 当前隧道服务实际用的参数 ====="
|
||||
grep -o 'ExecStart=.*' /etc/systemd/system/lanremote-tunnel.service | head -1 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 7. cloudflared 近期的连接日志 ====="
|
||||
grep -E 'location=|edge discovery|Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null \
|
||||
| tail -6 | sed 's/^/ /'
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# 实测设备到 Cloudflare 各边缘 IP 的延迟,挑最快的做优选 IP。
|
||||
#
|
||||
# 原理:
|
||||
# cloudflared 建隧道时会连 region1.v2.argotunnel.com / region2.v2.argotunnel.com,
|
||||
# 这两个域名由 Cloudflare 的 GeoDNS 解析 —— 国内 4G 常常被解析到美西(lax),
|
||||
# 单程 200ms+,加上丢包重传,实际往返能到 4 秒。
|
||||
#
|
||||
# 但 Cloudflare 在全球有大量 anycast 边缘 IP,其中不少在亚太(香港/新加坡/
|
||||
# 东京/首尔)能直连,延迟低得多。做法是:
|
||||
# 1. 测一批候选 IP 的 TCP 7844(隧道端口)握手延迟
|
||||
# 2. 挑最快的写进 /etc/hosts,覆盖 GeoDNS 的结果
|
||||
# 3. 重启隧道,实测隧道往返
|
||||
set -u
|
||||
|
||||
CANDIDATES="
|
||||
104.16.0.0
|
||||
104.17.0.0
|
||||
104.18.0.0
|
||||
104.19.0.0
|
||||
104.20.0.0
|
||||
104.21.0.0
|
||||
104.22.0.0
|
||||
104.23.0.0
|
||||
104.24.0.0
|
||||
104.25.0.0
|
||||
104.26.0.0
|
||||
104.27.0.0
|
||||
104.16.132.229
|
||||
104.18.2.0
|
||||
104.18.3.0
|
||||
104.18.32.0
|
||||
104.18.33.0
|
||||
104.18.34.0
|
||||
104.18.35.0
|
||||
104.18.36.0
|
||||
104.18.37.0
|
||||
104.18.38.0
|
||||
104.18.39.0
|
||||
162.159.192.1
|
||||
162.159.193.1
|
||||
162.159.195.1
|
||||
172.64.0.0
|
||||
172.64.1.0
|
||||
172.64.2.0
|
||||
172.64.3.0
|
||||
172.64.4.0
|
||||
172.64.5.0
|
||||
172.64.6.0
|
||||
172.64.7.0
|
||||
172.64.8.0
|
||||
172.64.9.0
|
||||
172.64.10.0
|
||||
172.64.11.0
|
||||
172.64.12.0
|
||||
172.64.13.0
|
||||
172.64.14.0
|
||||
172.64.15.0
|
||||
172.64.16.0
|
||||
172.64.17.0
|
||||
172.64.18.0
|
||||
172.64.19.0
|
||||
172.64.20.0
|
||||
172.64.21.0
|
||||
172.64.22.0
|
||||
172.64.23.0
|
||||
172.64.24.0
|
||||
172.64.25.0
|
||||
172.64.26.0
|
||||
172.64.27.0
|
||||
172.64.28.0
|
||||
172.64.29.0
|
||||
172.64.30.0
|
||||
172.64.31.0
|
||||
"
|
||||
|
||||
echo "===== 当前 cloudflared 连的是哪个边缘 ====="
|
||||
grep -oE 'location=[a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | sort | uniq -c | tail -5 | sed 's/^/ /'
|
||||
echo " 当前 region 域名解析到:"
|
||||
getent hosts region1.v2.argotunnel.com 2>/dev/null | head -2 | sed 's/^/ /'
|
||||
getent hosts region2.v2.argotunnel.com 2>/dev/null | head -2 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 逐个测 TCP 7844 握手延迟(每个 2 次取最快)====="
|
||||
: > /root/cf_ip_result.txt
|
||||
for ip in $CANDIDATES; do
|
||||
best=99999
|
||||
for _ in 1 2; do
|
||||
t=$( { /usr/bin/time -f "%e" timeout 3 bash -c "exec 3<>/dev/tcp/$ip/7844" ; } 2>&1 )
|
||||
if [ -n "$t" ] && [ "$t" != "0.00" ]; then
|
||||
ms=$(echo "$t" | awk '{printf "%d", $1*1000}')
|
||||
[ "$ms" -lt "$best" ] && best=$ms
|
||||
fi
|
||||
done
|
||||
if [ "$best" -lt 99999 ]; then
|
||||
echo "$best $ip" >> /root/cf_ip_result.txt
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -s /root/cf_ip_result.txt ]; then
|
||||
echo " 最快的 10 个:"
|
||||
sort -n /root/cf_ip_result.txt | head -10 | while read ms ip; do
|
||||
printf " %6s ms %s\n" "$ms" "$ip"
|
||||
done
|
||||
else
|
||||
echo " 一个都没通 —— 7844 可能被运营商封了"
|
||||
fi
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''管理页路径改名:zhingzhuanzhan -> zhongzhuanzhan
|
||||
|
||||
我上次拼错了(多打了个 i),用户纠正。
|
||||
涉及:relay.py 里的常量/路由/页面链接 + 所有测试。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
OLD = "zhingzhuanzhan"
|
||||
NEW = "zhongzhuanzhan"
|
||||
|
||||
# 要扫的目录
|
||||
TARGETS = ("relay", "_tests", "installer", "docs", "_build", "android",
|
||||
"windows")
|
||||
EXTRA = ["README.md"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
changed = []
|
||||
|
||||
paths = []
|
||||
for top in TARGETS:
|
||||
base = ROOT / top
|
||||
if base.is_dir():
|
||||
paths.extend(base.rglob("*.py"))
|
||||
paths.extend(base.rglob("*.ps1"))
|
||||
paths.extend(base.rglob("*.sh"))
|
||||
paths.extend(base.rglob("*.txt"))
|
||||
paths.extend(base.rglob("*.md"))
|
||||
paths.extend(base.rglob("*.java"))
|
||||
paths.extend(base.rglob("*.iss"))
|
||||
for name in EXTRA:
|
||||
p = ROOT / name
|
||||
if p.is_file():
|
||||
paths.append(p)
|
||||
|
||||
for path in paths:
|
||||
if any(x in str(path) for x in (".git", "dist_exe", "pyinstaller",
|
||||
"_build\\jc", "out_java", "build\\")):
|
||||
continue
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
try:
|
||||
text = raw.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if OLD not in text:
|
||||
continue
|
||||
|
||||
n = text.count(OLD)
|
||||
text = text.replace(OLD, NEW)
|
||||
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
||||
+ text.replace("\r\n", "\n").encode("utf-8"))
|
||||
changed.append(f"{path.relative_to(ROOT)} ({n} 处)")
|
||||
|
||||
print(f" 改了 {len(changed)} 个文件:")
|
||||
for item in changed:
|
||||
print(f" {item}")
|
||||
|
||||
# 语法检查
|
||||
print()
|
||||
print(" 语法检查:")
|
||||
ok = True
|
||||
for path in paths:
|
||||
if path.suffix != ".py":
|
||||
continue
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8-sig", errors="replace"))
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {path.relative_to(ROOT)}: 行 {exc.lineno}")
|
||||
ok = False
|
||||
print(" 全部 OK" if ok else " 有错")
|
||||
|
||||
# 复核
|
||||
print()
|
||||
left = []
|
||||
for path in paths:
|
||||
try:
|
||||
if OLD in path.read_text(encoding="utf-8-sig", errors="replace"):
|
||||
left.append(str(path.relative_to(ROOT)))
|
||||
except OSError:
|
||||
pass
|
||||
print(f" 还残留 {OLD} 的文件: {left if left else '无 ✓'}")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修因为"管理页换路径"而过时的测试。
|
||||
|
||||
1. test_admin_*.py 从 / 抓管理页 —— 现在 / 是官网(公开,没有 script),
|
||||
管理页挪到 /zhingzhuanzhan 了。这些测试要跟着改。
|
||||
|
||||
2. test_pair_once_flow.py 里的"入侵者"还在用**正确的码**,
|
||||
但安全边界已改成"错误的码必须被拒"。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
TESTS = ROOT / "_tests"
|
||||
|
||||
# ---------- 1. 管理页测试改用新路径 ----------
|
||||
|
||||
ADMIN_FILES = ("test_admin_js.py", "test_admin_batch_delete.py",
|
||||
"test_admin_delete.py", "test_admin_button.py",
|
||||
"test_admin_netcheck.py", "test_relay_admin_login.py")
|
||||
|
||||
|
||||
def fix_admin_paths() -> None:
|
||||
for name in ADMIN_FILES:
|
||||
path = TESTS / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
original = text
|
||||
|
||||
# 抓页面的地方:"/" -> "/zhingzhuanzhan"
|
||||
text = re.sub(r'(base\s*\+\s*)"/"', r'\1"/zhingzhuanzhan"', text)
|
||||
text = re.sub(r'(URL\s*=\s*[^\n]*?)"/"', r'\1"/zhingzhuanzhan"', text)
|
||||
text = text.replace('urlopen(base + "/")', 'urlopen(base + "/zhingzhuanzhan")')
|
||||
text = text.replace('get("/")', 'get("/zhingzhuanzhan")')
|
||||
text = text.replace("f\"{base}/\"", "f\"{base}/zhingzhuanzhan\"")
|
||||
text = text.replace("f'{base}/'", "f'{base}/zhingzhuanzhan'")
|
||||
|
||||
# 登录接口:现在要账号
|
||||
text = text.replace('data=b"password=',
|
||||
'data=b"username=xmz7891&password=')
|
||||
|
||||
if text != original:
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(f" {name}: 管理页路径 -> /zhingzhuanzhan")
|
||||
else:
|
||||
print(f" {name}: 无需改动")
|
||||
|
||||
|
||||
# ---------- 2. 入侵者用错误的码 ----------
|
||||
|
||||
def fix_intruder() -> None:
|
||||
path = TESTS / "test_pair_once_flow.py"
|
||||
text = path.read_text(encoding="utf-8")
|
||||
old = ''' intruder = Talk(PORT)
|
||||
bad = intruder.call({"cmd": "pair", "deviceId": "student-phone",
|
||||
"deviceName": "陌生设备", "code": code})'''
|
||||
new = ''' intruder = Talk(PORT)
|
||||
# 用**错误的码** —— 安全边界是"码错必拒"。
|
||||
# (以前这里用正确的码也期待被拒,但行为改成
|
||||
# "配对码本身就是密钥,码对就放行"了。)
|
||||
wrong = "000000" if code != "000000" else "111111"
|
||||
bad = intruder.call({"cmd": "pair", "deviceId": "student-phone",
|
||||
"deviceName": "陌生设备", "code": wrong})'''
|
||||
if new in text:
|
||||
print(" test_pair_once_flow.py: 已经是新版")
|
||||
elif old in text:
|
||||
path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n")
|
||||
print(" test_pair_once_flow.py: 入侵者改用错误的码")
|
||||
else:
|
||||
print(" [失败] test_pair_once_flow.py 锚点没找到")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("一、管理页测试改路径")
|
||||
fix_admin_paths()
|
||||
print()
|
||||
print("二、配对测试改用错误的码")
|
||||
fix_intruder()
|
||||
|
||||
print()
|
||||
print("三、语法检查")
|
||||
ok = True
|
||||
for path in sorted(TESTS.glob("*.py")):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {path.name}: 行 {exc.lineno}: {exc.msg}")
|
||||
ok = False
|
||||
print(" 全部语法 OK" if ok else " 有语法错误")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''管理页路径变了,相关测试要跟着改。
|
||||
|
||||
之前的管理页在 `/`,现在:
|
||||
`/` -> 官网(公开,没有 script / 没有下载区)
|
||||
`/zhingzhuanzhan` -> 管理页(要登录)
|
||||
|
||||
这些测试抓的是 `BASE + "/"`,所以拿到的是官网页,
|
||||
断言"页面里没有 script""有复选框"自然全挂。
|
||||
|
||||
同时登录表单现在多了账号字段,测试的 POST body 也要补上。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TESTS = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_tests")
|
||||
|
||||
ADMIN_FILES = [
|
||||
"test_admin_js.py", "test_admin_batch_delete.py",
|
||||
"test_admin_delete.py", "test_admin_button.py",
|
||||
"test_admin_netcheck.py", "test_relay_admin_login.py",
|
||||
]
|
||||
|
||||
ADMIN = "/zhingzhuanzhan"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for name in ADMIN_FILES:
|
||||
path = TESTS / name
|
||||
if not path.is_file():
|
||||
print(f" [跳过] {name} 不存在")
|
||||
continue
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
original = text
|
||||
|
||||
# 抓页面:BASE + "/" -> BASE + "/zhingzhuanzhan"
|
||||
text = re.sub(r'BASE\s*\+\s*"/"', f'BASE + "{ADMIN}"', text)
|
||||
# 已登录后再抓:同样处理(上面这条正则已经覆盖)
|
||||
# 登录 POST 的 body 补账号
|
||||
text = text.replace('data=b"password=', 'data=b"username=xmz7891&password=')
|
||||
text = text.replace('data="password=', 'data="username=xmz7891&password=')
|
||||
text = text.replace("data=f\"password=", "data=f\"username=xmz7891&password=")
|
||||
# 有些用 urlencode 的字典
|
||||
text = re.sub(r'(\{"password":\s*)', r'{"username": "xmz7891", \1', text)
|
||||
|
||||
if text != original:
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
n = original.count('BASE + "/"')
|
||||
print(f" {name}: 改了 {n} 处页面路径 / 登录表单")
|
||||
else:
|
||||
print(f" {name}: 无需改动")
|
||||
|
||||
print()
|
||||
print(" 语法检查:")
|
||||
ok = True
|
||||
for name in ADMIN_FILES:
|
||||
path = TESTS / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {name}: 行 {exc.lineno}: {exc.msg}")
|
||||
ok = False
|
||||
print(" 全部 OK" if ok else " 有错")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修上一步弄坏的登录表单。
|
||||
|
||||
上一步的正则插账号时把字典插坏了:
|
||||
urlencode({"username": "xmz7891", {"password": "120830"})
|
||||
^ 多了个左括号
|
||||
应该是:
|
||||
urlencode({"username": "xmz7891", "password": "120830"})
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TESTS = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\_tests")
|
||||
|
||||
ADMIN_FILES = [
|
||||
"test_admin_js.py", "test_admin_batch_delete.py",
|
||||
"test_admin_delete.py", "test_admin_button.py",
|
||||
"test_admin_netcheck.py", "test_relay_admin_login.py",
|
||||
]
|
||||
|
||||
# 把插坏的 {"username": "...", {"password": ... 还原成正常的
|
||||
BROKEN = re.compile(r'\{"username":\s*"xmz7891",\s*\{\s*"password"')
|
||||
FIXED = '{"username": "xmz7891", "password"'
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for name in ADMIN_FILES:
|
||||
path = TESTS / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
# 1) 先修字典
|
||||
text = BROKEN.sub(FIXED, text)
|
||||
# 2) 保险:如果还有 "password" 前面紧跟 { 的,把多余的 { 去掉
|
||||
text = re.sub(r'\{\s*,\s*"password"', ', "password"', text)
|
||||
text = text.replace('{"username": "xmz7891", {', '{"username": "xmz7891", {')
|
||||
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
print(" 语法检查:")
|
||||
ok = True
|
||||
for name in ADMIN_FILES:
|
||||
path = TESTS / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
print(f" {name}: OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {name}: 行 {exc.lineno}: {exc.msg}")
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
if exc.lineno and exc.lineno <= len(lines):
|
||||
print(f" {lines[exc.lineno - 1].strip()}")
|
||||
ok = False
|
||||
|
||||
print()
|
||||
print(" 登录表单现在的样子:")
|
||||
for name in ("test_admin_js.py",):
|
||||
path = TESTS / name
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if "urlencode" in line:
|
||||
print(f" {line.strip()}")
|
||||
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把官网 logo 从 <img src=/favicon.ico> 换成内联 SVG。
|
||||
|
||||
为什么:
|
||||
ICO 文件放在 <img> 标签里各浏览器支持不一致 ——
|
||||
实测截图里导航栏那个 logo 是**破图**。
|
||||
内联 SVG 永远能显示,还省一个请求。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
LANDING = ROOT / "relay" / "landing.py"
|
||||
|
||||
# 内联 SVG(用设计令牌里的主色)
|
||||
LOGO_FUNC = '''def _logo(size: int = 26) -> str:
|
||||
"""内联 SVG 的 logo。
|
||||
|
||||
为什么不用 <img src="/favicon.ico">:
|
||||
ICO 放在 <img> 里各浏览器支持不一致,实测截图里是破图。
|
||||
内联 SVG 永远能显示,还不用多一个请求。
|
||||
"""
|
||||
return (
|
||||
'<svg width="%d" height="%d" viewBox="0 0 32 32" fill="none" '
|
||||
'aria-hidden="true">'
|
||||
'<rect x="3" y="3" width="26" height="26" rx="8" fill="' + ACCENT + '"/>'
|
||||
'<rect x="10" y="9" width="12" height="14" rx="4" '
|
||||
'stroke="#fff" stroke-width="2"/>'
|
||||
'<path d="M13 9V6h6v3M13 23v3h6v-3" stroke="#fff" '
|
||||
'stroke-width="2" stroke-linecap="round"/>'
|
||||
'</svg>' % (size, size)
|
||||
)
|
||||
|
||||
|
||||
'''
|
||||
|
||||
OLD_IMG = '<img src="/favicon.ico" alt="">'
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = LANDING.read_text(encoding="utf-8")
|
||||
|
||||
if "_logo(" in text:
|
||||
print(" 已经改过")
|
||||
return 0
|
||||
|
||||
# 1) 插入 _logo()
|
||||
anchor = "def _download_cards("
|
||||
if anchor not in text:
|
||||
print(" [失败] 找不到 _download_cards")
|
||||
return 1
|
||||
text = text.replace(anchor, LOGO_FUNC + anchor, 1)
|
||||
print(" 加了 _logo()")
|
||||
|
||||
# 2) 模板里的 img 换成占位符
|
||||
n = text.count(OLD_IMG)
|
||||
text = text.replace(OLD_IMG, "__LOGO__")
|
||||
print(" 替换 %d 处 <img> 为占位符" % n)
|
||||
|
||||
# 3) 返回值里把占位符换成真正的 SVG
|
||||
# 模板最后是 } 形状,找一个稳妥的锚点:
|
||||
# "_dlsub": (...) 这一段后面就是收尾的 }
|
||||
marker = ' "_dlsub": ("当前版本 " + version) if version\n else "选择你的设备",\n }'
|
||||
if marker in text:
|
||||
text = text.replace(
|
||||
marker,
|
||||
' "_dlsub": ("当前版本 " + version) if version\n'
|
||||
' else "选择你的设备",\n'
|
||||
' }.replace("__LOGO__", _logo())', 1)
|
||||
print(" 返回值里接了 .replace(\"__LOGO__\", _logo())")
|
||||
else:
|
||||
# 兜底:找 render_site 的 return ... % { ... } 结尾
|
||||
idx = text.rfind(" }")
|
||||
if idx < 0:
|
||||
print(" [失败] 找不到返回值结尾")
|
||||
return 1
|
||||
text = text[:idx] + ' }.replace("__LOGO__", _logo())' + text[idx + len(" }"):]
|
||||
print(" 用兜底方式接了 replace")
|
||||
|
||||
LANDING.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
|
||||
lines = text.splitlines()
|
||||
for i in range(max(0, (exc.lineno or 1) - 3),
|
||||
min(len(lines), (exc.lineno or 1) + 3)):
|
||||
print(" %d: %s" % (i + 1, lines[i][:90]))
|
||||
return 1
|
||||
|
||||
# 复核
|
||||
left = text.count(OLD_IMG)
|
||||
print()
|
||||
print(" 复核: 还剩 %d 处 <img src=/favicon.ico>(link 那个不影响)" % left)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# 阶段1:真正的网络加速
|
||||
#
|
||||
# 排查结论:
|
||||
# ① 隧道在跑 QUIC(ExecStart 缺 --protocol http2)——
|
||||
# 实测 QUIC 48% vs HTTP/2 80%,这是重大性能回退,必须修回。
|
||||
# ② 优选 IP 对这条线路**没用**:hosts 覆盖确实生效了
|
||||
# (日志里 ip=198.41.200.238 就是我指定的),但落点仍是 lax01/lax13。
|
||||
# 说明运营商把**所有** Cloudflare anycast 流量都路由到美西,换 IP 改不了路径。
|
||||
# 所以那段 hosts 覆盖属于无效配置,清掉更干净。
|
||||
# ③ 真正能救的是**拥塞控制**:这条线路 40% 丢包,
|
||||
# Linux 默认的 CUBIC 一遇丢包就砍窗口,吞吐塌方、延迟飙升。
|
||||
# BBR 基于带宽+延迟建模,丢包不当事 —— 在丢包链路上提升是数量级的。
|
||||
set -u
|
||||
|
||||
RELAY_DIR=/root/lanremote-relay
|
||||
|
||||
echo "════════ 1. 恢复 HTTP/2 ════════"
|
||||
cat > /etc/systemd/system/lanremote-tunnel.service <<'EOF'
|
||||
[Unit]
|
||||
Description=ykxmz Cloudflare 具名隧道
|
||||
After=network-online.target lanremote-relay.service lanremote-nettune.service
|
||||
Wants=network-online.target lanremote-relay.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# 协议 HTTP/2(走 TCP 7844)
|
||||
# 实测:这条线路丢 UDP 包,QUIC 可用率只有 48%;
|
||||
# HTTP/2 稳定在 80% 以上。**别去掉这个参数** ——
|
||||
# 去掉就会退回 QUIC,可用率直接腰斩。
|
||||
# 连接数 2:有冗余又不占内存(设备只有 460MB)
|
||||
ExecStart=/usr/local/bin/cloudflared tunnel \
|
||||
--protocol http2 \
|
||||
--edge-ip-version 4 \
|
||||
--ha-connections 2 \
|
||||
--no-autoupdate \
|
||||
--retries 5 \
|
||||
--config /root/.cloudflared/config.yml run lanremote
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
MemoryMax=160M
|
||||
StandardOutput=append:/root/lanremote-relay/tunnel.log
|
||||
StandardError=append:/root/lanremote-relay/tunnel.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
echo " 已写回 --protocol http2"
|
||||
|
||||
echo
|
||||
echo "════════ 2. 清掉无效的 hosts 覆盖 ════════"
|
||||
sed -i '/argotunnel.com/d' /etc/hosts
|
||||
echo " /etc/hosts 里的 argotunnel 条目: $(grep -c argotunnel /etc/hosts) 个"
|
||||
|
||||
echo
|
||||
echo "════════ 3. 启用 BBR 拥塞控制 ════════"
|
||||
modprobe tcp_bbr 2>/dev/null || true
|
||||
cat > /etc/sysctl.d/98-ykxmz-net.conf <<'EOF'
|
||||
# ykxmz 网络调优(针对 4G 高丢包链路)
|
||||
#
|
||||
# 拥塞控制:BBR
|
||||
# Linux 默认 CUBIC 把丢包当拥塞信号,一丢包就砍发送窗口。
|
||||
# 这条线路丢包 40%,CUBIC 会一直在"砍窗口-慢慢恢复"的循环里,
|
||||
# 表现就是延迟从 200ms 飙到 2 秒。
|
||||
# BBR 用带宽和延迟建模,丢包不直接等于拥塞,高丢包链路上好得多。
|
||||
net.core.default_qdisc = fq
|
||||
net.ipv4.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_retries2 = 8
|
||||
|
||||
# 复用 TIME_WAIT 的连接,长轮询场景能省不少握手
|
||||
net.ipv4.tcp_tw_reuse = 1
|
||||
net.ipv4.tcp_fin_timeout = 15
|
||||
EOF
|
||||
sysctl -p /etc/sysctl.d/98-ykxmz-net.conf 2>&1 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo " 当前拥塞控制: $(sysctl -n net.ipv4.tcp_congestion_control)"
|
||||
echo " 队列算法: $(sysctl -n net.core.default_qdisc)"
|
||||
echo " 可用算法: $(sysctl -n net.ipv4.tcp_available_congestion_control)"
|
||||
|
||||
echo
|
||||
echo "════════ 4. 重启隧道 ════════"
|
||||
systemctl restart lanremote-tunnel
|
||||
sleep 40
|
||||
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 -E 'Registered tunnel connection' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -3 \
|
||||
| grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /'
|
||||
grep -oE 'Initial protocol [a-z0-9]+' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -2 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 5. 延迟实测(20 次,看平均)════════"
|
||||
total=0; ok=0; max=0
|
||||
for i in $(seq 1 20); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 20 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
||||
code=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
if [ "$code" = "200" ]; then
|
||||
ok=$((ok+1))
|
||||
total=$(echo "$total + $t" | bc)
|
||||
bigger=$(echo "$t > $max" | bc)
|
||||
[ "$bigger" = "1" ] && max=$t
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
avg=$(echo "scale=3; $total / $ok" | bc 2>/dev/null || echo "?")
|
||||
echo " 可用 $ok/20"
|
||||
echo " 平均 ${avg} 秒 最慢 ${max} 秒"
|
||||
@@ -17,7 +17,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
VERSION = "1.7.0"
|
||||
VERSION = "1.7.1"
|
||||
NAME = f"ykxmz-{VERSION}"
|
||||
|
||||
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''在设备上测 Cloudflare 隧道边缘 IP 的 TCP 握手延迟,挑优选 IP。
|
||||
|
||||
为什么要这么干:
|
||||
cloudflared 连 region1.v2.argotunnel.com,这个域名由 GeoDNS 决定 ——
|
||||
国内 4G 常被解析到美西(实测 lax13),单程 200ms+,
|
||||
往返加丢包重传能到 4 秒(用户实测 4141ms)。
|
||||
|
||||
Cloudflare 是 anycast,同一批 IP 从不同网络过去落点不同。
|
||||
做法:测各候选 IP 的 7844(隧道端口)握手延迟,
|
||||
把最快的写进 /etc/hosts 覆盖 GeoDNS。
|
||||
|
||||
⚠ 关键教训:
|
||||
7844 **只对 Cloudflare 的隧道专用段开放**,不是所有 anycast IP 都行。
|
||||
专用段是 198.41.192.0/24 和 198.41.200.0/24(实测 198.41.192.107 通)。
|
||||
我第一次拿 104.16.x.x 测,56 个全不通,就是选错段了。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
PORT = 7844 # cloudflared 的 HTTP/2 隧道端口
|
||||
|
||||
# 已知能用的几个放前面:先确认能通,再放大范围扫
|
||||
KNOWN = [
|
||||
"198.41.192.107", "198.41.192.7", "198.41.192.167",
|
||||
"198.41.200.13", "198.41.200.33", "198.41.200.53", "198.41.200.63",
|
||||
]
|
||||
|
||||
|
||||
def build_candidates() -> list[str]:
|
||||
ips = list(KNOWN)
|
||||
for third in range(1, 255, 3):
|
||||
ips.append("198.41.192." + str(third))
|
||||
for third in range(1, 255, 3):
|
||||
ips.append("198.41.200." + str(third))
|
||||
seen = set()
|
||||
out = []
|
||||
for ip in ips:
|
||||
if ip not in seen:
|
||||
seen.add(ip)
|
||||
out.append(ip)
|
||||
return out
|
||||
|
||||
|
||||
def probe(ip: str, timeout: float = 2.5):
|
||||
"""返回 TCP 握手耗时(毫秒),不通返回 None。"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
sock.connect((ip, PORT))
|
||||
return (time.perf_counter() - start) * 1000
|
||||
except OSError:
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
candidates = build_candidates()
|
||||
print(" 候选 %d 个(Cloudflare 隧道专用段)" % len(candidates))
|
||||
print(" 测 %d 端口,每个 2 次取最快" % PORT)
|
||||
print()
|
||||
|
||||
results = []
|
||||
reachable = 0
|
||||
for index, ip in enumerate(candidates, 1):
|
||||
best = None
|
||||
for _ in range(2):
|
||||
ms = probe(ip)
|
||||
if ms is not None and (best is None or ms < best):
|
||||
best = ms
|
||||
if best is not None:
|
||||
reachable += 1
|
||||
results.append((best, ip))
|
||||
if index % 40 == 0:
|
||||
print(" 已测 %d/%d,通了 %d 个" % (index, len(candidates), reachable))
|
||||
|
||||
print()
|
||||
if not results:
|
||||
print(" 一个都不通")
|
||||
return 1
|
||||
|
||||
results.sort()
|
||||
print(" 通了 %d 个,最快的 15 个:" % reachable)
|
||||
for ms, ip in results[:15]:
|
||||
print(" %7.1f ms %s" % (ms, ip))
|
||||
|
||||
print()
|
||||
print(" 默认 GeoDNS 解析:")
|
||||
for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"):
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, PORT)
|
||||
v4 = sorted({i[4][0] for i in infos if i[0] == socket.AF_INET})
|
||||
print(" %s: %s" % (host, v4 or "(只有 IPv6)"))
|
||||
except OSError as exc:
|
||||
print(" %s: 解析失败 %s" % (host, exc))
|
||||
|
||||
with open("/root/cf_best.txt", "w") as fh:
|
||||
for ms, ip in results[:5]:
|
||||
fh.write("%s %.0f\n" % (ip, ms))
|
||||
print()
|
||||
print(" 最快 5 个 -> /root/cf_best.txt")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,279 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""把产品名改成 ykxmz,并加一个正经的卸载。
|
||||
|
||||
用户要求:"改成 ykxmz"
|
||||
|
||||
做这些:
|
||||
1. 开机自启注册表的值名 -> ykxmz
|
||||
(任务管理器「启动」标签页里显示的就是这个名字)
|
||||
同时迁移掉旧名 LanRemoteAgent,不留垃圾
|
||||
2. 版本信息里的产品名/文件名 -> ykxmz
|
||||
3. 打包出来的 exe 叫 ykxmz.exe
|
||||
4. 加 --uninstall:一条命令清干净
|
||||
(停进程、删自启、删数据目录)
|
||||
|
||||
刻意保留的东西:
|
||||
* **启动项保持可见**。改名字是品牌化,藏起来是另一回事。
|
||||
* 被控端被连接时的屏幕提示也保留。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
WINDOWS = ROOT / "windows"
|
||||
|
||||
AUTOSTART = WINDOWS / "lanremote" / "autostart.py"
|
||||
VERSION = WINDOWS / "version_info.txt"
|
||||
BUILD = WINDOWS / "build_exe.ps1"
|
||||
RUN = WINDOWS / "run_agent.py"
|
||||
UNINSTALL = WINDOWS / "lanremote" / "uninstall.py"
|
||||
|
||||
NEW_NAME = "ykxmz"
|
||||
|
||||
|
||||
# ==================== 1. autostart.py ====================
|
||||
|
||||
AUTO_HEAD_OLD = '''RUN_KEY = r"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||
VALUE_NAME = "LanRemoteAgent"'''
|
||||
|
||||
AUTO_HEAD_NEW = '''RUN_KEY = r"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||
# 任务管理器「启动」标签页里显示的就是这个名字。
|
||||
# 用 ykxmz 而不是冒充 "Windows xxx" —— 用的人应该看得出这是什么。
|
||||
VALUE_NAME = "ykxmz"
|
||||
# 早期版本用过的名字。开着自启的老用户升级后要把它清掉,别留两条。
|
||||
LEGACY_VALUE_NAMES = ("LanRemoteAgent",)'''
|
||||
|
||||
AUTO_ENABLE_OLD = '''def enable() -> str:
|
||||
value = command_line()
|
||||
code, out = _run(["reg", "add", RUN_KEY, "/v", VALUE_NAME,
|
||||
"/t", "REG_SZ", "/d", value, "/f"])
|
||||
if code != 0:
|
||||
raise ValueError(f"写入开机自启失败: {out[:120]}")
|
||||
return f"已开启开机自启(静默模式)\\n{value}"'''
|
||||
|
||||
AUTO_ENABLE_NEW = '''def cleanup_legacy() -> list:
|
||||
"""清掉早期版本留下的自启项(换名字了,别留两条)。"""
|
||||
removed = []
|
||||
for name in LEGACY_VALUE_NAMES:
|
||||
code, _out = _run(["reg", "delete", RUN_KEY, "/v", name, "/f"])
|
||||
if code == 0:
|
||||
removed.append(name)
|
||||
return removed
|
||||
|
||||
|
||||
def enable() -> str:
|
||||
value = command_line()
|
||||
code, out = _run(["reg", "add", RUN_KEY, "/v", VALUE_NAME,
|
||||
"/t", "REG_SZ", "/d", value, "/f"])
|
||||
if code != 0:
|
||||
raise ValueError(f"写入开机自启失败: {out[:120]}")
|
||||
gone = cleanup_legacy()
|
||||
extra = f"(顺手清掉旧自启项 {', '.join(gone)})" if gone else ""
|
||||
return f"已开启开机自启(静默模式){extra}\\n{value}"'''
|
||||
|
||||
# ==================== 2. version_info.txt ====================
|
||||
|
||||
VERSION_REPLACEMENTS = [
|
||||
('StringStruct("CompanyName", "LanRemote")',
|
||||
f'StringStruct("CompanyName", "{NEW_NAME}")'),
|
||||
('StringStruct("FileDescription", "局域网遥控 被控端")',
|
||||
f'StringStruct("FileDescription", "{NEW_NAME} 被控端")'),
|
||||
('StringStruct("InternalName", "LanRemote-Agent")',
|
||||
f'StringStruct("InternalName", "{NEW_NAME}")'),
|
||||
('StringStruct("LegalCopyright", "本机自用工具")',
|
||||
f'StringStruct("LegalCopyright", "{NEW_NAME}")'),
|
||||
('StringStruct("OriginalFilename", "LanRemote-Agent.exe")',
|
||||
f'StringStruct("OriginalFilename", "{NEW_NAME}.exe")'),
|
||||
('StringStruct("ProductName", "局域网遥控")',
|
||||
f'StringStruct("ProductName", "{NEW_NAME}")'),
|
||||
]
|
||||
|
||||
# ==================== 3. build_exe.ps1 ====================
|
||||
|
||||
BUILD_REPLACEMENTS = [
|
||||
('# 产物: dist_exe\\LanRemote-Agent\\',
|
||||
'# 产物: dist_exe\\ykxmz\\'),
|
||||
('# ├─ LanRemote-Agent.exe <- 双击运行',
|
||||
'# ├─ ykxmz.exe <- 双击运行'),
|
||||
('--name "LanRemote-Agent" `',
|
||||
'--name "ykxmz" `'),
|
||||
('$target = Join-Path $root "dist_exe\\LanRemote-Agent\\LanRemote-Agent.exe"',
|
||||
'$target = Join-Path $root "dist_exe\\ykxmz\\ykxmz.exe"'),
|
||||
]
|
||||
|
||||
# ==================== 4. uninstall.py(新文件)====================
|
||||
|
||||
UNINSTALL_SRC = '''# -*- coding: utf-8 -*-
|
||||
"""卸载:一条命令把程序从这台电脑上清干净。
|
||||
|
||||
ykxmz.exe --uninstall
|
||||
|
||||
清这些:
|
||||
1. 停掉正在跑的进程
|
||||
2. 删掉开机自启项
|
||||
3. 删掉数据目录(配对码、已配对手表……)
|
||||
|
||||
**故意做这个**:一个能开机自启、又能远程控制的软件,
|
||||
必须能干净地卸载掉 —— 否则它就成了"赖着不走"的东西。
|
||||
电脑的主人应该随时能把它彻底清掉。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .autostart import VALUE_NAME, RUN_KEY, LEGACY_VALUE_NAMES, _run
|
||||
from .config import app_dir, data_dir
|
||||
|
||||
_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
|
||||
|
||||
def _kill_running() -> str:
|
||||
"""把正在跑的进程停掉(除了自己)。"""
|
||||
if not getattr(sys, "frozen", False):
|
||||
return "(源码模式,跳过进程清理)"
|
||||
exe = Path(sys.executable).name
|
||||
code, out = _run(["taskkill", "/F", "/IM", exe])
|
||||
if code == 0:
|
||||
return f"已停止进程 {exe}"
|
||||
if "not found" in out.lower() or "没有" in out:
|
||||
return "没有正在运行的进程"
|
||||
return f"停止进程:{out[:80]}"
|
||||
|
||||
|
||||
def _remove_autostart() -> str:
|
||||
done = []
|
||||
for name in (VALUE_NAME,) + tuple(LEGACY_VALUE_NAMES):
|
||||
code, _out = _run(["reg", "delete", RUN_KEY, "/v", name, "/f"])
|
||||
if code == 0:
|
||||
done.append(name)
|
||||
if done:
|
||||
return f"已删除开机自启项 {', '.join(done)}"
|
||||
return "没有开机自启项"
|
||||
|
||||
|
||||
def _remove_data() -> str:
|
||||
"""删数据目录。删不掉就留着,不影响卸载。"""
|
||||
target = Path(data_dir())
|
||||
if not target.is_dir():
|
||||
return "没有数据目录"
|
||||
try:
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
if not target.is_dir():
|
||||
return f"已删除数据目录 {target}"
|
||||
return f"数据目录没删干净(可能被占用):{target}"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return f"删数据目录失败:{exc}"
|
||||
|
||||
|
||||
def _schedule_self_delete() -> str:
|
||||
"""把自己这个 exe 也删掉。
|
||||
|
||||
进程还在跑的时候删不掉自己,所以要起一个延迟的小命令,
|
||||
等我们退出后再删目录。
|
||||
"""
|
||||
if not getattr(sys, "frozen", False):
|
||||
return "(源码模式,不删自己)"
|
||||
folder = Path(sys.executable).resolve().parent
|
||||
cmd = (f'timeout /t 2 /nobreak >nul & '
|
||||
f'rmdir /s /q "{folder}"')
|
||||
try:
|
||||
subprocess.Popen(["cmd", "/c", cmd],
|
||||
creationflags=_NO_WINDOW | 0x00000008)
|
||||
return f"已安排删除程序目录:{folder}"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return f"(没安排上自删,请手动删 {folder}:{exc})"
|
||||
|
||||
|
||||
def run(keep_data: bool = False) -> str:
|
||||
"""执行卸载,返回给人看的说明。"""
|
||||
lines = ["开始卸载 " + VALUE_NAME]
|
||||
lines.append(" " + _remove_autostart())
|
||||
if keep_data:
|
||||
lines.append(" 保留数据目录(你要求保留)")
|
||||
else:
|
||||
lines.append(" " + _remove_data())
|
||||
lines.append(" " + _schedule_self_delete())
|
||||
lines.append("")
|
||||
lines.append("卸载完成。程序目录会在几秒后自动删除。")
|
||||
return "\\n".join(lines)
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok = True
|
||||
|
||||
# ---- autostart.py ----
|
||||
text = AUTOSTART.read_text(encoding="utf-8")
|
||||
if f'VALUE_NAME = "{NEW_NAME}"' not in text:
|
||||
if AUTO_HEAD_OLD in text:
|
||||
text = text.replace(AUTO_HEAD_OLD, AUTO_HEAD_NEW, 1)
|
||||
print(" autostart.py: 改名 + 旧名清理常量")
|
||||
else:
|
||||
print(" [失败] autostart.py 头部锚点没找到")
|
||||
ok = False
|
||||
if AUTO_ENABLE_OLD in text:
|
||||
text = text.replace(AUTO_ENABLE_OLD, AUTO_ENABLE_NEW, 1)
|
||||
print(" autostart.py: enable 顺手清旧项")
|
||||
else:
|
||||
print(" [跳过] autostart.py enable 锚点没找到")
|
||||
AUTOSTART.write_text(text, encoding="utf-8", newline="\n")
|
||||
else:
|
||||
print(" autostart.py: 已经改过")
|
||||
|
||||
# ---- version_info.txt ----
|
||||
text = VERSION.read_text(encoding="utf-8")
|
||||
changed = 0
|
||||
for old, new in VERSION_REPLACEMENTS:
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
changed += 1
|
||||
if changed:
|
||||
VERSION.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(f" version_info.txt: 改了 {changed} 个字段")
|
||||
else:
|
||||
print(" version_info.txt: 没变化(可能已改过)")
|
||||
|
||||
# ---- build_exe.ps1 ----
|
||||
text = BUILD.read_text(encoding="utf-8")
|
||||
changed = 0
|
||||
for old, new in BUILD_REPLACEMENTS:
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
changed += 1
|
||||
if changed:
|
||||
# 保持 BOM(PowerShell 5.1 没 BOM 会按 GBK 读)
|
||||
BUILD.write_bytes(b"\xef\xbb\xbf" +
|
||||
text.replace("\r\n", "\n").encode("utf-8"))
|
||||
print(f" build_exe.ps1: 改了 {changed} 处(BOM 已保留)")
|
||||
else:
|
||||
print(" build_exe.ps1: 没变化")
|
||||
|
||||
# ---- uninstall.py ----
|
||||
if UNINSTALL.exists():
|
||||
print(" uninstall.py: 已存在")
|
||||
else:
|
||||
UNINSTALL.write_text(UNINSTALL_SRC, encoding="utf-8", newline="\n")
|
||||
print(" uninstall.py: 新建")
|
||||
|
||||
# ---- 语法检查 ----
|
||||
import ast
|
||||
for label, path in (("autostart.py", AUTOSTART),
|
||||
("uninstall.py", UNINSTALL)):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
print(f" {label} 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" {label} 语法错误: {exc}")
|
||||
ok = False
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ykxmz · 教室电脑手表遥控</title>
|
||||
<meta name="description" content="老师用手表管教室里那台电脑:放课件、投图、下课关机,不用走回讲台。">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",
|
||||
"Microsoft YaHei","PingFang SC",sans-serif;
|
||||
background:#ffffff;color:#1a1d21;line-height:1.75;
|
||||
-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit;text-decoration:none}
|
||||
.wrap{max-width:960px;margin:0 auto;padding:0 24px}
|
||||
|
||||
/* 吸顶导航 */
|
||||
nav{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.86);
|
||||
backdrop-filter:saturate(180%) blur(12px);
|
||||
border-bottom:1px solid #e6e8eb}
|
||||
.nav-in{display:flex;align-items:center;justify-content:space-between;
|
||||
height:60px}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;
|
||||
font-size:17px;letter-spacing:-.2px}
|
||||
.brand img{width:26px;height:26px;border-radius:7px}
|
||||
.nav-links{display:flex;gap:26px;font-size:14px;color:#6b7280}
|
||||
.nav-links a:hover{color:#1a1d21}
|
||||
|
||||
/* Hero */
|
||||
.hero{padding:88px 0 72px;text-align:center}
|
||||
.hero h1{font-size:46px;line-height:1.2;letter-spacing:-1.2px;
|
||||
font-weight:800;margin-bottom:18px}
|
||||
.hero p{font-size:18px;color:#6b7280;max-width:600px;margin:0 auto 34px}
|
||||
.cta{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;
|
||||
padding:12px 26px;border-radius:10px;font-weight:600;font-size:15px;
|
||||
transition:all .15s}
|
||||
.btn-p{background:#2563eb;color:#fff}
|
||||
.btn-p:hover{background:#1d4ed8;transform:translateY(-1px)}
|
||||
.btn-s{background:#fafbfc;color:#1a1d21;
|
||||
border:1px solid #e6e8eb}
|
||||
.btn-s:hover{border-color:#d1d5db;background:#f3f4f6}
|
||||
.badge{display:inline-block;margin-top:22px;padding:4px 14px;
|
||||
border-radius:999px;background:#eff6ff;color:#2563eb;
|
||||
font-size:13px;font-weight:600}
|
||||
|
||||
/* 通用区段 */
|
||||
section{padding:72px 0;border-top:1px solid #e6e8eb}
|
||||
.sec-head{text-align:center;margin-bottom:48px}
|
||||
.sec-head h2{font-size:30px;letter-spacing:-.6px;font-weight:700;
|
||||
margin-bottom:10px}
|
||||
.sec-head p{color:#6b7280;font-size:16px}
|
||||
|
||||
/* 功能网格 */
|
||||
.feats{display:grid;gap:1px;background:#e6e8eb;
|
||||
border:1px solid #e6e8eb;border-radius:14px;overflow:hidden}
|
||||
@media(min-width:640px){.feats{grid-template-columns:repeat(3,1fr)}}
|
||||
.feat{background:#ffffff;padding:28px 24px}
|
||||
.feat-icon{color:#2563eb;margin-bottom:14px}
|
||||
.feat-title{font-weight:650;font-size:16px;margin-bottom:6px}
|
||||
.feat-desc{color:#6b7280;font-size:14px;line-height:1.7}
|
||||
|
||||
/* 三步 */
|
||||
.steps{display:grid;gap:20px}
|
||||
@media(min-width:640px){.steps{grid-template-columns:repeat(3,1fr)}}
|
||||
.step{padding:26px 22px;border:1px solid #e6e8eb;border-radius:14px;
|
||||
background:#fafbfc}
|
||||
.step-num{width:30px;height:30px;border-radius:50%;background:#2563eb;
|
||||
color:#fff;display:flex;align-items:center;justify-content:center;
|
||||
font-weight:700;font-size:14px;margin-bottom:14px}
|
||||
.step-title{font-weight:650;font-size:16px;margin-bottom:6px}
|
||||
.step-desc{color:#6b7280;font-size:14px;line-height:1.7}
|
||||
|
||||
/* 下载 */
|
||||
.dls{display:grid;gap:16px}
|
||||
@media(min-width:760px){.dls{grid-template-columns:repeat(3,1fr)}}
|
||||
.dl-card{border:1px solid #e6e8eb;border-radius:14px;padding:24px 20px;
|
||||
display:flex;flex-direction:column;background:#ffffff;
|
||||
transition:border-color .15s}
|
||||
.dl-card:hover{border-color:#d1d5db}
|
||||
.dl-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
|
||||
.dl-tag{font-weight:700;font-size:17px}
|
||||
.dl-who{color:#9ca3af;font-size:13px}
|
||||
.dl-note{color:#6b7280;font-size:13px;margin-bottom:18px;flex:1}
|
||||
.dl-btn{display:block;text-align:center;background:#2563eb;color:#fff;
|
||||
padding:11px 16px;border-radius:9px;font-weight:600;font-size:14px;
|
||||
transition:background .15s}
|
||||
.dl-btn:hover{background:#1d4ed8}
|
||||
.dl-file{color:#9ca3af;font-size:11px;margin-top:9px;text-align:center;
|
||||
word-break:break-all}
|
||||
.dl-soon{text-align:center;color:#9ca3af;font-size:14px;padding:11px 0;
|
||||
border:1px dashed #e6e8eb;border-radius:9px}
|
||||
.dl-req{color:#9ca3af;font-size:12px;margin-top:12px;text-align:center}
|
||||
|
||||
/* FAQ */
|
||||
.faqs{max-width:720px;margin:0 auto}
|
||||
.faq{border-bottom:1px solid #e6e8eb}
|
||||
.faq summary{padding:20px 0;font-weight:600;font-size:16px;cursor:pointer;
|
||||
list-style:none;display:flex;justify-content:space-between;
|
||||
align-items:center;gap:16px}
|
||||
.faq summary::-webkit-details-marker{display:none}
|
||||
.faq-mark{width:9px;height:9px;border-right:2px solid #9ca3af;
|
||||
border-bottom:2px solid #9ca3af;transform:rotate(45deg);
|
||||
transition:transform .2s;flex:none;margin-right:4px}
|
||||
.faq[open] .faq-mark{transform:rotate(-135deg)}
|
||||
.faq-body{color:#6b7280;font-size:15px;padding:0 0 22px;line-height:1.8}
|
||||
|
||||
/* 页脚 */
|
||||
footer{padding:52px 0 64px;border-top:1px solid #e6e8eb;
|
||||
color:#6b7280;font-size:14px}
|
||||
.foot-in{display:flex;justify-content:space-between;gap:24px;
|
||||
flex-wrap:wrap;align-items:center}
|
||||
.foot-links{display:flex;gap:22px}
|
||||
.foot-links a:hover{color:#1a1d21}
|
||||
.copy{color:#9ca3af;font-size:13px;margin-top:26px;text-align:center}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav><div class="wrap nav-in">
|
||||
<a class="brand" href="/">
|
||||
<svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect x="3" y="3" width="26" height="26" rx="8" fill="#2563eb"/><rect x="10" y="9" width="12" height="14" rx="4" stroke="#fff" stroke-width="2"/><path d="M13 9V6h6v3M13 23v3h6v-3" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<span>ykxmz</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="#features">功能</a>
|
||||
<a href="#how">怎么用</a>
|
||||
<a href="#download">下载</a>
|
||||
<a href="#faq">常见问题</a>
|
||||
</div>
|
||||
</div></nav>
|
||||
|
||||
<div class="wrap hero">
|
||||
<h1>老师抬一下手腕<br>就能管教室那台电脑</h1>
|
||||
<p>放课件、放音频、投图到大屏、看看有没有人在跑无关程序、下课关机 ——
|
||||
不用走回讲台去点鼠标。</p>
|
||||
<div class="cta">
|
||||
<a class="btn btn-p" href="#download">下载安装</a>
|
||||
<a class="btn btn-s" href="#how">先看看怎么用</a>
|
||||
</div>
|
||||
<div class="badge">最新版 1.7.1</div>
|
||||
</div>
|
||||
|
||||
<section id="features"><div class="wrap">
|
||||
<div class="sec-head">
|
||||
<h2>上课用得上的,基本都在这</h2>
|
||||
<p>手表上十个分页,常用操作都在第一屏</p>
|
||||
</div>
|
||||
<div class="feats"><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18v11H3z"/><path d="M1 20h22"/></svg></div><div class="feat-title">屏幕查看</div><div class="feat-desc">手表上看教室里那台电脑在显示什么,顺手一键截屏存到手表</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4l14 8-14 8z"/></svg></div><div class="feat-title">课件播放</div><div class="feat-desc">放音频、放视频、调音量、静音 —— 不用走回讲台去点鼠标</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 16l-5-5-9 9"/></svg></div><div class="feat-title">投图到大屏</div><div class="feat-desc">手表上选张图,全屏显示在教室大屏。考试时贴个提示特别方便</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></div><div class="feat-title">传文件</div><div class="feat-desc">课件在两台机器之间来回倒腾,不用插 U 盘</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="3"/><path d="M9 6V3h6v3M9 18v3h6v-3"/></svg></div><div class="feat-title">课堂专注</div><div class="feat-desc">一键限制游戏相关站点、关闭游戏窗口,下课一键恢复</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v10"/><path d="M6 6a9 9 0 1 0 12 0"/></svg></div><div class="feat-title">电源控制</div><div class="feat-desc">锁屏、睡眠、关机、重启,还能设「30 分钟后自动关机」</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8.5 16a5.5 5.5 0 0 1 7 0"/><circle cx="12" cy="19.5" r="1"/></svg></div><div class="feat-title">局域网优先</div><div class="feat-desc">手表和电脑在同一个 WiFi 时直连,几十毫秒,不绕服务器</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/></svg></div><div class="feat-title">跨网络</div><div class="feat-desc">不在同一个网络时自动走中转,办公室也能管教室</div></div><div class="feat"><div class="feat-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></div><div class="feat-title">可见可控</div><div class="feat-desc">屏幕被看时电脑上一定有提示条,关不掉;本机随时能掐断</div></div></div>
|
||||
</div></section>
|
||||
|
||||
<section id="how"><div class="wrap">
|
||||
<div class="sec-head">
|
||||
<h2>三步装好</h2>
|
||||
<p>电脑装一个程序,手表装一个 App,一台电脑配一次就行</p>
|
||||
</div>
|
||||
<div class="steps"><div class="step"><div class="step-num">1</div><div class="step-title">电脑端</div><div class="step-desc">在教室那台 Windows 电脑上运行安装包,填一个 6 位配对码,装完自动后台运行</div></div><div class="step"><div class="step-num">2</div><div class="step-title">手表端</div><div class="step-desc">在安卓手表上装 APK,首页点「局域网连接」或「输入配对码连接」</div></div><div class="step"><div class="step-num">3</div><div class="step-title">连上就用</div><div class="step-desc">配对一次之后,以后打开就自动连,不用再输数字</div></div></div>
|
||||
</div></section>
|
||||
|
||||
<section id="download"><div class="wrap">
|
||||
<div class="sec-head">
|
||||
<h2>下载</h2>
|
||||
<p>当前版本 1.7.1</p>
|
||||
</div>
|
||||
<div class="dls"><div class="dl-card"><div class="dl-head"><span class="dl-tag">电脑端</span><span class="dl-who">教室那台 Windows 电脑</span></div><div class="dl-note">装完自动后台运行</div><a class="dl-btn" href="/download/ykxmz-setup-1.7.1.exe">下载 安装包</a><div class="dl-file">ykxmz-setup-1.7.1.exe · 14.4 MB</div><div class="dl-req">Windows 10 / 11</div></div><div class="dl-card"><div class="dl-head"><span class="dl-tag">手表端</span><span class="dl-who">安卓手表</span></div><div class="dl-note">直接安装,不用 root</div><a class="dl-btn" href="/download/ykxmz-watch-1.7.1.apk">下载 APK</a><div class="dl-file">ykxmz-watch-1.7.1.apk · 0.2 MB</div><div class="dl-req">Android 7.0+</div></div><div class="dl-card"><div class="dl-head"><span class="dl-tag">服务器端</span><span class="dl-who">一台能上外网的小机器</span></div><div class="dl-note">只在跨网络时才需要</div><a class="dl-btn" href="/download/ykxmz-server-1.7.1.zip">下载 压缩包</a><div class="dl-file">ykxmz-server-1.7.1.zip · 0.2 MB</div><div class="dl-req">Debian / Ubuntu</div></div></div>
|
||||
</div></section>
|
||||
|
||||
<section id="faq"><div class="wrap">
|
||||
<div class="sec-head"><h2>常见问题</h2></div>
|
||||
<div class="faqs"><details class="faq"><summary>手表连不上电脑怎么办?<span class="faq-mark"></span></summary><div class="faq-body">先确认电脑上程序在运行(任务管理器里找 ykxmz.exe)。如果被控端窗口提示「防火墙未放行」,点旁边的一键修复。还是不行就跑一次网络自检 —— 电脑上打开工具箱选 3,然后手表上点「检查服务器」。</div></details><details class="faq"><summary>为什么显示「延迟 2000 毫秒」这么慢?<span class="faq-mark"></span></summary><div class="faq-body">跨网络时数据要绕到中转服务器再回来,延迟取决于你那台小机器的线路。如果手表和电脑在同一个 WiFi,会自动走局域网直连,延迟通常只有几十毫秒。</div></details><details class="faq"><summary>需要另外买服务器吗?<span class="faq-mark"></span></summary><div class="faq-body">只有一个教室用的话不需要 —— 手表和电脑连同一个 WiFi 就能直连。要在办公室管教室才需要一台能上外网的小机器当中转。</div></details><details class="faq"><summary>配对码忘了怎么办?<span class="faq-mark"></span></summary><div class="faq-body">电脑上双击程序图标打开窗口就能看到。也可以在窗口里改一个自己好记的。</div></details><details class="faq"><summary>手表重装了还能连上原来的电脑吗?<span class="faq-mark"></span></summary><div class="faq-body">能。重新输一次那个 6 位配对码就行 —— 配对码本身就是钥匙。</div></details><details class="faq"><summary>屏幕被别人看着,我能知道吗?<span class="faq-mark"></span></summary><div class="faq-body">能。只要有人在看你的屏幕,电脑右上角就会出现一条红色提示条,没有开关能关掉它。电脑前面的人随时可以点「停止共享」断开。</div></details></div>
|
||||
</div></section>
|
||||
|
||||
<footer><div class="wrap">
|
||||
<div class="foot-in">
|
||||
<div class="brand"><svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect x="3" y="3" width="26" height="26" rx="8" fill="#2563eb"/><rect x="10" y="9" width="12" height="14" rx="4" stroke="#fff" stroke-width="2"/><path d="M13 9V6h6v3M13 23v3h6v-3" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg><span>ykxmz</span></div>
|
||||
<div class="foot-links">
|
||||
<a href="https://gitee.com/xmz7891/ykxmz" target="_blank" rel="noopener">源码</a>
|
||||
<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" rel="noopener">历史版本</a>
|
||||
<a href="#faq">常见问题</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy">ykxmz · 教室电脑手表遥控</div>
|
||||
</div></footer>
|
||||
|
||||
<script>
|
||||
// 点导航链接后收起移动端(这里没做汉堡菜单,留个锚点平滑滚动就行)
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var target = document.querySelector(a.getAttribute('href'));
|
||||
if (target) { e.preventDefault(); target.scrollIntoView({behavior:'smooth'}); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,298 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""中转站改造成「官网 + 管理页分家」。
|
||||
|
||||
用户要求:
|
||||
relay.xxmxxs.top/ -> 官网(简洁介绍 + 下载)
|
||||
relay.xxmxxs.top/zhingzhuanzhan -> 中转站管理(要登录)
|
||||
管理账号 xmz7891 密码 @REVAedu120830
|
||||
|
||||
改动点:
|
||||
1. 默认管理密码改掉,并加"账号"这个维度(原来只有密码)
|
||||
2. admin_login(username, password)
|
||||
3. GET / -> 官网(公开,不用登录)
|
||||
4. GET /zhingzhuanzhan -> 管理页(要登录)
|
||||
/admin 保留成 302 跳过去,免得老书签失效
|
||||
5. 登录表单加账号输入框
|
||||
6. 登录后的跳转、退出链接都指到 /zhingzhuanzhan
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py")
|
||||
|
||||
ADMIN_PATH = "/zhingzhuanzhan"
|
||||
ADMIN_USER = "xmz7891"
|
||||
ADMIN_PASS = "@REVAedu120830"
|
||||
|
||||
# 官网首页(公开)。做得简洁:一句话说清是什么、三个下载按钮。
|
||||
SITE_PAGE = '''def _site_page() -> str:
|
||||
"""官网首页:简洁介绍 + 下载。**不需要登录**。
|
||||
|
||||
用户要求把 relay.xxmxxs.top 当官网用,
|
||||
管理后台挪到 /zhingzhuanzhan(见 _login_page / _admin_page)。
|
||||
"""
|
||||
files = _list_downloads()
|
||||
# 挑出要展示的三个:安装包 / 服务器端 / 手表端
|
||||
setup = next((f for f in files if f["name"].endswith(".exe")), None)
|
||||
server = next((f for f in files
|
||||
if f["name"].endswith(".zip") and "server" in f["name"]), None)
|
||||
watch = next((f for f in files if f["name"].endswith(".apk")), None)
|
||||
|
||||
def card(title, desc, item, tag):
|
||||
if not item:
|
||||
return (f'<div class=card><div class=t>{title}</div>'
|
||||
f'<div class=d>{desc}</div>'
|
||||
f'<div class=soon>暂未上传</div></div>')
|
||||
mb = item["size"] / 1024 / 1024
|
||||
size = f"{mb:.1f} MB" if mb >= 0.1 else f"{item['size'] / 1024:.0f} KB"
|
||||
href = "/download/" + quote(item["name"])
|
||||
return (f'<div class=card><div class=t>{title}</div>'
|
||||
f'<div class=d>{desc}</div>'
|
||||
f'<a class=btn href="{href}">下载 {tag}</a>'
|
||||
f'<div class=s>{item["name"]} · {size}</div></div>')
|
||||
|
||||
ver = ""
|
||||
for f in files:
|
||||
m = re.search(r"(\\d+\\.\\d+\\.\\d+)", f["name"])
|
||||
if m:
|
||||
ver = m.group(1)
|
||||
break
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang=zh-CN><head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>ykxmz · 教室电脑手表遥控</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
*{{box-sizing:border-box}}
|
||||
body{{margin:0;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
|
||||
background:#0d1117;color:#e6edf3;line-height:1.7}}
|
||||
.wrap{{max-width:760px;margin:0 auto;padding:56px 20px 80px}}
|
||||
h1{{font-size:34px;margin:0 0 10px;letter-spacing:-.5px}}
|
||||
.sub{{color:#8b949e;font-size:17px;margin-bottom:8px}}
|
||||
.ver{{display:inline-block;background:#1f6feb22;color:#79c0ff;
|
||||
border:1px solid #1f6feb55;border-radius:20px;
|
||||
padding:2px 12px;font-size:13px;margin-bottom:34px}}
|
||||
.lead{{font-size:16px;color:#c9d1d9;margin:0 0 14px}}
|
||||
ul{{color:#8b949e;padding-left:22px;margin:0 0 40px}}
|
||||
li{{margin:4px 0}}
|
||||
.cards{{display:grid;gap:14px;grid-template-columns:1fr}}
|
||||
@media(min-width:640px){{.cards{{grid-template-columns:1fr 1fr 1fr}}}}
|
||||
.card{{background:#161b22;border:1px solid #30363d;border-radius:12px;
|
||||
padding:18px 16px;display:flex;flex-direction:column}}
|
||||
.card .t{{font-weight:600;font-size:15px;margin-bottom:6px}}
|
||||
.card .d{{color:#8b949e;font-size:13px;flex:1;margin-bottom:14px}}
|
||||
.btn{{display:block;text-align:center;background:#238636;color:#fff;
|
||||
text-decoration:none;padding:9px 14px;border-radius:8px;
|
||||
font-weight:600;font-size:14px}}
|
||||
.btn:hover{{background:#2ea043}}
|
||||
.soon{{color:#6e7681;font-size:13px;text-align:center;padding:9px 0}}
|
||||
.s{{color:#6e7681;font-size:11px;margin-top:8px;
|
||||
word-break:break-all;text-align:center}}
|
||||
.foot{{margin-top:46px;padding-top:20px;border-top:1px solid #21262d;
|
||||
color:#6e7681;font-size:13px}}
|
||||
.foot a{{color:#79c0ff;text-decoration:none}}
|
||||
</style></head><body><div class=wrap>
|
||||
|
||||
<h1>ykxmz</h1>
|
||||
<div class=sub>老师抬一下手腕,就能管教室里那台电脑。</div>
|
||||
<div class=ver>{("最新版 " + ver) if ver else ""}</div>
|
||||
|
||||
<p class=lead>放课件、放音频、投图到大屏、看看谁在打游戏、下课关机 ——
|
||||
不用走回讲台去点鼠标。</p>
|
||||
<ul>
|
||||
<li>手表和电脑在同一个 WiFi:直连,几十毫秒</li>
|
||||
<li>不在同一个网络:自动走中转,办公室也能管教室</li>
|
||||
<li>屏幕被看时电脑上一定有提示条,关不掉</li>
|
||||
</ul>
|
||||
|
||||
<div class=cards>
|
||||
{card("电脑端", "教室那台 Windows 电脑装这个", setup, "安装包")}
|
||||
{card("服务器端", "要跨网络才需要,装在一台小机器上", server, "压缩包")}
|
||||
{card("手表端", "安卓手表装这个 APK", watch, "APK")}
|
||||
</div>
|
||||
|
||||
<div class=foot>
|
||||
源码在 <a href="https://gitee.com/xmz7891/ykxmz" target=_blank rel=noopener>Gitee</a>
|
||||
· 历史版本在 <a href="https://gitee.com/xmz7891/ykxmz/releases" target=_blank rel=noopener>发行版</a>
|
||||
· 管理入口 <a href="{ADMIN_PATH}">在此</a>
|
||||
</div>
|
||||
|
||||
</div></body></html>"""
|
||||
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
ok = True
|
||||
|
||||
if "_site_page" in text:
|
||||
print(" 已经改造过")
|
||||
return 0
|
||||
|
||||
# ---- 1. 导入 quote / re ----
|
||||
if "\nimport re" not in text:
|
||||
text = text.replace("import os\n", "import os\nimport re\n", 1)
|
||||
print(" 补 import re")
|
||||
if "from urllib.parse import quote" not in text:
|
||||
text = text.replace("import urllib.parse\n",
|
||||
"import urllib.parse\nfrom urllib.parse import quote\n", 1)
|
||||
print(" 补 from urllib.parse import quote")
|
||||
|
||||
# ---- 2. 密码 / 账号 ----
|
||||
text = text.replace(
|
||||
'ADMIN_PASSWORD_DEFAULT = "120830"',
|
||||
f'ADMIN_PASSWORD_DEFAULT = "{ADMIN_PASS}"\n'
|
||||
f'# 管理页账号(原来只有密码,用户要求加账号)\n'
|
||||
f'ADMIN_USER_DEFAULT = "{ADMIN_USER}"\n'
|
||||
f'ADMIN_USER = os.environ.get("RELAY_ADMIN_USER", "") or ADMIN_USER_DEFAULT',
|
||||
1)
|
||||
text = text.replace(
|
||||
'def admin_login(password: str) -> str:',
|
||||
'def admin_login(username: str, password: str) -> str:', 1)
|
||||
text = text.replace(
|
||||
' if ADMIN_PASSWORD:\n return ADMIN_PASSWORD',
|
||||
' if ADMIN_PASSWORD:\n return ADMIN_PASSWORD',
|
||||
1)
|
||||
|
||||
# 函数体内:校验账号 + 密码
|
||||
old_check = " if not password or password != _admin_password():\n return \"\""
|
||||
new_check = (" if not password or password != _admin_password():\n"
|
||||
" return \"\"\n"
|
||||
" if username and username.strip() != ADMIN_USER:\n"
|
||||
" return \"\"")
|
||||
if old_check in text:
|
||||
text = text.replace(old_check, new_check, 1)
|
||||
print(" admin_login 加了账号校验")
|
||||
else:
|
||||
print(" [提示] admin_login 的密码校验锚点没找到,稍后手工确认")
|
||||
|
||||
# ---- 3. 路由:/ 官网,/zhingzhuanzhan 管理页 ----
|
||||
old_route = ''' if path in ("/", "/admin"):
|
||||
# 管理页要登录 —— 上面会显示配对码,不能谁都能看
|
||||
if not self._admin_gate():
|
||||
self._send(200, _login_page(
|
||||
"密码不对,再试一次" if _query(self.path, "bad") else ""
|
||||
), "text/html; charset=utf-8")
|
||||
return
|
||||
self._admin_page()
|
||||
return'''
|
||||
new_route = f''' # ---- 官网首页(公开,不用登录)----
|
||||
if path == "/":
|
||||
self._send(200, _site_page(), "text/html; charset=utf-8")
|
||||
return
|
||||
|
||||
# ---- 管理后台 ----
|
||||
# 挪到了 /zhingzhuanzhan(用户要求)。/admin 保留成跳转,
|
||||
# 免得存过书签的人打不开。
|
||||
if path == "/admin":
|
||||
self._redirect("{ADMIN_PATH}")
|
||||
return
|
||||
|
||||
if path == "{ADMIN_PATH}":
|
||||
# 管理页要登录 —— 上面会显示配对码,不能谁都能看
|
||||
if not self._admin_gate():
|
||||
self._send(200, _login_page(
|
||||
"账号或密码不对,再试一次"
|
||||
if _query(self.path, "bad") else ""
|
||||
), "text/html; charset=utf-8")
|
||||
return
|
||||
self._admin_page()
|
||||
return'''
|
||||
if old_route in text:
|
||||
text = text.replace(old_route, new_route, 1)
|
||||
print(" 路由:/ 官网,/zhingzhuanzhan 管理页")
|
||||
else:
|
||||
print(" [失败] 路由锚点没找到")
|
||||
ok = False
|
||||
|
||||
# ---- 4. 登录 POST:读账号 + 密码 ----
|
||||
old_login = ''' if path == "/admin/login":
|
||||
form = self._body().decode("utf-8", "replace")
|
||||
password = ""
|
||||
for piece in form.split("&"):
|
||||
name, _, value = piece.partition("=")
|
||||
if name == "password":
|
||||
from urllib.parse import unquote_plus
|
||||
password = unquote_plus(value)
|
||||
token = admin_login(password)
|
||||
if not token:
|
||||
log(f"管理页登录失败,来自 {self._client_ip()}")
|
||||
self._redirect("/?bad=1")
|
||||
return
|
||||
log(f"管理页登录成功,来自 {self._client_ip()}")
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/")'''
|
||||
new_login = f''' if path == "/admin/login":
|
||||
form = self._body().decode("utf-8", "replace")
|
||||
username = password = ""
|
||||
for piece in form.split("&"):
|
||||
name, _, value = piece.partition("=")
|
||||
if name == "password":
|
||||
from urllib.parse import unquote_plus
|
||||
password = unquote_plus(value)
|
||||
elif name == "username":
|
||||
from urllib.parse import unquote_plus
|
||||
username = unquote_plus(value)
|
||||
token = admin_login(username, password)
|
||||
if not token:
|
||||
log(f"管理页登录失败({username or '未填账号'}),"
|
||||
f"来自 {{self._client_ip()}}")
|
||||
self._redirect("{ADMIN_PATH}?bad=1")
|
||||
return
|
||||
log(f"管理页登录成功({{username}}),来自 {{self._client_ip()}}")
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "{ADMIN_PATH}")'''
|
||||
if old_login in text:
|
||||
text = text.replace(old_login, new_login, 1)
|
||||
print(" 登录接口:接受账号 + 密码")
|
||||
else:
|
||||
print(" [失败] 登录锚点没找到")
|
||||
ok = False
|
||||
|
||||
# ---- 5. 登录表单加账号框 ----
|
||||
old_form = '<form method=post action=/admin/login>'
|
||||
new_form = ('<form method=post action=/admin/login>'
|
||||
'<input name=username placeholder=账号 autocomplete=username required>')
|
||||
if old_form in text:
|
||||
text = text.replace(old_form, new_form, 1)
|
||||
print(" 登录表单加了账号输入框")
|
||||
|
||||
# ---- 6. 退出后回管理页 ----
|
||||
text = text.replace('<a href="/admin/logout"', f'<a href="/admin/logout"', 1)
|
||||
|
||||
# ---- 7. 插入官网页面函数(放在 _login_page 前面)----
|
||||
anchor = "def _login_page("
|
||||
if anchor in text:
|
||||
text = text.replace(anchor, SITE_PAGE + anchor, 1)
|
||||
print(" 插入官网首页 _site_page()")
|
||||
else:
|
||||
print(" [失败] 找不到 _login_page,官网函数插不进去")
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" (已把改到一半的内容写回,请检查)")
|
||||
return 1
|
||||
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" relay.py 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||||
lines = text.splitlines()
|
||||
for i in range(max(0, exc.lineno - 3), min(len(lines), exc.lineno + 2)):
|
||||
print(f" {i + 1}: {lines[i][:90]}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''本地渲染官网,检查内容完整性 + 生成预览文件。
|
||||
|
||||
用法:python _build/test_site_render.py
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "relay"))
|
||||
|
||||
import landing # noqa: E402
|
||||
|
||||
# 模拟 _list_downloads() 的返回
|
||||
FILES = [
|
||||
{"name": "ykxmz-setup-1.7.1.exe", "size": 15063814, "mtime": 1},
|
||||
{"name": "ykxmz-server-1.7.1.zip", "size": 194633, "mtime": 1},
|
||||
{"name": "ykxmz-watch-1.7.1.apk", "size": 201523, "mtime": 1},
|
||||
{"name": "ykxmz-watch-1.7.1-说明.txt", "size": 1875, "mtime": 1},
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
global passed, failed
|
||||
if ok:
|
||||
passed += 1
|
||||
print(" [PASS] " + name)
|
||||
else:
|
||||
failed += 1
|
||||
print(" [FAIL] " + name + " " + str(detail))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
html = landing.render_site(FILES)
|
||||
print(" 渲染 %d 字符" % len(html))
|
||||
print()
|
||||
|
||||
print("一、结构完整")
|
||||
for label, needle in (
|
||||
("DOCTYPE", "<!DOCTYPE html>"),
|
||||
("语言 zh-CN", 'lang="zh-CN"'),
|
||||
("吸顶导航", "nav-in"),
|
||||
("Hero 标题", "老师抬一下手腕"),
|
||||
("功能网格", 'id="features"'),
|
||||
("三步上手", 'id="how"'),
|
||||
("下载区", 'id="download"'),
|
||||
("FAQ", 'id="faq"'),
|
||||
("页脚", "foot-in"),
|
||||
("响应式 viewport", "width=device-width")):
|
||||
check(label, needle in html)
|
||||
|
||||
print()
|
||||
print("二、内容数量")
|
||||
feats = len(re.findall(r'class="feat"', html))
|
||||
steps = len(re.findall(r'class="step"', html))
|
||||
dls = len(re.findall(r'class="dl-card"', html))
|
||||
faqs = len(re.findall(r'<details class="faq"', html))
|
||||
check("9 张功能卡", feats == 9, "实际 %d" % feats)
|
||||
check("3 个步骤卡", steps == 3, "实际 %d" % steps)
|
||||
check("3 个下载卡", dls == 3, "实际 %d" % dls)
|
||||
check("6 条 FAQ", faqs == 6, "实际 %d" % faqs)
|
||||
|
||||
print()
|
||||
print("三、logo 与图标")
|
||||
check("导航 logo 是内联 SVG(不是破图 img)",
|
||||
'class="brand"' in html and "src=\"/favicon.ico\" alt=\"\"" not in html)
|
||||
check("图标数量足够(9 功能 + 2 logo;步骤卡用数字不用图标)",
|
||||
html.count("<svg") >= 11, "实际 %d" % html.count("<svg"))
|
||||
check("favicon 的 link 还在", 'rel="icon"' in html)
|
||||
|
||||
print()
|
||||
print("四、下载链接")
|
||||
for name in ("ykxmz-setup-1.7.1.exe", "ykxmz-server-1.7.1.zip",
|
||||
"ykxmz-watch-1.7.1.apk"):
|
||||
check("链接 " + name, ("/download/" + name) in html)
|
||||
check("没把说明 txt 当下载卡",
|
||||
"说明.txt" not in html or "dl-card" in html)
|
||||
|
||||
print()
|
||||
print("五、白色简洁配色")
|
||||
colors = sorted(set(re.findall(r"#[0-9a-fA-F]{6}", html)))
|
||||
check("底色是白", "#ffffff" in colors)
|
||||
check("有浅灰面板", "#fafbfc" in colors)
|
||||
# 深色只允许正文色和主色
|
||||
allow = {"#1a1d21", "#2563eb", "#1d4ed8", "#6b7280", "#9ca3af",
|
||||
"#e6e8eb", "#eff6ff", "#d1d5db", "#f3f4f6", "#ffffff", "#fafbfc"}
|
||||
weird = [c for c in colors if c not in allow]
|
||||
check("没有意外的颜色", not weird, weird)
|
||||
print(" 颜色: %s" % " ".join(colors))
|
||||
|
||||
print()
|
||||
print("六、版本号")
|
||||
check("显示最新版号", "1.7.1" in html)
|
||||
|
||||
print()
|
||||
print("七、不能有管理入口")
|
||||
check("官网里没有管理入口", "管理入口" not in html)
|
||||
check("官网里没有管理页路径", "zhongzhuanzhan" not in html)
|
||||
|
||||
# 写预览
|
||||
out = ROOT / "_build" / "site_preview.html"
|
||||
out.write_text(html, encoding="utf-8")
|
||||
print()
|
||||
print(" 预览 -> %s" % out.name)
|
||||
|
||||
print()
|
||||
print("=" * 58)
|
||||
print("通过 %d 项,失败 %d 项" % (passed, failed))
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''更新两个测试:配对窗口关闭时的预期行为变了。
|
||||
|
||||
旧行为(测试原来写的):
|
||||
配对窗口关着 -> **任何**新设备配对都被拒
|
||||
|
||||
新行为(这次改的,因为用户遇到过):
|
||||
配对窗口关着 -> **码对就放行,码错才拒**
|
||||
理由:配对码本身就是密钥。老逻辑"首台设备配对成功后自动关窗"
|
||||
会把重装手表的人永久挡在外面(实测报"被控端已关闭新设备配对")。
|
||||
|
||||
所以测试要改成:
|
||||
1. **错误的码** -> 被拒(这条才是安全边界,必须守住)
|
||||
2. **正确的码** -> 放行(手表重装后能重连,这是这次修的东西)
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
|
||||
# ---------------- test_pair_once_flow.py ----------------
|
||||
|
||||
FLOW = ROOT / "_tests" / "test_pair_once_flow.py"
|
||||
FLOW_OLD = ''' check("静默模式下新设备配对被拒绝", bad.get("ok") is False, str(bad)[:100])
|
||||
intruder.close()'''
|
||||
|
||||
FLOW_NEW = ''' # 注意:这里以前是拿**正确的码**去试,期待被拒。
|
||||
# 后来改成"码对就放行"(配对码本身就是密钥),
|
||||
# 所以安全边界换成"**错误的码**必须被拒"。
|
||||
check("拿着错误的码配不上", bad.get("ok") is False, str(bad)[:100])
|
||||
intruder.close()
|
||||
|
||||
# 表格重装/换新表:拿着正确的码应该能重新配上
|
||||
again = Talk(PORT)
|
||||
retry = again.call({"cmd": "pair", "deviceId": "watch-reinstalled",
|
||||
"deviceName": "手表", "code": code})
|
||||
check("手表重装后拿正确的码能重新配上",
|
||||
retry.get("ok") is True, str(retry)[:120])
|
||||
again.close()'''
|
||||
|
||||
# ---------------- test_silent_mode.py ----------------
|
||||
|
||||
SILENT = ROOT / "_tests" / "test_silent_mode.py"
|
||||
SILENT_OLD = ''' check("静默模式下新设备配对被拒绝", reply.get("ok") is False,
|
||||
f"竟然配上了: {reply}")
|
||||
check("给出的理由是明确的",
|
||||
"配对" in str(reply.get("error", "")) or
|
||||
"关闭" in str(reply.get("error", "")),
|
||||
str(reply.get("error")))'''
|
||||
|
||||
SILENT_NEW = ''' # 行为变了:配对窗口关着时,**码错才拒**,码对放行。
|
||||
# 因为配对码本身就是密钥,老逻辑"首台配对后自动关窗"
|
||||
# 会让重装手表的人永久连不上(实测踩过)。
|
||||
check("静默模式下拿着错误的码配不上",
|
||||
reply.get("ok") is False,
|
||||
f"竟然配上了: {reply}")
|
||||
check("给出的理由是明确的",
|
||||
"配对" in str(reply.get("error", "")) or
|
||||
"关闭" in str(reply.get("error", "")),
|
||||
str(reply.get("error")))'''
|
||||
|
||||
|
||||
def patch(path: Path, old: str, new: str, label: str) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if new.splitlines()[0].strip() in text and "错误的码" in text:
|
||||
print(f" {path.name}: 已经是新版")
|
||||
return True
|
||||
if old not in text:
|
||||
print(f" [失败] {path.name}: 找不到「{label}」那段")
|
||||
return False
|
||||
path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n")
|
||||
print(f" {path.name}: 已更新({label})")
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
return True
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {path.name} 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok = patch(FLOW, FLOW_OLD, FLOW_NEW, "未配对设备被挡")
|
||||
ok = patch(SILENT, SILENT_OLD, SILENT_NEW, "静默模式配对") and ok
|
||||
|
||||
# 静默模式那个测试原来用的是正确码还是错误码?看一下
|
||||
print()
|
||||
text = SILENT.read_text(encoding="utf-8")
|
||||
import re
|
||||
m = re.search(r'"code":\s*([^,}\n]+)', text)
|
||||
print(f" test_silent_mode 里用的码: {m.group(1).strip() if m else '?'}")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# 验证新官网:每个区块都要在,且能正常访问
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
|
||||
echo " 1) 本地官网"
|
||||
curl -s --max-time 20 "$BASE/" -o /tmp/site.html 2>/dev/null
|
||||
size=$(wc -c < /tmp/site.html)
|
||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/") ${size} 字节"
|
||||
|
||||
echo " 2) 各区块"
|
||||
for pair in "吸顶导航:nav-in" "Hero:hero" "功能网格:id=features" \
|
||||
"三步上手:id=how" "下载区:id=download" "FAQ:id=faq" \
|
||||
"页脚:foot-in" "应用图标:favicon.ico"; do
|
||||
label="${pair%%:*}"
|
||||
key="${pair##*:}"
|
||||
if grep -q "$key" /tmp/site.html; then
|
||||
echo " OK $label"
|
||||
else
|
||||
echo " XX $label"
|
||||
fi
|
||||
done
|
||||
|
||||
echo " 3) 下载链接"
|
||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sed 's/^/ /'
|
||||
|
||||
echo " 4) 配色(应该全是浅色)"
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
html = open('/tmp/site.html', encoding='utf-8').read()
|
||||
colors = sorted(set(re.findall(r'#[0-9a-fA-F]{6}', html)))
|
||||
dark = [c for c in colors if int(c[1:3], 16) + int(c[3:5], 16) + int(c[5:7], 16) < 200]
|
||||
print(' 用到的颜色:', ' '.join(colors))
|
||||
print(' 深色值:', dark if dark else '无 ✓')
|
||||
PY
|
||||
|
||||
echo " 5) 公网访问"
|
||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 40 -H 'User-Agent: LanRemote/1.4.0' "$PUB/")"
|
||||
|
||||
echo " 6) 管理页还能进吗(路径应该没被影响)"
|
||||
echo " /zhongzhuanzhan HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/zhongzhuanzhan")"
|
||||
echo " 官网里不该有管理入口: $(grep -c '管理入口\|zhongzhuanzhan' /tmp/site.html) 处"
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# 验证新官网(修正引号问题)
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
|
||||
curl -s --max-time 20 "$BASE/" -o /tmp/site.html 2>/dev/null
|
||||
size=$(wc -c < /tmp/site.html)
|
||||
echo " 官网: HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/") ${size} 字节"
|
||||
|
||||
echo
|
||||
echo " 各区块(注意 HTML 里是 id=\"xxx\" 带引号):"
|
||||
check() {
|
||||
if grep -q "$2" /tmp/site.html; then
|
||||
echo " OK $1"
|
||||
else
|
||||
echo " XX $1"
|
||||
fi
|
||||
}
|
||||
check "吸顶导航" 'nav-in'
|
||||
check "Hero 标题" '老师抬一下手腕'
|
||||
check "最新版号" 'badge'
|
||||
check "功能网格" 'id="features"'
|
||||
check "三步上手" 'id="how"'
|
||||
check "下载区" 'id="download"'
|
||||
check "常见问题" 'id="faq"'
|
||||
check "页脚" 'foot-in'
|
||||
check "应用图标" 'favicon.ico'
|
||||
check "九张功能卡" 'feat-title'
|
||||
check "三个步骤卡" 'step-num'
|
||||
check "三个下载卡" 'dl-card'
|
||||
check "六条 FAQ" 'class="faq"'
|
||||
|
||||
echo
|
||||
echo " 各区块计数:"
|
||||
printf " 功能卡 %s 张\n" "$(grep -o 'feat-title' /tmp/site.html | wc -l)"
|
||||
printf " 步骤卡 %s 个\n" "$(grep -o 'step-num' /tmp/site.html | wc -l)"
|
||||
printf " 下载卡 %s 个\n" "$(grep -o 'dl-card' /tmp/site.html | wc -l)"
|
||||
printf " FAQ %s 条\n" "$(grep -o 'class="faq"' /tmp/site.html | wc -l)"
|
||||
|
||||
echo
|
||||
echo " 下载链接:"
|
||||
grep -o 'href="/download/[^"]*"' /tmp/site.html | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo " 配色(白底主题,深色只该有正文色):"
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
html = open('/tmp/site.html', encoding='utf-8').read()
|
||||
colors = sorted(set(re.findall(r'#[0-9a-fA-F]{6}', html)))
|
||||
print(' ', ' '.join(colors))
|
||||
# 允许的深色:正文 #1a1d21、主色 #2563eb、深一档 #1d4ed8
|
||||
ALLOW_DARK = {'#1a1d21', '#2563eb', '#1d4ed8'}
|
||||
bad = []
|
||||
for c in colors:
|
||||
v = int(c[1:3], 16) + int(c[3:5], 16) + int(c[5:7], 16)
|
||||
if v < 420 and c not in ALLOW_DARK:
|
||||
bad.append(c)
|
||||
print(' 非白底配色里不该出现的深色:', bad if bad else '无 ✓')
|
||||
print(' 底色是白:', '#ffffff' in colors)
|
||||
PY
|
||||
|
||||
echo
|
||||
echo " 其它页面没被影响:"
|
||||
echo " 管理页 /zhongzhuanzhan HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/zhongzhuanzhan")"
|
||||
echo " /admin 跳转 -> $(curl -s -o /dev/null -w '%{redirect_url}' --max-time 15 "$BASE/admin")"
|
||||
echo " favicon HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/favicon.ico")"
|
||||
echo " 下载接口 HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/v1/downloads")"
|
||||
echo " 官网里没有管理入口: $(grep -c '管理入口' /tmp/site.html) 处"
|
||||
|
||||
echo
|
||||
echo " 公网:"
|
||||
echo " 官网 HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 40 -H 'User-Agent: LanRemote/1.4.0' "$PUB/")"
|
||||
echo " 延迟 $(curl -s -o /dev/null -w '%{time_total}' --max-time 40 -H 'User-Agent: LanRemote/1.4.0' "$PUB/") 秒"
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把官网渲染换成新的独立模块 site.render_site()。
|
||||
|
||||
原来的 _site_page() 是内联在 relay.py 里的一大段 HTML。
|
||||
现在拆到 relay/site.py,relay.py 里只留一个薄封装。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
RELAY = ROOT / "relay" / "relay.py"
|
||||
|
||||
# 找出 _site_page 函数的起止,整段替换
|
||||
NEW_FUNC = '''def _site_page() -> str:
|
||||
"""官网首页(公开,不用登录)。
|
||||
|
||||
实际渲染在 relay/site.py —— relay.py 已经两千多行,
|
||||
再放几百行 HTML 就没法看了。网站属于展示层,
|
||||
跟中转逻辑分开更清楚。
|
||||
"""
|
||||
try:
|
||||
from site import render_site
|
||||
except ImportError:
|
||||
# 打包/部署时 relay 目录就是工作目录,用相对导入兜底
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ykxmz_site", Path(__file__).resolve().parent / "site.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
render_site = module.render_site
|
||||
return render_site(_list_downloads())
|
||||
'''
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
|
||||
if "from site import render_site" in text:
|
||||
print(" 已经接过了")
|
||||
return 0
|
||||
|
||||
start = text.find("def _site_page() -> str:")
|
||||
if start < 0:
|
||||
print(" [失败] 找不到 _site_page")
|
||||
return 1
|
||||
|
||||
# 找到下一个顶层 def
|
||||
nxt = text.find("\ndef ", start + 10)
|
||||
if nxt < 0:
|
||||
print(" [失败] 找不到 _site_page 的结尾")
|
||||
return 1
|
||||
|
||||
text = text[:start] + NEW_FUNC + "\n" + text[nxt + 1:]
|
||||
|
||||
# 确保 Path 和 _list_downloads 可用
|
||||
if "\nfrom pathlib import Path" not in text:
|
||||
text = text.replace("import os\n", "import os\nfrom pathlib import Path\n", 1)
|
||||
print(" 补 from pathlib import Path")
|
||||
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" _site_page 改成调用 site.render_site()")
|
||||
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" relay.py 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
|
||||
lines = text.splitlines()
|
||||
for i in range(max(0, (exc.lineno or 1) - 3),
|
||||
min(len(lines), (exc.lineno or 1) + 2)):
|
||||
print(" %d: %s" % (i + 1, lines[i][:90]))
|
||||
return 1
|
||||
|
||||
# site.py 也检查一下
|
||||
try:
|
||||
ast.parse((ROOT / "relay" / "site.py").read_text(encoding="utf-8"))
|
||||
print(" site.py 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(" [失败] site.py 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user