mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 20:40:43 +00:00
用户这次提了一整套升级。这个提交先落地三块(其余几块在做)。
== 一、静默自动更新 ==
用户原话:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新,
其他什么设置都不变 …… 不弹任何窗口出来"。
链路:
手表点按钮 -> 被控端问中转"最新是哪版"
-> 下载到 %TEMP%\ykxmz-update\
-> 校验 sha256
-> 起一个**分离的 .cmd 帮手**
-> 自己退出
-> 帮手等它退出 -> 静默跑安装器 -> 重新拉起
关键设计说明:
* **为什么要有那个 .cmd 帮手**:Windows 上正在运行的 exe 是锁着的,
安装器覆盖不了,所以必须"先退出、再装、再起来",而这三步
不能由被控端自己做(它一退出就没人执行后面的了)。
用 DETACHED_PROCESS 起的助手不受父进程退出影响。
* **配置为什么不会丢**:配置在 %ProgramData%\ykxmz\data\,
**不在程序目录里**。安装器升级只替换 {app} 下的文件,
不碰 ProgramData —— 中转地址、配对码、各种开关原样保留。
这是这个功能能做的前提。
* **更新源是用户自己的中转站**,不是作者的服务器,也不是任何
写死的公网地址。被控端问的是它自己配置的那个地址。
中转没有这个接口(老版本服务端)时会明确提示"先手动升级一次服务端"。
* **sha256 必须校验**:自更新最大的风险是下到一半断了或被换了,
装上去是个坏程序、而且它还会自己再更新 —— 螺旋下去很难救。
对不上就**放弃这一轮**,保持现状(现状至少能用)。
中转新增:GET /v1/update/latest?platform=windows
返回 version / name / url / size / sha256。版本比较按**数字段**
(不能按字符串,否则 "1.9.0" > "2.1.4")。sha256 按
(文件名,大小,mtime) 缓存,避免每次重算 9MB。
手表端:"电脑端版本:点这里查看" 按钮,查完显示
"电脑端 2.1.4 → 2.1.5(点这里更新)",确认后才真的更新。
== 二、远程鼠标控制 ==
新增 windows/lanremote/input.py,用 SendInput 模拟鼠标键盘。
* **为什么用 SendInput 而不是 mouse_event**:后者是 Win95 时代的
API,会被 UIPI 拦,而且没有 dwExtraInfo(多个注入源会互相干扰)。
SendInput 还能一次打包多个事件,按下+抬起之间不会插进别的输入。
* **坐标是 0..65535 归一化**,而且**必须除以 (屏幕宽-1)**。
少了那个 -1,最右边和最下边几像素点不到 —— 表现为
"鼠标到不了屏幕边缘",任务栏在底下的机器上尤其明显。
* **INPUT 结构在 32/64 位下大小不同**(28 / 40),
ctypes 里按进程位数算,不能写死。
* 权限说明:目标窗口如果是管理员权限的(UAC 提升过的),
普通权限的注入会被 UIPI 拦掉。这不是 bug,是 Windows 的设计。
被控端以管理员运行就都能点。
== 三、一键显示桌面 ==
用户要求快捷栏里那个"不能改"的按钮就是这个功能,等于 Win+D。
和鼠标控制共用 input.py。
== 顺手修的一个自己造的坑 ==
往 server.py 插方法时,**两次**把 dispatch 的兜底
`return {"ok": False, "error": f"未知命令: {cmd}"}` 一起卷进了新方法里:
* pyflakes 报 "undefined name 'cmd'"
* 而且 dispatch 丢了兜底分支(未知命令会返回 None)
第一次靠 pyflakes 抓到,第二次也是。为了以后不再犯,
`_tests/test_update_flow.py` 里加了**结构断言**:
兜底必须在带 CMD_UPDATE 的那个方法体里,而各新方法里不能出现它。
(那条断言第一版写得太死 —— 要求"紧跟 CMD_UPDATE 分支",
后来往 dispatch 里插了新分支就误报了。改成验结构真相。)
== 验证 ==
全量回归(含新增的 3a10 / 3a11 两关)全部 PASS
静默自动更新:协议 + updater 15 项 + 服务端结构 8 项
远程鼠标:坐标归一化 / 滚轮夹取 / Win+D 事件顺序 / 非法参数 22 项
610 个 Python 文件语法通过
实际注入鼠标成功(move / click / scroll / show_desktop)
== 还没做的(下一批) ==
手表顶部快捷栏(可自定义最多 4 个,1 个固定"显示桌面")
屏幕镜像去掉提示条、去掉所有弹窗
托盘图标改成设置项
「允许新设备连接」开关(中转后台 + 安卓端)
弹文字窗口位置/大小可自定义
中转后台设备列表显示版本 + 远程点更新
== 顺手清理 ==
_tests/out_app/ 下面那堆 .class 是 Java 编译检查的产物,
以前误提交进仓库了(152 个文件)。这次从索引里移除并加进 .gitignore。
685 lines
26 KiB
Python
685 lines
26 KiB
Python
# -*- 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 = "2.1.6"
|
||
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,直接装。
|
||
|
||
|
||
【怎么用】四步
|
||
|
||
**先说最要紧的一条:本软件不带任何中转地址,也不提供官方服务器。**
|
||
只在同一个 WiFi 下用(手表 + 电脑连同一个路由器)的话,
|
||
跳过第一步和第四步,装完就能连。
|
||
要跨网络(在办公室管教室),必须自己搭一台中转站。
|
||
|
||
第一步:架服务器端(只做一次,**跨网络才需要**)
|
||
把「2-服务器端」整个文件夹拷到那台能上外网的 Linux 小机器上,执行:
|
||
bash deploy.sh
|
||
它会装好中转服务 + 网络调优 + Cloudflare 隧道,并打印一个
|
||
**属于你自己的公网地址**。记下这个地址。
|
||
(形如 https://你的域名 —— 域名是你自己的,脚本里没有默认域名。)
|
||
|
||
小提示:忘了地址就在那台机器上执行 lanremote-url,或者浏览器打开
|
||
http://<那台机器的IP>:8080/url —— 直接返回当前地址。
|
||
|
||
第二步:装被控端(电脑上,只做一次)
|
||
双击安装包
|
||
→ 输入 6 位配对码(自己定,比如 789113)
|
||
→ 勾上"开机自启"和"放行防火墙"
|
||
→ 点确定,会弹一次管理员确认,之后窗口全消失,后台静默运行
|
||
|
||
第三步:装控制端(手表上)
|
||
装 APK
|
||
→ 点「自动搜索电脑」(同一个 WiFi 下用这个,不用填地址)
|
||
→ 或者点「输入配对码连接」填电脑上那个 6 位码
|
||
|
||
第四步:跨网络的话,把中转地址填上(**自己搭的那个**)
|
||
电脑: ykxmz.exe --relay https://你的域名
|
||
手表: 首页点「中转设置」→ 填同一个地址
|
||
然后回首页点「输入配对码连接」,输那 6 位数字,连上。
|
||
|
||
|
||
【关于配对码:单设备单配对码】
|
||
|
||
一台电脑第一次设的配对码会被服务器记住。
|
||
以后同一台电脑再报别的码,服务器会**改回它原来那个**。
|
||
好处:你不会因为重装/误操作把配对码搞乱,手表永远认得出这台电脑。
|
||
|
||
一个配对码也不能同时属于两台电脑(服务器会拒绝)。
|
||
|
||
|
||
【服务器端能管什么】
|
||
|
||
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
|
||
管理密码:<改从环境变量读>(可在服务器上改)
|
||
|
||
能看:哪些设备在线、配对码、流量、会话数
|
||
能做:踢掉某个设备的会话、抹掉某个配对码绑定
|
||
|
||
注意:中转服务器**只是管道**,它不认识传输内容。
|
||
配对、令牌校验、权限判断全部由电脑端自己做 ——
|
||
所以就算中转机被攻破,也进不了你的电脑。
|
||
|
||
|
||
【每台设备分别看哪个说明】
|
||
|
||
电脑: 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,点「是」
|
||
→ 黑终端一闪,在修防火墙
|
||
→ 窗口全消失,转入后台静默运行
|
||
(没有托盘图标、没有窗口、没有终端)
|
||
|
||
|
||
【开箱即用:一个地址都不用填(同一个 WiFi 下)】
|
||
|
||
手表和这台电脑连**同一个路由器**时,直接就能连上,
|
||
配对码对上就行 —— 不需要任何服务器、不需要填任何地址。
|
||
|
||
程序会自己往局域网广播找电脑,所以电脑上装了几块虚拟网卡、
|
||
IP 是多少,都不用管。
|
||
|
||
|
||
【跨网络:必须自己搭一台中转站,然后填地址】
|
||
|
||
⚠ **本软件没有内置任何中转地址,作者也不对外提供中转服务。**
|
||
要跨网络用(比如在办公室管教室),你得自己部署一台中转站:
|
||
服务端压缩包里有 deploy.sh,一条命令装好,它会打印一个
|
||
属于你自己的公网地址。然后把这个地址填给被控端:
|
||
|
||
ykxmz.exe --relay https://你的域名
|
||
|
||
只在同一个 WiFi 下用的话,这一节完全不用管。
|
||
|
||
|
||
【支持多个地址,按顺序试】
|
||
|
||
用分号隔开,第一个通的就用它。推荐把局域网地址也写进去:
|
||
|
||
ykxmz.exe --relay "192.168.1.50:8080;https://你的域名"
|
||
|
||
为什么带上局域网地址?
|
||
* 快:局域网几十毫秒,绕隧道要一秒多
|
||
* 稳:不依赖隧道,隧道抽风也不影响
|
||
* 那个地址永远不变,不用管公网地址变没变
|
||
|
||
地址**不用写 http:// 或 https://**,程序会自己判断:
|
||
192.168.x.x / 10.x.x.x / *.local -> 按 http 连(局域网是明文)
|
||
域名 -> 按 https 连
|
||
|
||
看看当前状态(会显示用了哪个地址、有几个候选):
|
||
ykxmz.exe --relay-status
|
||
|
||
关掉中转(只走局域网直连):
|
||
ykxmz.exe --relay ""
|
||
|
||
|
||
【忘了中转地址?】
|
||
|
||
如果那台小机器在你身边(同一个网络),浏览器打开:
|
||
http://<它的IP>:8080/url
|
||
它会直接返回当前的公网地址(纯文本,不用登录)。
|
||
|
||
或者在那台机器上执行:
|
||
lanremote-url
|
||
|
||
小技巧:把局域网地址也填进 --relay,就永远不用管公网地址变没变:
|
||
ykxmz.exe --relay "192.168.1.50:8080;https://你的域名"
|
||
|
||
|
||
【配对码被别人占了会弹通知】
|
||
|
||
每台电脑第一次设的配对码会存到**中转服务器**上。
|
||
|
||
如果另一台电脑也想用同一个码:
|
||
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 deploy.sh
|
||
|
||
它会一口气做完这些,不用你一步步敲:
|
||
|
||
1. 检查并安装 cloudflared(已经装了就用现成的)
|
||
2. 把中转程序装到 /root/lanremote-relay/
|
||
3. 网络调优(keepalive / MSS / MTU / 开机加速)
|
||
4. 注册 systemd 服务并设成开机自启
|
||
(中转、网络调优、两份隧道、看门狗)
|
||
5. 配好 Cloudflare 隧道
|
||
6. 自检,然后把**公网地址**打印出来
|
||
|
||
── 三种常见情形 ──
|
||
|
||
① 机器上已经有隧道凭据(/root/.cloudflared/*.json)
|
||
直接: bash deploy.sh
|
||
|
||
② 有 Cloudflare 给的隧道 Token
|
||
TUNNEL_TOKEN=eyJhIjoi... bash deploy.sh
|
||
(用环境变量而不是 --token,Token 就不会留在 shell 历史里)
|
||
|
||
③ 换域名 / 只装中转不要隧道
|
||
bash deploy.sh --hostname relay.你的域名
|
||
bash deploy.sh --skip-tunnel
|
||
|
||
看全部参数: bash deploy.sh --help
|
||
|
||
|
||
**重跑 deploy.sh = 升级**,不会把已有配置冲掉:
|
||
设备数据、配对码、管理密码、隧道凭据、手工改过的 config.yml
|
||
都会原样保留。
|
||
|
||
|
||
【常用命令】
|
||
|
||
lanremote-status 看整体状态(服务 / 端口 / 公网 / 内存)
|
||
lanremote-url 打印当前公网地址
|
||
lanremote-key 打印管理钥匙
|
||
systemctl status lanremote-relay
|
||
systemctl status lanremote-tunnel
|
||
journalctl -u lanremote-relay -f
|
||
|
||
|
||
【关于 install.sh(旧版脚本,仍在包里)】
|
||
|
||
装匿名快速隧道,公网地址每次重启都变。
|
||
新机器建议用 deploy.sh(具名隧道,地址固定)。
|
||
install.sh 保留是为了兼容已经用它装过的机器。
|
||
|
||
|
||
【地址老变怎么办?记住一个局域网地址就行】
|
||
|
||
匿名隧道的公网地址每次重启都会变。查当前地址:
|
||
|
||
浏览器打开 http://<随身WiFi的IP>:8080/url
|
||
|
||
它会直接返回当前的公网地址,纯文本,不用登录。
|
||
|
||
具名隧道(deploy.sh 装的)地址是固定的,不用管这个。
|
||
|
||
|
||
【管理】
|
||
|
||
官网: <公网地址>/
|
||
管理页: <公网地址>/?key=<lanremote-key 的输出>
|
||
管理密码: <改从环境变量读>(存在 data/admin_password.txt,可改)
|
||
|
||
能看到:设备列表、在线状态、配对码、流量、会话数
|
||
能操作:踢会话、抹掉配对码绑定
|
||
|
||
|
||
【下载区:静态文件走 Cloudflare 边缘缓存】
|
||
|
||
官网页面上的三个文件(安装包 / 服务端 zip / APK)和产品截图,
|
||
都由 Cloudflare 的边缘节点**直接发送**,不用回源到这台小机器。
|
||
|
||
这不是默认行为 —— relay.py 给这些路径发了
|
||
`Cache-Control: public, max-age=604800, immutable`
|
||
(下载文件名带版本号,同一名字内容永不改变,所以能长缓存)。
|
||
|
||
为什么重要:这台机器通常在 4G 上,回源吞吐实测只有 3~67 KB/s,
|
||
15 MB 的安装包根本下不动。走边缘之后实测 695 KB/s,快四十多倍,
|
||
而且**不消耗这台机器的流量**。
|
||
|
||
换了新版本的安装包怎么办?
|
||
文件名里带版本号(ykxmz-setup-1.8.2.exe),新文件是新 URL,
|
||
自然就是新的缓存条目,不会拿到旧的。
|
||
旧版本的条目到期(7 天)后自动消失。
|
||
|
||
|
||
【网络调优:这些参数都是实测调出来的】
|
||
|
||
deploy-tune.sh 会做这五件事,改动之前先看它开头的说明:
|
||
|
||
① TCP keepalive 30/10/3
|
||
这条 4G 线路会**静默丢连接**(不发明文 FIN/RST)。
|
||
Linux 默认要 7200 秒才发现死链,表现就是"过几分钟突然 502"。
|
||
改成 30 秒首发探测之后,最快 60 秒就能发现并重建。
|
||
|
||
② tcp_mtu_probing = 1
|
||
接口写着 MTU 1500,但实际路径扛不住(1472 通、1500 全丢)。
|
||
不打开探测的话,新建 TLS 连接约 30% 失败。
|
||
|
||
③ MSS 钳制到 1360
|
||
只限制 TCP 段,不动链路层。
|
||
实测:不夹 87%,夹到 1360/1400 都是 100%。
|
||
|
||
④ 接口 MTU 1400
|
||
**这一条是专门给 QUIC 隧道的**:MSS 钳制走 iptables 的
|
||
TCPMSS 目标,只对 TCP 生效;QUIC 跑 UDP,完全不受它保护。
|
||
QUIC 的包超过路径 MTU 就会被丢,而 UDP 丢包不回 ICMP,
|
||
只能干等握手超时(日志里那句
|
||
"failed to dial to edge with quic: timeout: no recent network activity")。
|
||
实测把 MTU 从 1500 降到 1400:建齐 6 条连接 28 秒 -> 13 秒。
|
||
|
||
⑤ mask 掉 systemd-networkd-wait-online
|
||
这台机器 wlan0 归 NetworkManager 管,
|
||
systemd-networkd-wait-online 傻等一个不归它的网络,
|
||
干等 2 分钟超时,把 relay / tunnel 全拖住。
|
||
实测:掉电重启后公网恢复 2 分 10 秒 -> 24 秒。
|
||
|
||
|
||
【隧道:两种协议同时在跑(这是故意的)】
|
||
|
||
deploy.sh 会装**两份**隧道服务,而且**两份都启用**:
|
||
|
||
lanremote-tunnel QUIC
|
||
lanremote-tunnel-h2 HTTP/2
|
||
|
||
它们跑的是**同一个隧道 ID**,Cloudflare 会把请求分摊到两个实例
|
||
注册上来的全部连接上(合计 10 条,横跨两种协议)。
|
||
|
||
为什么要这么干 —— 两种协议各有各的病,实测:
|
||
|
||
QUIC 尾部延迟好(P90 约 2.5 秒),
|
||
但建连会 "no recent network activity" 超时,偶发失败
|
||
HTTP/2 建连可靠(走 TCP,受 MSS 钳制保护),
|
||
但尾部延迟差(P90 约 14 秒)——
|
||
TCP 队头阻塞:一条连接上丢一个包,所有流一起卡
|
||
|
||
两个同时跑 = 协议级故障隔离。QUIC 那半边犯病时 HTTP/2 还在服务,
|
||
反过来也一样。实测连续 25 分钟可用率 100%(单协议时是 96~98%)。
|
||
|
||
所以**不要**为了"省内存"随手停掉其中一个 —— 停了就退化成单协议了。
|
||
|
||
真想只留一种(比如这台机器的线路对 UDP 特别不友好,
|
||
日志里大量 "Failed to dial a quic connection"):
|
||
|
||
systemctl disable --now lanremote-tunnel-h2 # 只留 QUIC
|
||
systemctl disable --now lanremote-tunnel # 只留 HTTP/2
|
||
|
||
|
||
【改端口 / 改协议 / 换域名】
|
||
|
||
重跑 deploy.sh,带上参数就行(幂等,不会冲掉已有数据):
|
||
|
||
bash deploy.sh --protocol http2 # 只跑 HTTP/2(停掉 QUIC 那半边)
|
||
bash deploy.sh --protocol quic # 只跑 QUIC
|
||
bash deploy.sh # 不传 = 两条一起跑(推荐)
|
||
bash deploy.sh --port 9090 # 换监听端口
|
||
bash deploy.sh --hostname relay.你的域名
|
||
|
||
|
||
【省内存】
|
||
|
||
默认给服务设了 MemoryMax(relay 128M / tunnel 160M)。
|
||
要再省就把用不上的服务停掉:
|
||
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 里没有内置任何中转地址,作者也不对外提供中转服务。**
|
||
|
||
只在同一个 WiFi 下用的话,不用填任何地址 —— 点「局域网连接」就行。
|
||
|
||
要跨网络,你必须先自己部署一台中转站(服务端 zip 里有 deploy.sh
|
||
一键脚本),拿到它打印的公网地址,填进「中转设置」。
|
||
电脑那边也要填同一个地址:ykxmz.exe --relay https://你的域名
|
||
|
||
两个地址填得不一样是连不上的 —— 这是最常见的坑。
|
||
|
||
|
||
【连不上时看这个】
|
||
|
||
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 / .ico / .service / .timer 都带上。
|
||
# 后缀白名单:
|
||
# .py 服务本体
|
||
# .sh 部署/运维脚本
|
||
# .ico 管理页图标(漏了的话新设备上管理页是默认样式)
|
||
# .service /
|
||
# .timer systemd 单元。**这两个是后加的** ——
|
||
# 加监测单元的时候才发现白名单里没有它们,
|
||
# 于是 monitor.sh 进了包、拉它的 timer 却没进,
|
||
# 装到新机器上监测根本不会启动(而且不会报错,只是静默不跑)。
|
||
wanted = sorted(
|
||
[f for f in RELAY_SRC.iterdir()
|
||
if f.suffix in (".py", ".sh", ".ico", ".service", ".timer")
|
||
and f.is_file()],
|
||
key=lambda f: f.name)
|
||
for src in wanted:
|
||
shutil.copy2(src, server_dir / src.name)
|
||
# ⚠ 产品截图是个**目录**,上面那个 iterdir() 只收文件、而且不递归,
|
||
# 所以 site_shots/ 一直没被打进服务端包 ——
|
||
# 装到新机器上官网的截图全是 404。
|
||
# 这个是拿「从 zip 解压出来真跑一遍」的端到端测试才发现的。
|
||
shots_src = RELAY_SRC / "site_shots"
|
||
if shots_src.is_dir():
|
||
shutil.copytree(shots_src, server_dir / "site_shots")
|
||
else:
|
||
print(" [警告] relay/site_shots 不存在,官网截图会缺失")
|
||
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}")
|
||
# 一键部署脚本必须进包 —— 用户拿到的 zip 里要有它才能双击/一条命令跑起来
|
||
for must in ("deploy.sh", "deploy-tune.sh"):
|
||
if not (server_dir / must).is_file():
|
||
print(f" [失败] 服务器端少了一键部署脚本 {must}")
|
||
return 1
|
||
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",
|
||
"deploy.sh", "deploy-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())
|