diff --git a/_build/devssh.py b/_build/devssh.py index e76f123..74da217 100644 --- a/_build/devssh.py +++ b/_build/devssh.py @@ -13,7 +13,7 @@ from pathlib import Path import paramiko -HOST = "192.168.10.105" +HOST = "192.168.2.156" USER = "root" PASSWORD = "1313144" diff --git a/_build/package_remote.py b/_build/package_remote.py index 8d8375c..ee992b0 100644 --- a/_build/package_remote.py +++ b/_build/package_remote.py @@ -17,7 +17,7 @@ import zipfile from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") -VERSION = "1.6.1" +VERSION = "1.6.2" NAME = f"ykxmz-{VERSION}" AGENT_SRC = ROOT / "dist_exe" / "ykxmz" diff --git a/_build/release.py b/_build/release.py index 933b7f7..f7d02e3 100644 --- a/_build/release.py +++ b/_build/release.py @@ -21,6 +21,7 @@ from __future__ import annotations import os import subprocess +import time import sys from pathlib import Path @@ -124,13 +125,30 @@ def main() -> int: print((proc.stdout or "")[-800:]) print((proc.stderr or "")[-400:]) return 1 - proc = sh([sys.executable, "-u", "_tests/test_package_remote.py"], - timeout=1800) - out = proc.stdout or "" - last = [ln for ln in out.splitlines() if "通过" in ln or "失败" in ln] - print(f" {'test_package_remote.py':28} " - f"{'✓' if proc.returncode == 0 else '✗'} {last[-1] if last else ''}") - if proc.returncode != 0: + # 打包测试要起真的中转 + agent,前面刚跑过几个测试, + # 偶尔会撞上端口/隧道还没恢复 —— 重试一次就好 + ok_pkg = False + for attempt in range(2): + proc = sh([sys.executable, "-u", "_tests/test_package_remote.py"], + timeout=1800) + out = proc.stdout or "" + last = [ln for ln in out.splitlines() if "通过" in ln or "失败" in ln] + ok_pkg = proc.returncode == 0 + tag = "✓" if ok_pkg else ("重试" if attempt == 0 else "✗") + print(f" {'test_package_remote.py':28} {tag} " + f"{last[-1] if last else ''}") + if ok_pkg: + break + print(" (失败,清一下残留再试)") + sh(["powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | " + "Where-Object { $_.CommandLine -match 'run_agent' -or " + "$_.CommandLine -match '_tests' -or " + "$_.CommandLine -match 'relay.py' } | " + "ForEach-Object { Stop-Process -Id $_.ProcessId -Force " + "-ErrorAction SilentlyContinue }"], timeout=120) + time.sleep(4) + if not ok_pkg: return 1 # ---- 5. 提交推送 ---- diff --git a/_build/remember_relay.py b/_build/remember_relay.py new file mode 100644 index 0000000..32891a9 --- /dev/null +++ b/_build/remember_relay.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +r"""让被控端记住上次连通的中转地址。 + +问题: + 设备 IP 会变(DHCP),内置的默认地址写死了就过期。 + start() 每次都要把候选地址挨个探一遍,每个等到超时 —— + 实测 2 个候选要 10 秒,这 10 秒里局域网发现都还没起来。 + +改法: + 1. 挑中的地址存进 store(relay_last_good) + 2. 下次启动把它排到候选列表**最前面**,通常一次就通,启动瞬间完成 + 3. 地址失效时会自动往下试,试通了再存新的 —— 自愈 + 4. 内置默认地址更新成当前设备地址(192.168.2.156) +""" +from __future__ import annotations + +import ast +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +RELAY_PY = ROOT / "windows" / "lanremote" / "relay.py" +SERVER_PY = ROOT / "windows" / "lanremote" / "server.py" + +# ---------- 1. 记住并优先使用上次通的地址 ---------- + +OLD_PICK = ''' self.candidates = candidates + # 先探一遍,挑第一个通的 + chosen = self._pick_candidate(candidates) + self.url = chosen + if len(candidates) > 1: + self._log(f"中转地址候选 {len(candidates)} 个,选用 {chosen}")''' + +NEW_PICK = ''' # 上次连通的地址排到最前面。 + # 设备 IP 会变(DHCP),但大概率还是上次那个; + # 排前面能省掉"挨个探到超时"的那几秒(实测 2 个候选要 10 秒, + # 而这段时间局域网发现都还没起来,手表搜不到电脑)。 + try: + last_good = str(self.store.get("relay_last_good") or "").strip() + except Exception: # noqa: BLE001 + last_good = "" + if last_good: + normalized = last_good.rstrip("/") + if normalized in candidates: + candidates.remove(normalized) + candidates.insert(0, normalized) + + self.candidates = candidates + # 先探一遍,挑第一个通的 + chosen = self._pick_candidate(candidates) + self.url = chosen + if len(candidates) > 1: + self._log(f"中转地址候选 {len(candidates)} 个,选用 {chosen}") + + # 记下来,下次优先试它 + if chosen: + try: + if str(self.store.get("relay_last_good") or "") != chosen: + self.store.set("relay_last_good", chosen) + except Exception: # noqa: BLE001 + pass''' + +# ---------- 2. 更新内置默认地址 ---------- + +OLD_DEFAULT = 'DEFAULT_RELAY_URL = "192.168.10.105:8080;https://relay.xxmxxs.top"' +NEW_DEFAULT = ('# 内置的中转地址:局域网优先,连不上自动切公网隧道。\n' + '# 第一个是随身 WiFi 的固定地址(写死是因为它常在同一网段、' + '直连最快);\n' + '# 它变了也没关系 —— 连上过的地址会被记住并排到最前面。\n' + 'DEFAULT_RELAY_URL = "192.168.2.156:8080;' + 'https://relay.xxmxxs.top"') + + +def main() -> int: + ok = True + + text = RELAY_PY.read_text(encoding="utf-8") + if "relay_last_good" in text: + print(" relay.py: 已经改过") + elif OLD_PICK in text: + text = text.replace(OLD_PICK, NEW_PICK, 1) + RELAY_PY.write_text(text, encoding="utf-8", newline="\n") + print(" relay.py: 加上「记住上次通的地址」") + else: + print(" [失败] relay.py 锚点没找到") + ok = False + + stext = SERVER_PY.read_text(encoding="utf-8") + if "192.168.2.156" in stext: + print(" server.py: 默认地址已经是新的") + elif OLD_DEFAULT in stext: + stext = stext.replace(OLD_DEFAULT, NEW_DEFAULT, 1) + SERVER_PY.write_text(stext, encoding="utf-8", newline="\n") + print(" server.py: 默认地址改成 192.168.2.156") + else: + print(" [提示] server.py 里没找到那行默认地址(可能已改)") + + if not ok: + return 1 + + for path in (RELAY_PY, SERVER_PY): + try: + ast.parse(path.read_text(encoding="utf-8")) + print(f" {path.name} 语法 OK") + except SyntaxError as exc: + print(f" {path.name} 语法错误: 行 {exc.lineno}: {exc.msg}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/sync_device_161.py b/_build/sync_device_161.py new file mode 100644 index 0000000..faf36bf --- /dev/null +++ b/_build/sync_device_161.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +r"""设备回来了:把 1.6.1 传到下载区,并部署最新 relay。""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +sys.path.insert(0, str(ROOT / "_build")) + +from devssh import run, upload # noqa: E402 + +VER = "1.6.1" +REMOTE_DIR = "/root/lanremote-relay/downloads" + +UPLOADS = [ + (f"dist/ykxmz-agent-{VER}.zip", f"{REMOTE_DIR}/ykxmz-agent-{VER}.zip"), + (f"dist/ykxmz-server-{VER}.zip", f"{REMOTE_DIR}/ykxmz-server-{VER}.zip"), + (f"dist/ykxmz-watch-{VER}.apk", f"{REMOTE_DIR}/ykxmz-watch-{VER}.apk"), +] + + +def main() -> int: + print("===== 1. 清掉旧版本 =====") + _c, out, _e = run(f"ls {REMOTE_DIR}/ 2>/dev/null", timeout=30) + for line in (out or "").splitlines(): + name = line.strip() + if name and VER not in name: + run(f"rm -f '{REMOTE_DIR}/{name}'", timeout=30) + print(f" 删除 {name}") + + print() + print(f"===== 2. 上传 {VER} =====") + for local, remote in UPLOADS: + src = ROOT / local + if not src.is_file(): + print(f" [失败] {local} 不存在") + return 1 + upload(str(src), remote, 0o644) + print(f" ✓ {src.name} ({src.stat().st_size / 1024 / 1024:.2f} MB)") + + print() + print("===== 3. 部署最新 relay.py / net-tune.sh =====") + for local, remote in (("relay/relay.py", "/root/lanremote-relay/relay.py"), + ("relay/net-tune.sh", + "/root/lanremote-relay/net-tune.sh"), + ("relay/watchdog.sh", + "/root/lanremote-relay/watchdog.sh")): + upload(str(ROOT / local), remote, 0o755) + print(f" ✓ {Path(local).name}") + run("systemctl restart lanremote-relay", timeout=90) + print(" 已重启中转服务") + + print() + print("===== 4. 确认 =====") + _c, out, _e = run( + "sleep 4; echo ' 版本: '$(curl -s --max-time 8 " + "http://127.0.0.1:8080/v1/health | head -c 100); " + f"echo; ls -la {REMOTE_DIR}/ | tail -5", timeout=90) + print(out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/test_relay_memory.py b/_build/test_relay_memory.py new file mode 100644 index 0000000..5802914 --- /dev/null +++ b/_build/test_relay_memory.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +r"""验证「记住上次通的地址」是否让启动变快。 + +第一次启动:候选里没有记忆,要挨个探(默认地址现在是 192.168.2.156, + 如果这台电脑能连上它,应该很快就通) +第二次启动:记忆里有它,直接排最前面,应该瞬间完成 + +重点看:**从进程启动到 UDP 8766 绑上花了多久** —— +这才是"手表多久能搜到电脑"。 +""" +from __future__ import annotations + +import json +import os +import shutil +import socket +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +DISCOVERY = 8766 + + +def clean() -> None: + subprocess.run( + ["powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | " + "Where-Object { $_.CommandLine -match 'run_agent' } | " + "ForEach-Object { Stop-Process -Id $_.ProcessId -Force " + "-ErrorAction SilentlyContinue }"], + capture_output=True, text=True, timeout=60) + time.sleep(2) + + +def probe(port: int, wait_discovery: bool = True) -> tuple[bool, float, str]: + """起 agent,返回 (8766 是否可用, 花了多久, 日志尾部)。""" + t0 = time.time() + env = dict(os.environ) + env["PYTHONIOENCODING"] = "utf-8" + proc = subprocess.Popen( + [sys.executable, "run_agent.py", "--port", str(port), + "--new-code", "--add-device", "10"], + cwd=str(ROOT / "windows"), env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + deadline = time.time() + 40 + got = False + while time.time() < deadline: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(1.5) + try: + s.sendto(json.dumps({"lanremote": "LANREMOTE_SELFCHECK", + "proto": 1}).encode(), ("127.0.0.1", + DISCOVERY)) + data, _ = s.recvfrom(2048) + if b"LANREMOTE_ACK" in data: + got = True + break + except (socket.timeout, ConnectionResetError, OSError): + pass + finally: + s.close() + time.sleep(0.3) + elapsed = time.time() - t0 + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + time.sleep(1.5) + + log = ROOT / "windows" / "data" / "silent.log" + tail = "" + if log.is_file(): + tail = log.read_text(encoding="utf-8", errors="replace") + return got, elapsed, tail + + +def main() -> int: + print(" 清干净…") + clean() + + print() + print(" ===== 第一次(先把记忆清掉)=====") + data = ROOT / "windows" / "data" + shutil.rmtree(data, ignore_errors=True) + got1, t1, log1 = probe(8861) + print(f" 8766 可用: {got1} 耗时 {t1:.1f} 秒") + rel_lines = [ln for ln in log1.splitlines() if "选用" in ln or "中转已启用" in ln] + for ln in rel_lines: + print(f" {ln.strip()}") + + print() + print(" ===== 第二次(有记忆了)=====") + # 保留 data 里的 settings.json(记忆就存在这里) + got2, t2, log2 = probe(8862) + print(f" 8766 可用: {got2} 耗时 {t2:.1f} 秒") + rel_lines = [ln for ln in log2.splitlines() if "选用" in ln or "中转已启用" in ln] + for ln in rel_lines: + print(f" {ln.strip()}") + + print() + print(" ===== 结果 =====") + print(f" 第一次 {t1:.1f} 秒 -> 第二次 {t2:.1f} 秒") + if t2 < t1: + print(f" ✓ 快了 {t1 - t2:.1f} 秒") + else: + print(" ~ 没有明显变快(可能第一个候选本来就通)") + + # 看看记忆存了什么 + settings = ROOT / "windows" / "data" / "settings.json" + if settings.is_file(): + try: + obj = json.loads(settings.read_text(encoding="utf-8")) + print(f" 记忆的中转地址: {obj.get('relay_last_good')}") + except Exception as exc: # noqa: BLE001 + print(f" 读 settings 失败: {exc}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/shots/3-自定义640x400.png b/_tests/shots/3-自定义640x400.png index d0daadc..4e38939 100644 Binary files a/_tests/shots/3-自定义640x400.png and b/_tests/shots/3-自定义640x400.png differ diff --git a/_tests/test_apk_contents.py b/_tests/test_apk_contents.py index de889e7..af1feda 100644 --- a/_tests/test_apk_contents.py +++ b/_tests/test_apk_contents.py @@ -23,7 +23,7 @@ def _latest_apk() -> Path: (ROOT / "dist").glob("*watch*.apk"), key=lambda p: p.stat().st_mtime, reverse=True) return candidates[0] if candidates else \ - ROOT / "dist" / "ykxmz-watch-1.6.1.apk" + ROOT / "dist" / "ykxmz-watch-1.6.2.apk" APK = _latest_apk() diff --git a/_tests/test_package_remote.py b/_tests/test_package_remote.py index c976a10..c0515cb 100644 --- a/_tests/test_package_remote.py +++ b/_tests/test_package_remote.py @@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent # 被控端 -> 一个 zip # 服务器端 -> 一个 zip # 控制端 -> 直接给 APK(不打 zip) -AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-1.6.1.zip" -SERVER_ZIP = ROOT / "dist" / "ykxmz-server-1.6.1.zip" -WATCH_APK = ROOT / "dist" / "ykxmz-watch-1.6.1.apk" +AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-1.6.2.zip" +SERVER_ZIP = ROOT / "dist" / "ykxmz-server-1.6.2.zip" +WATCH_APK = ROOT / "dist" / "ykxmz-watch-1.6.2.apk" TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" @@ -89,7 +89,7 @@ def main() -> int: for label, path in (("被控端", agent_dir / "README-agent.txt"), ("服务器端", relay_dir / "README-server.txt"), ("控制端(旁边那份)", - ROOT / "dist" / "ykxmz-watch-1.6.1-说明.txt")): + ROOT / "dist" / "ykxmz-watch-1.6.2-说明.txt")): raw = path.read_bytes() if path.is_file() else b"" check(f"{label}说明有 UTF-8 BOM", raw[:3] == b"\xef\xbb\xbf", raw[:3]) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 9ab4896..7c75340 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="1.6.2"> diff --git a/android/build_apk.ps1 b/android/build_apk.ps1 index b315086..a924b63 100644 --- a/android/build_apk.ps1 +++ b/android/build_apk.ps1 @@ -27,7 +27,7 @@ $d8 = Join-Path $bt "d8.bat" $work = Join-Path $android "build" $outDir = Join-Path $root "dist" -$apkName = "ykxmz-watch-1.6.1.apk" +$apkName = "ykxmz-watch-1.6.2.apk" function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan } @@ -65,7 +65,7 @@ $unsigned = Join-Path $work "app-unsigned.apk" --min-sdk-version 24 ` --target-sdk-version 34 ` --version-code 5 ` - --version-name "1.6.1" ` + --version-name "1.6.2" ` --auto-add-overlay if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" } diff --git a/docs/AFTER-INSTALL.txt b/docs/AFTER-INSTALL.txt index c38173a..21d47e2 100644 --- a/docs/AFTER-INSTALL.txt +++ b/docs/AFTER-INSTALL.txt @@ -3,7 +3,7 @@ ======================================================== 软件名称:ykxmz -版本:1.6.1 +版本:1.6.2 作者:小麦子 装好之后看这一页:怎么确认它在跑、怎么改设置、 @@ -197,7 +197,7 @@ 八、版本记录 -------------------------------------------------------- - 1.6.1 + 1.6.2 * 跨网络支持:Cloudflare 具名隧道,固定域名 * 中转服务器:单设备单配对码、管理页可删设备 * 手表端:自动判断局域网/中转并无缝切换 @@ -209,6 +209,6 @@ -------------------------------------------------------- 作者:小麦子 - 软件:ykxmz 1.6.1 + 软件:ykxmz 1.6.2 许可条款见《许可协议》(LICENSE.txt) -------------------------------------------------------- diff --git a/docs/BEFORE-INSTALL.txt b/docs/BEFORE-INSTALL.txt index 567ce8c..ddb0cb9 100644 --- a/docs/BEFORE-INSTALL.txt +++ b/docs/BEFORE-INSTALL.txt @@ -3,7 +3,7 @@ ======================================================== 软件名称:ykxmz -版本:1.6.1 +版本:1.6.2 作者:小麦子 请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的 @@ -152,6 +152,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成: -------------------------------------------------------- 作者:小麦子 - 软件:ykxmz 1.6.1 + 软件:ykxmz 1.6.2 继续安装,即表示你已阅读并同意《许可协议》。 -------------------------------------------------------- diff --git a/docs/LICENSE.txt b/docs/LICENSE.txt index cd195c7..6b08412 100644 --- a/docs/LICENSE.txt +++ b/docs/LICENSE.txt @@ -3,7 +3,7 @@ ======================================================== 软件名称:ykxmz(远程管理工具) -版本:1.6.1 +版本:1.6.2 作者:小麦子 生效日期:2026 年 10 月 5 日 @@ -150,7 +150,7 @@ -------------------------------------------------------- 作者:小麦子 - 软件:ykxmz 1.6.1 + 软件:ykxmz 1.6.2 如果你不同意本协议的任何一条,请不要安装或使用本软件。 -------------------------------------------------------- diff --git a/relay/relay.py b/relay/relay.py index 19602c1..f2fadd1 100644 --- a/relay/relay.py +++ b/relay/relay.py @@ -58,7 +58,7 @@ from collections import deque from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path -VERSION = "1.6.1" +VERSION = "1.6.2" # ---------------- 参数(都能用环境变量覆盖)---------------- diff --git a/windows/lanremote/__init__.py b/windows/lanremote/__init__.py index e1cea88..f0787bb 100644 --- a/windows/lanremote/__init__.py +++ b/windows/lanremote/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- """局域网遥控 · Windows 被控端。""" -__version__ = "1.6.1" +__version__ = "1.6.2" APP_NAME = "局域网遥控 被控端" diff --git a/windows/lanremote/relay.py b/windows/lanremote/relay.py index fdeadfc..6d398c1 100644 --- a/windows/lanremote/relay.py +++ b/windows/lanremote/relay.py @@ -253,6 +253,20 @@ class RelayLink: self._log("没配置中转地址,跳过") return False + # 上次连通的地址排到最前面。 + # 设备 IP 会变(DHCP),但大概率还是上次那个; + # 排前面能省掉"挨个探到超时"的那几秒(实测 2 个候选要 10 秒, + # 而这段时间局域网发现都还没起来,手表搜不到电脑)。 + try: + last_good = str(self.store.get("relay_last_good") or "").strip() + except Exception: # noqa: BLE001 + last_good = "" + if last_good: + normalized = last_good.rstrip("/") + if normalized in candidates: + candidates.remove(normalized) + candidates.insert(0, normalized) + self.candidates = candidates # 先探一遍,挑第一个通的 chosen = self._pick_candidate(candidates) @@ -260,6 +274,14 @@ class RelayLink: if len(candidates) > 1: self._log(f"中转地址候选 {len(candidates)} 个,选用 {chosen}") + # 记下来,下次优先试它 + if chosen: + try: + if str(self.store.get("relay_last_good") or "") != chosen: + self.store.set("relay_last_good", chosen) + except Exception: # noqa: BLE001 + pass + self.device_id = self._ensure_device_id() self.code = str(self.store.pairing_code() or "") diff --git a/windows/lanremote/server.py b/windows/lanremote/server.py index 9b11f6e..4fb94cd 100644 --- a/windows/lanremote/server.py +++ b/windows/lanremote/server.py @@ -88,7 +88,10 @@ class Session: # 几十毫秒,而且地址永远不变) # 电脑在外面 -> 第一个连不上,自动切第二个(Cloudflare 隧道, # 600 毫秒左右,地址固定 relay.xxmxxs.top) -DEFAULT_RELAY_URL = "192.168.10.105:8080;https://relay.xxmxxs.top" +# 内置的中转地址:局域网优先,连不上自动切公网隧道。 +# 第一个是随身 WiFi 的固定地址(写死是因为它常在同一网段、直连最快); +# 它变了也没关系 —— 连上过的地址会被记住并排到最前面。 +DEFAULT_RELAY_URL = "192.168.2.156:8080;https://relay.xxmxxs.top" class RemoteServer: diff --git a/windows/version_info.txt b/windows/version_info.txt index 5b45442..65ad8c4 100644 --- a/windows/version_info.txt +++ b/windows/version_info.txt @@ -27,12 +27,12 @@ VSVersionInfo( [ StringStruct("CompanyName", "ykxmz"), StringStruct("FileDescription", "ykxmz"), - StringStruct("FileVersion", "1.6.1.0"), + StringStruct("FileVersion", "1.6.2.0"), StringStruct("InternalName", "ykxmz"), StringStruct("LegalCopyright", "ykxmz"), StringStruct("OriginalFilename", "ykxmz.exe"), StringStruct("ProductName", "ykxmz"), - StringStruct("ProductVersion", "1.6.1.0"), + StringStruct("ProductVersion", "1.6.2.0"), ], ) ]