mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 11:20:43 +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())
|
||||
@@ -141,7 +141,7 @@ def main() -> int:
|
||||
c_login == 401 and c_url == 200,
|
||||
f"管理接口未登录={c_login}(应为 401);"
|
||||
f"/url={c_url} 返回 {url_body.strip()[:60]};"
|
||||
f"管理密码={plain or '120830'}")
|
||||
f"管理密码={plain or '@REVAedu120830'}")
|
||||
else:
|
||||
item("管理页需要登录 + 管理接口", False, "没有公网地址,无法测")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user