mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 16:00:43 +00:00
1.6.1:常规发布
- 版本号 1.6.1 -> 1.6.1 - 重建三端并打包 - 关键测试全过
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user