Files
ykxmz/_build/package_remote.py
T
xmz7891 b86eeb349b 1.7.0:常规发布
- 版本号 1.7.0 -> 1.7.0
- 重建三端并打包
- 关键测试全过
2026-10-06 22:49:14 +08:00

559 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""打「跨网络版」的三件套压缩包。
用户要求:压缩包里三个东西
1. 被控端(Windows 电脑)
2. 服务器端(随身 WiFi / Linux)
3. 控制端(手表 APK)
产物:
dist/LanRemote-跨网络版-1.5.0.zip
"""
from __future__ import annotations
import shutil
import sys
import zipfile
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
VERSION = "1.7.0"
NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
# 三份正式文档(许可协议 / 安装前必读 / 安装后须知)
DOCS = ROOT / "docs"
RELAY_SRC = ROOT / "relay"
APK_SRC = ROOT / "dist" / f"ykxmz-watch-{VERSION}.apk"
# 被控端目录里带的 .bat 工具
TOOLS = ("fxfhq.bat", "wlzj.bat", "xffhq.bat")
TOP_README = f"""========================================================
ykxmz {VERSION} · 远程管理工具
========================================================
作者:小麦子
先看这三份:
LICENSE.txt 许可协议(安装前必看,有使用红线)
BEFORE-INSTALL.txt 安装前必读(前信息)
AFTER-INSTALL.txt 安装后须知(后信息)
下面是这个包里有哪三个东西、怎么装。
这个包里三个东西,分别装在三台设备上:
1-被控端(电脑) 装在你**要控制的那台 Windows 电脑**上。
双击 ykxmz.exe 就行,不用装 Python。
2-服务器端(随身WiFi) 装在一台能上外网的 Linux 小机器上(你的随身 WiFi)。
它负责把"不在同一个网络"的电脑和手表接起来。
3-控制端(手表) 装在你的安卓手表上。一个 APK,直接装。
【怎么用】三步
第一步:架服务器端(只做一次)
把「2-服务器端」整个文件夹拷到随身 WiFi 上,执行:
bash install.sh
它会装好中转服务 + Cloudflare 隧道,并打印一个**公网地址**。
记下这个地址(形如 https://relay.你的域名)。
小提示:公网地址每次重启会变。忘了地址就打开
http://<随身WiFi的IP>:8080/url —— 直接返回当前地址。
第二步:装被控端(电脑上,只做一次)
双击 ykxmz.exe
→ 输入 6 位配对码(自己定,比如 789113)
→ 勾上"开机自启"和"放行防火墙"
→ 点确定,会弹一次管理员确认,之后窗口全消失,后台静默运行
告诉它中转地址(这样才跨网络):
ykxmz.exe --relay https://relay.你的域名
第三步:装控制端(手表上)
装 APK → 首页点「中转设置」→ 填同一个中转地址
→ 点「自动搜索电脑」(同局域网)或直接输配对码
→ 连上
【关于配对码:单设备单配对码】
一台电脑第一次设的配对码会被服务器记住。
以后同一台电脑再报别的码,服务器会**改回它原来那个**。
好处:你不会因为重装/误操作把配对码搞乱,手表永远认得出这台电脑。
一个配对码也不能同时属于两台电脑(服务器会拒绝)。
【服务器端能管什么】
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
管理密码:120830(可在服务器上改)
能看:哪些设备在线、配对码、流量、会话数
能做:踢掉某个设备的会话、抹掉某个配对码绑定
注意:中转服务器**只是管道**,它不认识传输内容。
配对、令牌校验、权限判断全部由电脑端自己做 ——
所以就算中转机被攻破,也进不了你的电脑。
【每台设备分别看哪个说明】
电脑: 1-agent/README-agent.txt
随身 WiFi:2-server/README-server.txt
手表: 装完打开 App,首页有「连不上?点这里诊断」
"""
AGENT_README = f"""========================================================
被控端(Windows 电脑) 版本 {VERSION}
========================================================
*** 不需要安装 Python ***
ykxmz.exe 是编译好的独立程序,
拷到任何一台 Windows 电脑上双击就能用。
【装】
1) 双击 ykxmz.exe
2) 弹出窗口,输入 6 位配对码(自己定个记得住的,比如 789113)
两个勾:
[x] 设置开机自启
[x] 顺便放行防火墙(要管理员确认一次)
3) 点「确定,开始使用」
→ 弹一次 UAC,点「是」
→ 黑终端一闪,在修防火墙
→ 窗口全消失,转入后台静默运行
(没有托盘图标、没有窗口、没有终端)
【开箱即用:一个地址都不用填】
程序里**已经内置了中转地址**,双击就能用。
内置的是两个,用分号隔开,按顺序尝试:
192.168.2.156:8080;https://relay.xxmxxs.top
电脑和随身 WiFi 在同一个网络 -> 走第一个
(局域网直连,几十毫秒,地址永远不变)
电脑在外面 -> 第一个连不上,自动切第二个
(Cloudflare 隧道,600 毫秒左右)
想改成别的当然也可以,见下一节。
【跨网络:告诉它中转地址】
支持**多个地址**,用分号隔开,按顺序尝试,第一个通的就用它。
推荐这样配(局域网优先,断了自动切隧道):
ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top"
为什么局域网优先?
* 快:局域网几十毫秒,绕隧道要 600 毫秒左右
* 稳:不依赖隧道,隧道抽风也不影响
* 地址永远不变:不用管隧道地址变来变去
只走隧道也行:
ykxmz.exe --relay https://relay.xxmxxs.top
地址**不用写 http:// 或 https://**,程序会自己判断:
192.168.x.x / 10.x.x.x / *.local -> 按 http 连(局域网是明文)
域名 -> 按 https 连
看看当前状态(会显示用了哪个地址、有几个候选):
ykxmz.exe --relay-status
关掉中转(只走局域网直连):
ykxmz.exe --relay ""
【忘了中转地址?】
如果随身 WiFi 在你身边(同一个网络),浏览器打开:
http://<随身WiFi的IP>:8080/url
它会告诉你当前的公网地址(纯文本,不用登录)。
小技巧:把局域网地址也填进 --relay,电脑在家里就走局域网,
完全不用管隧道地址变没变:
ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top"
【配对码被别人占了会弹通知】
每台电脑第一次设的配对码会存到**中转服务器**上。
如果另一台电脑也想用同一个码:
1. 服务器拒绝它
2. 那台电脑上**弹通知**:
「该配对码已被使用,请换一个配对码」
3. 它停下等你换码(不会反复弹、不会刷屏)
4. 你在设置里换个新码,它自动接着连上
所以一个配对码同时只能有一台电脑用。
【配对码:单设备单配对码】
这台电脑第一次设的配对码会被服务器记住。
以后就算你重设成别的,服务器也会改回原来那个 ——
手表永远认得出这台电脑。
相关:
--new-code 换一组随机配对码(服务器会按上面的规则处理)
--reset-devices 清空已配对的手表
【目录里这几个文件】
ykxmz.exe 主程序
wlzj.bat 网络自检(连不上时跑它)
xffhq.bat 一键修复防火墙(右键以管理员运行)
fxfhq.bat 放行脚本(程序自动生成的)
【其它命令行】
--setup 重新打开设置窗口
--silent 直接静默运行
--add-device 10 静默运行 + 开放 10 分钟配对
--autostart on|off 开关开机自启
--status 看当前状态
--netcheck 90 网络自检(同 wlzj.bat)
--fix-firewall 提权修复防火墙
【连不上?按这个顺序】
1) 任务管理器里能看到 ykxmz.exe 吗?没有就 --setup
2) 右键 xffhq.bat → 以管理员身份运行
3) 双击 wlzj.bat 做网络自检
4) 手表上点「连不上?点这里诊断」
5) 跨网络的话,确认 --relay-status 显示在线
"""
RELAY_README = f"""========================================================
服务器端(随身 WiFi / Linux) 版本 {VERSION}
========================================================
作用:让**不在同一个网络**的电脑和手表连上。
手表 ──HTTPS──┐
├──> Cloudflare ──> 本机 :8080
电脑 ──HTTPS──┘
本程序是**纯标准库**的 Python,不装任何第三方包。
内存占用约 20MB,加 cloudflared 共约 60MB —— 512MB 的小机器跑得动。
【装】
把本文件夹拷到设备上,然后:
bash install.sh
它会:
1. 检查 python3 / cloudflared(没有就下载)
2. 装两个 systemd 服务并设为开机自启:
lanremote-relay 中转服务
lanremote-tunnel Cloudflare 隧道
3. 打印公网地址和管理页地址
【常用命令】
lanremote-url 打印当前公网地址
lanremote-key 打印管理钥匙
systemctl status lanremote-relay
systemctl status lanremote-tunnel
journalctl -u lanremote-relay -f
【地址老变怎么办?记住一个局域网地址就行】
匿名隧道的公网地址每次重启都会变。查当前地址:
浏览器打开 http://<随身WiFi的IP>:8080/url
比如 http://192.168.2.156:8080/url
它会直接返回当前的公网地址,纯文本,不用登录。
下面还有两个命令(SSH 上去用):
lanremote-url 打印当前公网地址
systemctl restart lanremote-tunnel 换个地址(超时的时候用)
【管理】
管理页: <公网地址>/?key=<lanremote-key 的输出>
管理页顶部也会大字显示"当前公网地址"。
管理密码:120830(存在 data/admin_password.txt,可改)
能看到:设备列表、在线状态、配对码、流量、会话数
能操作:踢会话、抹掉配对码绑定
【重要:这几个参数都是实测调出来的】
install.sh 里给 cloudflared 加了:
--edge-ip-version 4 只走 IPv4
--ha-connections 1 并发连接数
--retries 5 连边缘失败时多重试
为什么(都是踩出来的,不是拍脑袋):
① IPv6:这条线路**分配了 IPv6 地址但实际不通**,
而 Cloudflare 边缘默认优先 IPv6,会导致建隧道一直超时
(日志里是 context deadline exceeded)。
② 协议:**用默认的 QUIC,不要写 --protocol http2**。
实测这条线路把 HTTP/2 用的 TCP 7844 挡了:
TLS handshake with edge error: read tcp ...:7844: i/o timeout
而 QUIC 走 UDP 7844 是通的。
(早期版本这里误写成 --protocol http2,隧道完全起不来。)
③ 连接数:用 1(默认是 4)。这条线路对并发 UDP 很敏感:
链路好时 ha=4 只成功注册 1 条(9/10),ha=2 是 10/10;
链路退化时 ha=2 掉到 0/10(一条失败重试把另一条也拖下水),
而 ha=1 是 15/15 + 10/10(100%)。
多一条连接不是多一份冗余,而是多一份争抢。
冗余交给看门狗(每 30 秒查一次,断了就重启)更靠谱。
换了网络环境可以试着去掉这些参数用默认值。
【关于隧道地址会变】
默认用的是 Cloudflare **匿名快速隧道**,地址每次重启会变。
想固定地址要用**具名隧道**(需要你自己的域名):
cloudflared tunnel login # 浏览器点授权
cloudflared tunnel create lanremote
cloudflared tunnel route dns lanremote relay.你的域名
# 然后把 lanremote-tunnel.service 的 ExecStart 换成:
# cloudflared tunnel --no-autoupdate run lanremote
具名隧道不受匿名隧道限流,稳定性完全不是一个级别。
【省内存】
默认给两个服务各设了 MemoryMax=128M。
要再省就把用不上的服务停掉:
systemctl stop mariadb php8.2-fpm apache2
systemctl disable mariadb php8.2-fpm apache2
"""
WATCH_README = f"""========================================================
控制端(安卓手表) 版本 {VERSION}
========================================================
【装】
把 ykxmz-watch-{VERSION}.apk 传到手表上安装。
手表如果提示"未知来源",在设置里允许一下。
【连】
1) 打开 App,首页有四个东西:
中转设置(跨网络用) 填服务器端给的地址
局域网连接(同一个WiFi)同一个 WiFi 时走这条,最快
输入配对码连接 经中转连
服务器:还没测 点右边「检查服务器」看延迟
2) 电脑和手表在同一个 WiFi -> 点「局域网连接」,自动搜到电脑,点一下就连
电脑在外面 -> 点「输入配对码连接」,填电脑上那个 6 位码
3) 连上后是 10 个标签页:
媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐
【连不上时看这个】
App 会把错误翻译成人话:
"证书校验失败" -> 手表系统太旧或网络被劫持
"证书时间不对" -> 把手表的日期时间调成自动获取
"连接超时" -> 网络慢或被防火墙挡
"电脑拒绝连接" -> 电脑上 ykxmz 没开
"域名解析不了" -> 手表没联网
首页的「检查服务器」能测出到中转的延迟(快/一般/慢三档配色)。
【回退】
系统页最上面三个按钮:
回退上一步 撤销刚才那一步
回退全部 把能撤的都撤了
看能回退什么 列出还能撤哪些
能撤的:所有开关、所有画面(全屏大字/投图/通知等)
撤不了的:关机、重启、删文件、杀进程 —— 这些**不会**出现在回退列表里,
免得你以为什么都能退回去。
"""
def write_readme(path: Path, text: str) -> None:
"""说明文档统一写成 UTF-8 with BOM —— Windows 记事本不加 BOM 会认成 GBK。"""
path.write_bytes(b"\xef\xbb\xbf" + text.replace("\n", "\r\n").encode("utf-8"))
def _zip_dir(zip_path: Path, source: Path, arc_root: str) -> int:
"""把一个目录打成一个 zip(内部带一层 arc_root 目录名)。"""
if zip_path.exists():
zip_path.unlink()
count = 0
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=6) as archive:
for item in sorted(source.rglob("*")):
if item.is_file():
archive.write(item, Path(arc_root) / item.relative_to(source))
count += 1
return count
def _copy_docs(target: Path) -> int:
"""把三份正式文档拷到目标目录(UTF-8 BOM + CRLF)。"""
done = 0
for name in ("LICENSE.txt", "BEFORE-INSTALL.txt", "AFTER-INSTALL.txt"):
src = DOCS / name
if src.is_file():
write_readme(target / name, src.read_text(encoding="utf-8"))
done += 1
else:
print(f" [警告] 缺少文档 {src}")
return done
def main() -> int:
if not AGENT_SRC.is_dir():
print(f" [失败] 找不到被控端: {AGENT_SRC}")
return 1
if not APK_SRC.is_file():
print(f" [失败] 找不到 APK: {APK_SRC}")
return 1
dist = ROOT / "dist"
dist.mkdir(parents=True, exist_ok=True)
work = dist / "_build_tmp"
if work.exists():
shutil.rmtree(work, ignore_errors=True)
results = []
# ================= 1. 被控端 =================
print(" [1/3] 被控端(Windows)")
agent_name = f"ykxmz-agent-{VERSION}"
agent_dir = work / agent_name
shutil.copytree(AGENT_SRC, agent_dir,
ignore=shutil.ignore_patterns("data", "received",
"*.idsig", "*.log"))
for tool in TOOLS:
if not (agent_dir / tool).is_file():
src = ROOT / "tools" / tool
if src.is_file():
shutil.copy2(src, agent_dir / tool)
write_readme(agent_dir / "README-agent.txt", AGENT_README)
for junk in ("先看这个.txt", "使用说明.txt"):
target = agent_dir / junk
if target.is_file():
target.unlink()
_copy_docs(agent_dir)
if not (agent_dir / "ykxmz.exe").is_file():
print(" [失败] 被控端里没有 ykxmz.exe")
return 1
agent_zip = dist / f"{agent_name}.zip"
n = _zip_dir(agent_zip, agent_dir, agent_name)
results.append(("被控端", agent_zip, n))
# ================= 2. 服务器端 =================
print(" [2/3] 服务器端(随身 WiFi / Linux)")
server_name = f"ykxmz-server-{VERSION}"
server_dir = work / server_name
server_dir.mkdir(parents=True)
# 文件清单写死容易漏 —— watchdog.sh / net-tune.sh 都漏过一次。
# 改成:relay 目录里所有 .py / .sh 都带上。
# 后缀白名单:.py / .sh 是服务本体,.ico 是管理页图标。
# 漏掉 .ico 的话,装到新设备上管理页又会变成没有图标的默认样式。
wanted = sorted(
[f for f in RELAY_SRC.iterdir()
if f.suffix in (".py", ".sh", ".ico") and f.is_file()],
key=lambda f: f.name)
for src in wanted:
shutil.copy2(src, server_dir / src.name)
write_readme(server_dir / "README-server.txt", RELAY_README)
_copy_docs(server_dir)
for must in ("relay.py", "install.sh", "watchdog.sh", "net-tune.sh"):
if not (server_dir / must).is_file():
print(f" [警告] 服务器端少了 {must}")
server_zip = dist / f"{server_name}.zip"
n = _zip_dir(server_zip, server_dir, server_name)
results.append(("服务器端", server_zip, n))
# ================= 3. 控制端(直接给 APK)=================
print(" [3/3] 控制端(手表)—— 不打 zip,直接给 APK")
# 顺带把说明也放一份在旁边,方便看
watch_note = dist / f"ykxmz-watch-{VERSION}-说明.txt"
write_readme(watch_note, WATCH_README)
results.append(("控制端", APK_SRC, 1))
shutil.rmtree(work, ignore_errors=True)
# ================= 汇总 =================
print()
print(" ================ 产物 ================")
ok = True
for label, path, count in results:
if not path.is_file():
print(f" {label}: ✗ 缺失!")
ok = False
continue
size = path.stat().st_size / 1024 / 1024
extra = f"({count} 个文件)" if count > 1 else ""
print(f" {label:6} {path.name:34} {size:6.2f} MB {extra}")
if not ok:
return 1
# ================= 验证 =================
print()
print(" 验证压缩包内容:")
for label, path, _count in results:
if path.suffix != ".zip":
continue
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
bad = archive.testzip()
if bad:
print(f" [失败] {path.name} 里有坏文件: {bad}")
return 1
print(f" {path.name}: {len(names)} 个条目,完整")
with zipfile.ZipFile(results[0][1]) as archive:
names = archive.namelist()
if not any("ykxmz.exe" in n for n in names):
print(" [失败] 被控端包里没有 ykxmz.exe")
return 1
with zipfile.ZipFile(results[1][1]) as archive:
names = archive.namelist()
for must in ("relay.py", "install.sh", "watchdog.sh", "net-tune.sh"):
if not any(must in n for n in names):
print(f" [失败] 服务器端包里没有 {must}")
return 1
print()
print(" 三个产物齐全")
return 0
if __name__ == "__main__":
sys.exit(main())