mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
878b7311e9 |
@@ -59,3 +59,8 @@ desktop.ini
|
||||
|
||||
# ---- 测试收发的临时数据 ----
|
||||
/_tests/received/
|
||||
|
||||
# 令牌(绝对不要进仓库)
|
||||
/_build/.gitee_token
|
||||
*.token
|
||||
.env
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
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.
|
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 |
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,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)
|
||||
--------------------------------------------------------
|
||||
|
||||
@@ -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
@@ -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
@@ -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,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""局域网遥控 · Windows 被控端。"""
|
||||
|
||||
__version__ = "1.6.0"
|
||||
__version__ = "1.6.1"
|
||||
APP_NAME = "局域网遥控 被控端"
|
||||
|
||||
+22
-13
@@ -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}")
|
||||
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user