1.6.1:常规发布

- 版本号 1.6.1 -> 1.6.1
- 重建三端并打包
- 关键测试全过
This commit is contained in:
xmz7891
2026-10-06 23:08:07 +08:00
parent 07e96a9595
commit 65dc2fad65
81 changed files with 728 additions and 34 deletions
+5
View File
@@ -59,3 +59,8 @@ desktop.ini
# ---- 测试收发的临时数据 ----
/_tests/received/
# 令牌(绝对不要进仓库)
/_build/.gitee_token
*.token
.env
View File
View File
View File
View File
+135
View File
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
r"""诊断:发现端口 8766 到底有没有被 agent 绑上。
test_selfcheck 广播到 127.0.0.1:8766 收到 WSAECONNRESET(ICMP 端口不可达),
说明**没有进程在监听 8766**。但测试里 agent 明明起来了(8841 能连)。
查清楚是:
a) agent 绑 8766 失败(被占 / 权限)
b) agent 压根没去绑
c) 绑上了但没回包
"""
from __future__ import annotations
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")
PORT = 8851
DISCOVERY = 8766
def port_holder(port: int, udp: bool) -> list[str]:
out: list[str] = []
if udp:
cmd = ("Get-NetUDPEndpoint -LocalPort %d -ErrorAction SilentlyContinue | "
"ForEach-Object { \"$($_.LocalAddress):$($_.LocalPort) PID $($_.OwningProcess)\" }"
% port)
else:
cmd = ("Get-NetTCPConnection -LocalPort %d -ErrorAction SilentlyContinue | "
"ForEach-Object { \"$($_.LocalAddress):$($_.LocalPort) PID $($_.OwningProcess)\" }"
% port)
proc = subprocess.run(["powershell", "-NoProfile", "-Command", cmd],
capture_output=True, text=True, timeout=30)
for line in (proc.stdout or "").splitlines():
if line.strip():
out.append(line.strip())
return out
def main() -> int:
# 清干净(注意别把自己杀了 —— $PID 在 PowerShell 里是 PowerShell 的,
# 所以这里按命令行特征挑,不用 PID 比较)
subprocess.run(
["powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | "
"Where-Object { $_.CommandLine -match 'run_agent' -or "
"$_.CommandLine -match 'test_' } | "
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force "
"-ErrorAction SilentlyContinue }"],
capture_output=True, text=True, timeout=60)
time.sleep(2)
print(" 起之前,8766 有没有人占:")
holders = port_holder(DISCOVERY, udp=True)
print(f" {holders if holders else '空闲'}")
data_dir = ROOT / "windows" / "data"
if data_dir.exists():
shutil.rmtree(data_dir, ignore_errors=True)
env = dict(os.environ)
env["PYTHONIOENCODING"] = "utf-8"
print()
print(f" 起 agent(TCP {PORT})…")
proc = subprocess.Popen(
[sys.executable, "run_agent.py", "--port", str(PORT),
"--new-code", "--add-device", "10"],
cwd=str(ROOT / "windows"), env=env,
stdout=open(ROOT / "_build" / "agent_diag.log", "wb"),
stderr=subprocess.STDOUT)
try:
# 等 TCP 就绪
ready = False
for _ in range(50):
try:
s = socket.create_connection(("127.0.0.1", PORT), timeout=1)
s.close()
ready = True
break
except OSError:
time.sleep(0.4)
print(f" TCP {PORT} 就绪: {ready}")
time.sleep(1.5)
print()
print(f" 起之后,8766 有没有人占:")
holders = port_holder(DISCOVERY, udp=True)
print(f" {holders if holders else '还是空闲 —— 说明 agent 没绑上!'}")
print()
print(" 直接探测 8766:")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3)
import json
request = json.dumps({"lanremote": "LANREMOTE_SELFCHECK",
"proto": 1}).encode("utf-8")
try:
sock.sendto(request, ("127.0.0.1", DISCOVERY))
data, addr = sock.recvfrom(2048)
print(f" ✓ 收到应答: {data[:160]!r}")
except ConnectionResetError as exc:
print(f" ✗ WSAECONNRESET —— 端口不可达,没人监听({exc})")
except socket.timeout:
print(" ✗ 超时 —— 有人监听但没回")
finally:
sock.close()
print()
print(" agent 日志尾部:")
log = ROOT / "_build" / "agent_diag.log"
if log.is_file():
text = log.read_text(encoding="utf-8", errors="replace")
for line in text.splitlines()[-14:]:
print(f" {line}")
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
return 0
if __name__ == "__main__":
sys.exit(main())
+114
View File
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
r"""修:中转连接尝试阻塞了局域网发现。
现象(agent 日志):
[20:31:17] 没配过中转地址,用内置默认值:192.168.10.105:8080;https://...
[20:31:25] http://192.168.10.105:8080 不通,试下一个 <- 等了 8 秒
[20:31:27] https://relay.xxmxxs.top 不通,试下一个 <- 又 2 秒
[20:31:27] 自动发现已开启(UDP 8766) <- 10 秒后才绑上
server.start() 原来的顺序:
1. 绑 TCP
2. enable_relay() <- 挨个试中转地址,每个都要等超时(这里 10 秒)
3. 起 accept 线程
4. 起 DiscoveryResponder
问题:**局域网发现被中转拖住了**。
电脑和手表在同一个 WiFi 时,本来应该"广播一下就找到",
结果因为中转连不上,要等 10 几秒手表才搜得到电脑。
中转地址越烂、候选越多,等得越久。
影响:
* 用户体验上表现为"刚开电脑时手表搜不到,过一会儿才行"
* 自动化测试也栽在这(广播时发现服务还没起)
改法:把局域网发现**提到 enable_relay() 之前**。
局域网是本机能力,不该依赖任何网络外部条件。
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
SERVER = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows"
r"\lanremote\server.py"))
OLD = ''' # 配了中转地址就同时启用跨网络通道(和局域网监听并存,不冲突)
try:
self.enable_relay()
except Exception as exc: # noqa: BLE001
self._log(f"启用中转失败:{exc}")
self._accept_thread = threading.Thread(target=self._accept_loop,
name="accept", daemon=True)
self._accept_thread.start()
self._clip_thread = threading.Thread(target=self._clip_loop,
name="clip", daemon=True)
self._clip_thread.start()
# 启动局域网自动发现,让手表不用输 IP
self._discovery = DiscoveryResponder(self.store, self.port)
# 手表点「连不上?」会广播请求,这里就把网络自检窗口弹出来
self._discovery.on_selfcheck = self._on_selfcheck_request
if self._discovery.start():
self._log(f"自动发现已开启(UDP {DISCOVERY_PORT})")
else:
self._log(f"自动发现未开启:{self._discovery.last_error}")'''
NEW = ''' # ── 先起局域网发现 ──
#
# 顺序很重要:enable_relay() 会挨个试中转地址,
# 每个候选都要等到超时才算"不通",地址越烂等得越久(实测 10 秒+)。
# 原来这一步排在发现服务前面,导致:
# 电脑刚开机的十几秒里,手表广播搜不到它。
# 局域网发现是纯本机能力,不该被"连不连得上外网"拖住,
# 所以挪到前面,开机即可被搜到。
self._discovery = DiscoveryResponder(self.store, self.port)
# 手表点「连不上?」会广播请求,这里就把网络自检结论算出来
self._discovery.on_selfcheck = self._on_selfcheck_request
if self._discovery.start():
self._log(f"自动发现已开启(UDP {DISCOVERY_PORT})")
else:
self._log(f"自动发现未开启:{self._discovery.last_error}")
# ── 再起收发线程 ──
self._accept_thread = threading.Thread(target=self._accept_loop,
name="accept", daemon=True)
self._accept_thread.start()
self._clip_thread = threading.Thread(target=self._clip_loop,
name="clip", daemon=True)
self._clip_thread.start()
# ── 最后才连中转(会阻塞,所以放最后)──
# 配了中转地址就同时启用跨网络通道(和局域网监听并存,不冲突)
try:
self.enable_relay()
except Exception as exc: # noqa: BLE001
self._log(f"启用中转失败:{exc}")'''
def main() -> int:
text = SERVER.read_text(encoding="utf-8")
if "先起局域网发现" in text:
print(" 已经改过")
return 0
if OLD not in text:
print(" [失败] 锚点没找到")
return 1
text = text.replace(OLD, NEW, 1)
SERVER.write_text(text, encoding="utf-8", newline="\n")
print(" 已把局域网发现提到 enable_relay() 之前")
try:
ast.parse(text)
print(" server.py 语法 OK")
except SyntaxError as exc:
print(f" 语法错误: 行 {exc.lineno}: {exc.msg}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+84
View File
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
r"""改 Gitee 仓库的简介 / 主页,并核对发行版。
用法(令牌只走命令行,不落盘):
python _build/gitee_meta.py <令牌>
"""
from __future__ import annotations
import json
import sys
import urllib.error
import urllib.request
OWNER, REPO = "xmz7891", "ykxmz"
API = f"https://gitee.com/api/v5/repos/{OWNER}/{REPO}"
DESCRIPTION = (
"教室电脑手表遥控 · 老师用手表管教室电脑:放课件、投图、"
"屏蔽游戏网站、下课关机。支持局域网直连与跨网络中继"
)
HOMEPAGE = "https://relay.xxmxxs.top"
def call(url, payload=None, method="GET", timeout=60):
data = None
headers = {"User-Agent": "ykxmz-meta"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json;charset=UTF-8"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8", "replace")
return resp.status, (json.loads(body) if body.strip() else {})
def main() -> int:
if len(sys.argv) < 2:
print(" 用法: python _build/gitee_meta.py <令牌>")
return 1
token = sys.argv[1]
print("===== 1. 改仓库简介 =====")
try:
status, reply = call(
API,
{"access_token": token, "name": REPO,
"description": DESCRIPTION, "homepage": HOMEPAGE},
method="PATCH")
if status in (200, 201):
print(f" ✓ 简介已更新")
print(f" {reply.get('description', '')}")
print(f" 主页: {reply.get('homepage', '')}")
else:
print(f" ✗ HTTP {status}: {reply}")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")[:300]
print(f" ✗ HTTP {exc.code}: {detail}")
return 1
print()
print("===== 2. 核对发行版 =====")
try:
_s, releases = call(f"{API}/releases?access_token={token}&per_page=5")
if not isinstance(releases, list):
print(f" 读不到发行版列表: {releases}")
for rel in releases[:3]:
print(f" {rel.get('tag_name')} {rel.get('name')}")
for asset in rel.get("assets", []):
size = asset.get("size", 0) / 1024 / 1024
print(f" {asset.get('browser_download_url', '')}"
.replace("https://gitee.com", ""))
print(f" 大小 {size:.2f} MB "
f"下载次数 {asset.get('download_count', 0)}")
except Exception as exc: # noqa: BLE001
print(f" 查发行版失败: {exc}")
print()
print(f" 仓库: https://gitee.com/{OWNER}/{REPO}")
print(f" 发行版: https://gitee.com/{OWNER}/{REPO}/releases")
return 0
if __name__ == "__main__":
sys.exit(main())
+181
View File
@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
r"""在 Gitee 上创建发行版(Release)并上传三个文件。
用法:
python _build/gitee_release.py <令牌> [版本号]
令牌只从命令行参数读,**不写进任何文件、不进仓库**。
Gitee 的两个接口:
建发行版 POST /api/v5/repos/{owner}/{repo}/releases
传附件 POST /api/v5/repos/{owner}/{repo}/releases/{id}/attach_files
SSH 密钥只能推代码,建发行版必须用这个令牌。
"""
from __future__ import annotations
import json
import mimetypes
import sys
import urllib.error
import urllib.request
import uuid
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
OWNER = "xmz7891"
REPO = "ykxmz"
API = f"https://gitee.com/api/v5/repos/{OWNER}/{REPO}"
def api(url: str, payload: dict | None = None, method: str = "GET",
timeout: int = 60):
data = None
headers = {"User-Agent": "ykxmz-release"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json;charset=UTF-8"
request = urllib.request.Request(url, data=data, headers=headers,
method=method)
with urllib.request.urlopen(request, timeout=timeout) as response:
body = response.read().decode("utf-8", "replace")
return response.status, (json.loads(body) if body.strip() else {})
def upload_file(url: str, path: Path, token: str, timeout: int = 600):
"""multipart 上传单个文件。"""
boundary = "----ykxmz" + uuid.uuid4().hex
content_type = (mimetypes.guess_type(path.name)[0]
or "application/octet-stream")
head = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="access_token"\r\n\r\n'
f"{token}\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="file"; '
f'filename="{path.name}"\r\n'
f"Content-Type: {content_type}\r\n\r\n"
).encode("utf-8")
tail = f"\r\n--{boundary}--\r\n".encode("utf-8")
body = head + path.read_bytes() + tail
request = urllib.request.Request(
url, data=body, method="POST",
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(body)),
"User-Agent": "ykxmz-release",
})
with urllib.request.urlopen(request, timeout=timeout) as response:
text = response.read().decode("utf-8", "replace")
return response.status, (json.loads(text) if text.strip() else {})
def main() -> int:
if len(sys.argv) < 2:
print(" 用法: python _build/gitee_release.py <令牌> [版本号]")
return 1
token = sys.argv[1]
version = sys.argv[2] if len(sys.argv) > 2 else "1.6.0"
tag = f"v{version}"
files = [
ROOT / "dist" / f"ykxmz-agent-{version}.zip",
ROOT / "dist" / f"ykxmz-server-{version}.zip",
ROOT / "dist" / f"ykxmz-watch-{version}.apk",
]
for path in files:
if not path.is_file():
print(f" [失败] 找不到 {path}")
return 1
print(f" 仓库: {OWNER}/{REPO}")
print(f" 标签: {tag}")
print()
# ---- 1. 建发行版 ----
body = f"""## ykxmz {version}
教室电脑手表遥控。
老师抬一下手腕就能管教室里那台电脑 —— 放课件、放音频、投图、
看看谁在打游戏、下课关机,不用走回讲台。
### 这次下载哪个
| 你是 | 下这个 |
|---|---|
| 教室电脑装被控端 | `ykxmz-agent-{version}.zip` |
| 要跨网络(办公室管教室) | `ykxmz-server-{version}.zip` |
| 手表装控制端 | `ykxmz-watch-{version}.apk` |
只在一个教室用的话,第三件(中转服务器)可以先不装,
手表和电脑连同一个 WiFi 就能直连。
### 怎么用
装完看包里的 `README.txt`,或者仓库首页的说明。
### 这一版改了什么
- 重写说明文档,按老师 / 电教 / 开发者 / 普通用户分开讲
- 手表端内置根证书,修部分手表连中转时的证书校验失败
- 中转站加下载区
- 网络稳定性:修 MTU 问题,公网可用率从 48% 提到 95% 以上
"""
print(" 建发行版…")
try:
status, reply = api(
f"{API}/releases",
{"access_token": token, "tag_name": tag, "name": f"ykxmz {version}",
"body": body, "target_commitish": "main", "prerelease": False},
method="POST")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")[:400]
print(f" [失败] HTTP {exc.code}: {detail}")
return 1
if status not in (200, 201):
print(f" [失败] HTTP {status}: {reply}")
return 1
release_id = reply.get("id")
print(f" ✓ 发行版已建,id={release_id}")
print(f" {reply.get('html_url', '')}")
# 如果标签已经存在(重复发布),走"找已有发行版再传附件"的路
if not release_id:
print(" [提示] 没拿到 id,尝试按标签查已有的发行版…")
try:
_s, existing = api(f"{API}/releases/tags/{tag}?access_token={token}")
release_id = existing.get("id")
print(f" 已有发行版 id={release_id}")
except Exception as exc: # noqa: BLE001
print(f" [失败] 查不到: {exc}")
return 1
# ---- 2. 传附件 ----
print()
print(" 上传文件:")
attach = f"{API}/releases/{release_id}/attach_files"
for path in files:
size_mb = path.stat().st_size / 1024 / 1024
try:
status, reply = upload_file(attach, path, token)
if status in (200, 201):
print(f" ✓ {path.name} ({size_mb:.2f} MB)")
else:
print(f" ✗ {path.name}: HTTP {status} {reply}")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")[:200]
print(f" ✗ {path.name}: HTTP {exc.code} {detail}")
except Exception as exc: # noqa: BLE001
print(f" ✗ {path.name}: {exc}")
print()
print(f" 完成:https://gitee.com/{OWNER}/{REPO}/releases/tag/{tag}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -17,7 +17,7 @@ import zipfile
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
VERSION = "1.6.0"
VERSION = "1.6.1"
NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
+167
View File
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
r"""一条命令完成发布。
用法:
python _build/release.py 1.7.0 # 升版本 + 构建 + 打包 + 推送 + 建发行版
python _build/release.py 1.7.0 --no-readme # 明说这版没改 README
它干这些事:
1. 升版本号(三端 + 文档 + 测试断言)
2. 重建 EXE 和 APK
3. 打三个成品
4. 跑一遍关键测试
5. git 提交 + 推送
6. 在 Gitee 建发行版,把三个文件传上去
7. 覆盖仓库简介(只在第一次需要,重复设成一样也无害)
令牌从 _build/.gitee_token 读(那个文件在 .gitignore 里,不会进仓库)。
第一次用:把令牌写进去就行。
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
TOKEN_FILE = ROOT / "_build" / ".gitee_token"
GIT = r"C:\Program Files\Git\bin\git.exe"
GIT_SSH = r"C:\Program Files\Git\usr\bin\ssh.exe"
def sh(args, cwd=ROOT, check=False, timeout=1800, env=None):
merged = dict(os.environ)
if env:
merged.update(env)
proc = subprocess.run(args, cwd=cwd, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout,
env=merged)
if check and proc.returncode != 0:
raise RuntimeError(f"{args[0]} 失败:{proc.stdout}\n{proc.stderr}")
return proc
def step(n, title):
print()
print(f" {'=' * 56}")
print(f" {n}. {title}")
print(f" {'=' * 56}")
def read_current_version() -> str:
text = (ROOT / "_build" / "package_remote.py").read_text(encoding="utf-8")
for line in text.splitlines():
if line.startswith("VERSION = "):
return line.split('"')[1]
return "0.0.0"
def main() -> int:
if len(sys.argv) < 2:
print(" 用法: python _build/release.py <新版本号>")
print(f" 当前版本: {read_current_version()}")
return 1
new_version = sys.argv[1]
old_version = read_current_version()
note_readme = "--no-readme" in sys.argv
if not TOKEN_FILE.is_file():
print(f" [失败] 找不到令牌文件 {TOKEN_FILE}")
print(" 把 Gitee 令牌写进那个文件(一行,别加引号)")
return 1
token = TOKEN_FILE.read_text(encoding="utf-8").strip()
if len(token) < 20:
print(f" [失败] 令牌看起来不对(长度 {len(token)})")
return 1
print(f" 发布 {old_version} -> {new_version}")
print(f" README: {'这次改了' if note_readme else '没改'}")
# ---- 1. 升版本 ----
step(1, "升版本号")
proc = sh([sys.executable, "_build/bump_version_generic.py",
old_version, new_version])
print(proc.stdout[-1200:])
if proc.returncode != 0:
print(proc.stderr[-500:])
return 1
# ---- 2. 重建 ----
step(2, "重建 EXE 和 APK")
sh([GIT, "--version"]) # 探活,无关紧要
for args in (["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", "android\\build_apk.ps1"],
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", "windows\\build_exe.ps1"]):
proc = sh(args, timeout=2400)
tail = (proc.stdout or "").strip().splitlines()[-2:]
for line in tail:
print(f" {line}")
if proc.returncode != 0:
print(f" [失败] {args[-1]}")
print((proc.stderr or "")[-600:])
return 1
# ---- 3. 打包 ----
step(3, "打三个成品")
proc = sh([sys.executable, "_build/package_remote.py"])
print(proc.stdout[-1400:])
if proc.returncode != 0:
return 1
# ---- 4. 关键测试 ----
step(4, "跑关键测试")
for test in ("audit_encoding.py", "test_net_tune.py", "test_chunked_body.py",
"test_admin_js.py"):
proc = sh([sys.executable, "-u", f"_tests/{test}"], timeout=900)
# 用退出码判断 —— 各测试的输出格式不一样
# (有的打"失败 0 项",有的打"结论:JS 合法",字符串匹配会误判)
bad = proc.returncode != 0
print(f" {test:28} {'✗' if bad else '✓'}")
if bad:
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:
return 1
# ---- 5. 提交推送 ----
step(5, "提交并推送")
env = {"GIT_SSH_COMMAND": f'"{GIT_SSH}"'}
sh([GIT, "add", "-A"])
msg = (f"{new_version}:{'README 与' if note_readme else ''}常规发布\n\n"
f"- 版本号 {old_version} -> {new_version}\n"
f"- 重建三端并打包\n"
f"- 关键测试全过\n")
sh([GIT, "commit", "-q", "-m", msg])
proc = sh([GIT, "push", "origin", "main"], env=env, timeout=1800)
print(f" {proc.stdout.strip()[-300:]}")
if proc.returncode != 0:
print(proc.stderr[-600:])
return 1
# ---- 6. 发行版 ----
step(6, "建 Gitee 发行版")
proc = sh([sys.executable, "_build/gitee_release.py", token, new_version],
timeout=2400)
print(proc.stdout[-1200:])
if proc.returncode != 0:
print(proc.stderr[-600:])
return 1
print()
print(f" ✓ {new_version} 发布完成")
print(f" https://gitee.com/xmz7891/ykxmz/releases/tag/v{new_version}")
return 0
if __name__ == "__main__":
sys.exit(main())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 608 KiB

+1 -1
View File
@@ -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.0.apk"
ROOT / "dist" / "ykxmz-watch-1.6.1.apk"
APK = _latest_apk()
+4 -4
View File
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
# 被控端 -> 一个 zip
# 服务器端 -> 一个 zip
# 控制端 -> 直接给 APK(不打 zip)
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-1.6.0.zip"
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-1.6.0.zip"
WATCH_APK = ROOT / "dist" / "ykxmz-watch-1.6.0.apk"
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"
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.0-说明.txt")):
ROOT / "dist" / "ykxmz-watch-1.6.1-说明.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])
-1
View File
@@ -402,7 +402,6 @@ New-Item -ItemType Directory -Force -Path $out | Out-Null
(Join-Path $root "android\src\com\lanremote\watch\DiscoveryCore.java") `
(Join-Path $root "android\src\com\lanremote\watch\Transport.java") `
(Join-Path $root "android\src\com\lanremote\watch\RelayTransport.java") `
# TlsCompat:内置根证书,修老手表连中转时的 SSLHandshakeException
(Join-Path $root "android\src\com\lanremote\watch\TlsCompat.java") `
(Join-Path $root "android\src\com\lanremote\watch\RemoteClient.java") `
(Join-Path $root "_tests\java\JavaClientTest.java") 2>&1 | Out-Null
+1 -1
View File
@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lanremote.watch"
android:versionCode="5"
android:versionName="1.6.0">
android:versionName="1.6.1">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+2 -2
View File
@@ -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.0.apk"
$apkName = "ykxmz-watch-1.6.1.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.0" `
--version-name "1.6.1" `
--auto-add-overlay
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
+3 -3
View File
@@ -3,7 +3,7 @@
========================================================
软件名称:ykxmz
版本:1.6.0
版本:1.6.1
作者:小麦子
装好之后看这一页:怎么确认它在跑、怎么改设置、
@@ -197,7 +197,7 @@
八、版本记录
--------------------------------------------------------
1.6.0
1.6.1
* 跨网络支持:Cloudflare 具名隧道,固定域名
* 中转服务器:单设备单配对码、管理页可删设备
* 手表端:自动判断局域网/中转并无缝切换
@@ -209,6 +209,6 @@
--------------------------------------------------------
作者:小麦子
软件:ykxmz 1.6.0
软件:ykxmz 1.6.1
许可条款见《许可协议》(LICENSE.txt)
--------------------------------------------------------
+2 -2
View File
@@ -3,7 +3,7 @@
========================================================
软件名称:ykxmz
版本:1.6.0
版本:1.6.1
作者:小麦子
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
@@ -152,6 +152,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
--------------------------------------------------------
作者:小麦子
软件:ykxmz 1.6.0
软件:ykxmz 1.6.1
继续安装,即表示你已阅读并同意《许可协议》。
--------------------------------------------------------
+2 -2
View File
@@ -3,7 +3,7 @@
========================================================
软件名称:ykxmz(远程管理工具)
版本:1.6.0
版本:1.6.1
作者:小麦子
生效日期:2026 年 10 月 5 日
@@ -150,7 +150,7 @@
--------------------------------------------------------
作者:小麦子
软件:ykxmz 1.6.0
软件:ykxmz 1.6.1
如果你不同意本协议的任何一条,请不要安装或使用本软件。
--------------------------------------------------------
+1 -1
View File
@@ -58,7 +58,7 @@ from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
VERSION = "1.6.0"
VERSION = "1.6.1"
# ---------------- 参数(都能用环境变量覆盖)----------------
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""局域网遥控 · Windows 被控端。"""
__version__ = "1.6.0"
__version__ = "1.6.1"
APP_NAME = "局域网遥控 被控端"
+22 -13
View File
@@ -149,12 +149,23 @@ class RemoteServer:
self._sock = sock
self._running = True
# 配了中转地址就同时启用跨网络通道(和局域网监听并存,不冲突)
try:
self.enable_relay()
except Exception as exc: # noqa: BLE001
self._log(f"启用中转失败:{exc}")
# ── 先起局域网发现 ──
#
# 顺序很重要:enable_relay() 会挨个试中转地址,
# 每个候选都要等到超时才算"不通",地址越烂等得越久(实测 10 秒+)。
# 原来这一步排在发现服务前面,导致:
# 电脑刚开机的十几秒里,手表广播搜不到它。
# 局域网发现是纯本机能力,不该被"连不连得上外网"拖住,
# 所以挪到前面,开机即可被搜到。
self._discovery = DiscoveryResponder(self.store, self.port)
# 手表点「连不上?」会广播请求,这里就把网络自检结论算出来
self._discovery.on_selfcheck = self._on_selfcheck_request
if self._discovery.start():
self._log(f"自动发现已开启(UDP {DISCOVERY_PORT})")
else:
self._log(f"自动发现未开启:{self._discovery.last_error}")
# ── 再起收发线程 ──
self._accept_thread = threading.Thread(target=self._accept_loop,
name="accept", daemon=True)
self._accept_thread.start()
@@ -162,14 +173,12 @@ class RemoteServer:
name="clip", daemon=True)
self._clip_thread.start()
# 启动局域网自动发现,让手表不用输 IP
self._discovery = DiscoveryResponder(self.store, self.port)
# 手表点「连不上?」会广播请求,这里就把网络自检窗口弹出来
self._discovery.on_selfcheck = self._on_selfcheck_request
if self._discovery.start():
self._log(f"自动发现已开启(UDP {DISCOVERY_PORT})")
else:
self._log(f"自动发现未开启:{self._discovery.last_error}")
# ── 最后才连中转(会阻塞,所以放最后)──
# 配了中转地址就同时启用跨网络通道(和局域网监听并存,不冲突)
try:
self.enable_relay()
except Exception as exc: # noqa: BLE001
self._log(f"启用中转失败:{exc}")
self._log(f"服务已启动,监听 0.0.0.0:{self.port}")
+2 -2
View File
@@ -27,12 +27,12 @@ VSVersionInfo(
[
StringStruct("CompanyName", "ykxmz"),
StringStruct("FileDescription", "ykxmz"),
StringStruct("FileVersion", "1.6.0.0"),
StringStruct("FileVersion", "1.6.1.0"),
StringStruct("InternalName", "ykxmz"),
StringStruct("LegalCopyright", "ykxmz"),
StringStruct("OriginalFilename", "ykxmz.exe"),
StringStruct("ProductName", "ykxmz"),
StringStruct("ProductVersion", "1.6.0.0"),
StringStruct("ProductVersion", "1.6.1.0"),
],
)
]