# -*- 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())