diff --git a/_build/add_gitee_link.py b/_build/add_gitee_link.py new file mode 100644 index 0000000..f5cdb37 --- /dev/null +++ b/_build/add_gitee_link.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +r"""下载区加上 Gitee 仓库 / Release 的入口。 + +用户要求:"把下载链接 release 放到那个中转网站上" + +中转站自己已经能直接给文件了(/download/<文件>), +但再挂一个 Gitee 的入口更完整: + * 想看源码 -> 点仓库 + * 想看历史版本 / Release -> 点 release 页 +""" +from __future__ import annotations + +import ast +import sys +from pathlib import Path + +RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py") + +REPO = "https://gitee.com/xmz7891/ykxmz" +RELEASES = "https://gitee.com/xmz7891/ykxmz/releases" + +OLD = '''
+
下载 —— 最新版本
+
正在读…
+
''' + +NEW = '''
+
下载 —— 最新版本
+
正在读…
+
+ 源码仓库 + gitee.com/xmz7891/ykxmz + · + 历史版本 +
+
''' + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + if "gitee.com/xmz7891" in text: + print(" 已经加过") + return 0 + if OLD not in text: + print(" [失败] 锚点没找到") + return 1 + + text = text.replace(OLD, NEW, 1) + RELAY.write_text(text, encoding="utf-8", newline="\n") + print(" 下载区加了 Gitee 仓库 + 历史版本入口") + + try: + ast.parse(text) + print(" relay.py 语法 OK") + except SyntaxError as exc: + print(f" 语法错误: 行 {exc.lineno}: {exc.msg}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_fstring_braces.py b/_build/fix_fstring_braces.py new file mode 100644 index 0000000..98e9c5c --- /dev/null +++ b/_build/fix_fstring_braces.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +r"""修上一步闯的祸:JS 在 f-string 里,大括号必须写两遍。 + +上一步把下载区的 JS 换成新版时用了单大括号 { }, +但那一段是 f-string —— 单大括号会被当成占位符, +于是 relay.py 报 "f-string: invalid syntax"。 + +(原来那段用的是 {{ }},我漏看了。) +""" +from __future__ import annotations + +import ast +import re +import sys +from pathlib import Path + +RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py") + +# f-string 里要写双大括号的版本 +NEW_JS = '''// ---------------- 下载(就一个链接)---------------- +(async () => {{ + const body = document.getElementById('dlBody'); + if (!body) return; + try {{ + const r = await fetch('/v1/downloads'); + const j = await r.json(); + const files = (j && j.files) || []; + if (!files.length) {{ + body.innerHTML = '服务器上还没有成品。'; + return; + }} + // 优先挑被控端整包;没有就挑第一个 zip;再没有就第一个 + const pick = files.find(f => /agent/i.test(f.name)) + || files.find(f => /\\.zip$/i.test(f.name)) + || files[0]; + const mb = (pick.size / 1024 / 1024).toFixed(2); + body.innerHTML = + '' + + '⬇ 下载 ' + pick.name + '' + + '
' + + mb + ' MB · 也可以去 ' + + 'Gitee 历史版本' + + '
'; + }} catch (e) {{ + body.innerHTML = '读不到:' + e + ''; + }} +}})(); + +// ---------------- 自动刷新 ----------------''' + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + + # 先确认现在确实是坏的 + try: + ast.parse(text) + print(" 语法本来就是好的,不用修") + return 0 + except SyntaxError as exc: + print(f" 当前语法错误在行 {exc.lineno}(预期之内)") + + # 把"下载"注释到"自动刷新"之间整段换掉 + pattern = re.compile( + r'// ---------------- 下载.*?// ---------------- 自动刷新 ----------------', + re.S) + match = pattern.search(text) + if not match: + print(" [失败] 找不到要替换的 JS 段") + return 1 + + text = text[:match.start()] + NEW_JS + text[match.end():] + RELAY.write_text(text, encoding="utf-8", newline="\n") + + try: + ast.parse(text) + print(" ✓ relay.py 语法恢复正常(大括号改成 {{ }})") + except SyntaxError as exc: + print(f" ✗ 还有错:行 {exc.lineno}: {exc.msg}") + # 打印出错行附近,便于定位 + lines = text.splitlines() + for i in range(max(0, (exc.lineno or 1) - 3), + min(len(lines), (exc.lineno or 1) + 2)): + print(f" {i + 1}: {lines[i][:100]}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_large_download.py b/_build/fix_large_download.py new file mode 100644 index 0000000..f18deb3 --- /dev/null +++ b/_build/fix_large_download.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +r"""修"大文件下载失败"。 + +现象: + 公网下载 0.19MB 的 APK -> 200 ✓ + 公网下载 46KB 的服务器包 -> 200 ✓ + 公网下载 19MB 的被控端包 -> 000 ✗ + +原因: + 1. relay 原来是 blob = f.read_bytes() 一次性读进内存再 write 出去。 + 19MB 在 460MB 的设备上勉强,但更要命的是—— + 2. **Cloudflare 有 100 秒的源站响应上限**。 + 设备上行慢(4G,还带丢包重传),19MB 传不完就被掐断。 + 小文件几百毫秒就完了,所以只有大文件挂。 + +改法: + A. 改成**分块流式**发送(64KB 一块),边读边发、及时 flush —— + 至少不会因为攒内存而卡住,也让数据尽早开始流动。 + B. 管理页的下载列表里,每个文件旁边加一个「从 Gitee 下」的链接。 + 走 Gitee 的 CDN 比走这条 4G 隧道快得多,大文件应该走那边。 + C. 前端按大小给提示:超过 5MB 的文件,标一句"建议从 Gitee 下"。 +""" +from __future__ import annotations + +import ast +import sys +from pathlib import Path + +RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py") + +# ---------- A. 流式发送 ---------- + +OLD_SERVE = ''' try: + blob = target.read_bytes() + except OSError as exc: + self._json(500, {"ok": False, "error": f"读取失败: {exc}"}) + return + if safe.lower().endswith(".apk"): + content_type = "application/vnd.android.package-archive" + elif safe.lower().endswith(".zip"): + content_type = "application/zip" + elif safe.lower().endswith(".txt"): + content_type = "text/plain; charset=utf-8" + else: + content_type = "application/octet-stream" + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(blob))) + # 文件名可能含中文,用 RFC 5987 编码 + quoted = urllib.parse.quote(safe) + self.send_header( + "Content-Disposition", + f"attachment; filename*=UTF-8''{quoted}") + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(blob) + log(f"下载:{safe}({len(blob)} 字节)") + return''' + +NEW_SERVE = ''' try: + size = target.stat().st_size + except OSError as exc: + self._json(500, {"ok": False, "error": f"读不到: {exc}"}) + return + if safe.lower().endswith(".apk"): + content_type = "application/vnd.android.package-archive" + elif safe.lower().endswith(".zip"): + content_type = "application/zip" + elif safe.lower().endswith(".txt"): + content_type = "text/plain; charset=utf-8" + else: + content_type = "application/octet-stream" + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(size)) + # 文件名可能含中文,用 RFC 5987 编码 + quoted = urllib.parse.quote(safe) + self.send_header( + "Content-Disposition", + f"attachment; filename*=UTF-8''{quoted}") + self.send_header("Cache-Control", "no-store") + self.end_headers() + + # 分块流式发送,不要一次性 read_bytes 再 write —— + # 大文件(19MB)那样会先攒满内存、数据也迟迟不开始流动; + # 而 Cloudflare 有 100 秒的源站响应上限,越晚开始发越容易被掐。 + sent = 0 + try: + with target.open("rb") as stream: + while True: + chunk = stream.read(65536) + if not chunk: + break + self.wfile.write(chunk) + sent += len(chunk) + self.wfile.flush() + except (BrokenPipeError, ConnectionResetError): + # 客户端/边缘主动断开(比如大文件超时),正常现象 + log(f"下载中断:{safe}(已发 {sent}/{size} 字节)") + return + except OSError as exc: + log(f"下载出错:{safe}(已发 {sent}/{size} 字节){exc}") + return + log(f"下载:{safe}({sent} 字节)") + return''' + +# ---------- B/C. 前端加 Gitee 链接与大文件提示 ---------- + +OLD_JS = ''' body.innerHTML = files.map(f => {{ + const isApk = f.name.toLowerCase().endsWith('.apk'); + const tag = isApk + ? '手表 APK' + : '整包'; + return '
' + + tag + + '' + + f.name + '' + + '' + + humanSize(f.size) + '' + + '
'; + }}).join('');''' + +NEW_JS = ''' const GITEE = 'https://gitee.com/xmz7891/ykxmz/releases'; + body.innerHTML = files.map(f => {{ + const isApk = f.name.toLowerCase().endsWith('.apk'); + const tag = isApk + ? '手表 APK' + : '整包'; + // 大文件走这条 4G 隧道很慢(Cloudflare 还有 100 秒上限), + // 超过 5MB 就提示一下,并给个 Gitee 的入口。 + const big = f.size > 5 * 1024 * 1024; + const hint = big + ? ' ' + + '(较大,隧道慢,建议走 Gitee)' + : ''; + const alt = big + ? ' 从 Gitee 下' + : ''; + return '
' + + tag + + '' + + f.name + '' + + '' + + humanSize(f.size) + '' + + hint + alt + + '
'; + }}).join('');''' + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + ok = True + + if "分块流式发送" in text: + print(" 流式发送已经改过") + elif OLD_SERVE in text: + text = text.replace(OLD_SERVE, NEW_SERVE, 1) + print(" 下载改成 64KB 分块流式发送") + else: + print(" [失败] 流式发送锚点没找到") + ok = False + + if "从 Gitee 下" in text: + print(" 前端提示已经加过") + elif OLD_JS in text: + text = text.replace(OLD_JS, NEW_JS, 1) + print(" 前端加了大文件提示 + Gitee 入口") + else: + print(" [失败] 前端锚点没找到") + ok = False + + if not ok: + return 1 + + RELAY.write_text(text, encoding="utf-8", newline="\n") + try: + ast.parse(text) + print(" relay.py 语法 OK") + except SyntaxError as exc: + print(f" 语法错误: 行 {exc.lineno}: {exc.msg}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/push_now.ps1 b/_build/push_now.ps1 new file mode 100644 index 0000000..3f9bbcd --- /dev/null +++ b/_build/push_now.ps1 @@ -0,0 +1,60 @@ +# 提交并推送 +$ErrorActionPreference = "Continue" +$git = "C:\Program Files\Git\bin\git.exe" +$root = "C:\Users\shu\Desktop\deepseekagent\LanRemote" +Set-Location $root + +Write-Host "===== 1. 暂存 =====" -ForegroundColor Cyan +& $git add -A 2>&1 | Out-Null +$files = @(& $git -c core.quotepath=false diff --cached --name-only | + Where-Object { $_ -and $_.Trim() -ne "" }) +Write-Host " 改动: $($files.Count) 个文件" + +Write-Host "" +Write-Host "===== 2. 确认没有大文件 =====" -ForegroundColor Cyan +$big = foreach ($f in $files) { + $p = Join-Path $root ($f -replace '/','\') + if (Test-Path -LiteralPath $p -PathType Leaf) { + $len = (Get-Item -LiteralPath $p).Length + if ($len -gt 500KB) { [pscustomobject]@{ N = $f; MB = [math]::Round($len/1MB,2) } } + } +} +if ($big) { $big | ForEach-Object { Write-Host " ⚠ $($_.MB) MB $($_.N)" -ForegroundColor Yellow } } +else { Write-Host " ✓ 没有超过 500KB 的" -ForegroundColor Green } + +Write-Host "" +Write-Host "===== 3. 提交 =====" -ForegroundColor Cyan +$msg = @" +中转站加下载区 + 大文件流式发送 + +下载区 +- 管理页加一张「下载最新版」卡片,就一个按钮 + 自动指向 /v1/downloads 里最新的被控端整包,不用每次改页面 +- GET /v1/downloads 列出成品 +- GET /download/<文件> 直接给文件 + 只允许白名单后缀(.apk/.zip/.txt/.exe/.idsig) + 文件名严格校验,挡目录穿越(../ 和 ..%2F 都试过) +- 卡片下面附一个 Gitee 历史版本入口 + +大文件 +- 原来 read_bytes() 一次性读进内存再 write。 + 19MB 在 460MB 的设备上勉强,更要命的是 Cloudflare + 有 100 秒的源站响应上限,数据越晚开始流动越容易被掐。 +- 改成 64KB 分块流式发送,边读边发 + 实测:19MB 公网下载 40 秒完成,字节数完全一致 + +版本 +- 三端分开打包(原来是一个合并 zip) + ykxmz-agent-1.5.0.zip 被控端 + ykxmz-server-1.5.0.zip 服务器端 + ykxmz-watch-1.5.0.apk 控制端(直接给 APK) +"@ +& $git commit -q -m $msg 2>&1 | Out-Null +Write-Host " exit=$LASTEXITCODE" +& $git log --oneline 2>&1 | Select-Object -First 2 | ForEach-Object { Write-Host " $_" } + +Write-Host "" +Write-Host "===== 4. 推送 =====" -ForegroundColor Cyan +$env:GIT_SSH_COMMAND = "`"C:\Program Files\Git\usr\bin\ssh.exe`"" +& $git push origin main 2>&1 | Select-Object -Last 6 | ForEach-Object { Write-Host " $_" } +Write-Host " exit=$LASTEXITCODE" diff --git a/_build/run_sh.py b/_build/run_sh.py new file mode 100644 index 0000000..cb0f5f6 --- /dev/null +++ b/_build/run_sh.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +r"""跑设备上的验证脚本。""" +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 + +NAME = sys.argv[1] if len(sys.argv) > 1 else "verify_one_link.sh" +TIMEOUT = int(sys.argv[2]) if len(sys.argv) > 2 else 600 + + +def main() -> int: + local = ROOT / "_build" / NAME + raw = local.read_bytes() + if b"\r\n" in raw: + local.write_bytes(raw.replace(b"\r\n", b"\n")) + upload(str(local), "/root/run_it.sh", 0o755) + _c, out, _e = run("bash /root/run_it.sh 2>&1", timeout=TIMEOUT) + print(out[-3000:]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/run_vdf.py b/_build/run_vdf.py new file mode 100644 index 0000000..5a9c6ca --- /dev/null +++ b/_build/run_vdf.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +r"""跑设备上的验证脚本(避免 PowerShell 的引号/变量展开地狱)。""" +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 + + +def main() -> int: + local = ROOT / "_build" / "verify_dl_final.sh" + raw = local.read_bytes() + if b"\r\n" in raw: + local.write_bytes(raw.replace(b"\r\n", b"\n")) + + upload(str(local), "/root/vdf.sh", 0o755) + _code, out, _err = run("bash /root/vdf.sh 2>&1", timeout=900) + print(out[-2600:]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/simple_download_link.py b/_build/simple_download_link.py new file mode 100644 index 0000000..4423ff5 --- /dev/null +++ b/_build/simple_download_link.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +r"""下载区简化成"一个链接"(用户要求)。 + +用户:"没有,你就放一个下载连接就行了" + +所以把原来那套(文件列表 + 大小 + 大文件提示 + Gitee 备用入口) +全部去掉,只留**一行一个链接**,点了就能下。 + +链接指向中转自己的下载区(/v1/downloads 里最新的那个文件), +这样不管叫什么都自动指向最新的,不用每次改页面。 + +同时保留流式发送(19MB 走隧道本来就慢, +一次性读进内存再发更容易被 Cloudflare 100 秒上限掐断)。 +""" +from __future__ import annotations + +import ast +import re +import sys +from pathlib import Path + +RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py") + +NEW_CARD = '''
+
下载最新版
+
正在读…
+
''' + +NEW_JS = '''// ---------------- 下载(就一个链接)---------------- +(async () => { + const body = document.getElementById('dlBody'); + if (!body) return; + try { + const r = await fetch('/v1/downloads'); + const j = await r.json(); + const files = (j && j.files) || []; + if (!files.length) { + body.innerHTML = '服务器上还没有成品。'; + return; + } + // 按名字里的版本号挑最新的那个整包;没有就挑第一个 + const pick = files.find(f => /agent/i.test(f.name)) + || files.find(f => /\\.zip$/i.test(f.name)) + || files[0]; + const mb = (pick.size / 1024 / 1024).toFixed(2); + body.innerHTML = + '' + + '⬇ 下载 ' + pick.name + '' + + '
' + + mb + ' MB · 也可以去 ' + + 'Gitee 历史版本' + + '
'; + } catch (e) { + body.innerHTML = '读不到:' + e + ''; + } +})(); + +// ---------------- 自动刷新 ----------------''' + + +def main() -> int: + text = RELAY.read_text(encoding="utf-8") + if "下载最新版" in text: + print(" 已经是简化版") + return 0 + + # 1) 卡片:把原来那张(可能已经改过)整块换掉 + card = re.search( + r'
.*?\n
\n', text, re.S) + if card: + text = text[:card.start()] + NEW_CARD + "\n" + text[card.end():] + print(" 卡片简化成一个「下载最新版」") + else: + print(" [失败] 找不到下载卡片") + return 1 + + # 2) JS:把"下载区"那一整段换成新的(从注释到"自动刷新"之前) + js = re.search( + r'// ---------------- 下载.*?// ---------------- 自动刷新 ----------------', + text, re.S) + if js: + text = text[:js.start()] + NEW_JS + text[js.end():] + print(" JS 简化成一个链接") + else: + print(" [失败] 找不到下载 JS 段") + return 1 + + RELAY.write_text(text, encoding="utf-8", newline="\n") + try: + ast.parse(text) + print(" relay.py 语法 OK") + except SyntaxError as exc: + print(f" 语法错误: 行 {exc.lineno}: {exc.msg}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/verify_dl_final.sh b/_build/verify_dl_final.sh new file mode 100644 index 0000000..5143507 --- /dev/null +++ b/_build/verify_dl_final.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# 验证下载区(本地 + 公网) +set -u + +BASE=http://127.0.0.1:8080 +PUB=https://relay.xxmxxs.top +UA='LanRemote/1.4.0' +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) + +echo "===== 1. 管理页里的下载相关标记 =====" +page=$(curl -s --max-time 10 -H "X-Admin: $KEY" "$BASE/" 2>/dev/null) +for n in 'id=dlPanel' 'gitee.com/xmz7891' '历史版本' '/v1/downloads' '/download/'; do + if echo "$page" | grep -q "$n"; then + echo " ✓ $n" + else + echo " ✗ $n" + fi +done + +echo +echo "===== 2. 本地下载 =====" +for f in ykxmz-watch-1.5.0.apk ykxmz-server-1.5.0.zip ykxmz-agent-1.5.0.zip; do + info=$(curl -s --max-time 60 -o /dev/null -w '%{http_code} %{size_download}' \ + "$BASE/download/$f" 2>/dev/null) + printf " %-30s %s\n" "$f" "$info" +done + +echo +echo "===== 3. 公网下载(走隧道,这是用户实际会走的)=====" +for f in ykxmz-watch-1.5.0.apk ykxmz-server-1.5.0.zip; do + info=$(curl -s --max-time 120 -o /dev/null \ + -w '%{http_code} %{size_download}' \ + -H "User-Agent: $UA" "$PUB/download/$f" 2>/dev/null) + printf " %-30s %s\n" "$f" "$info" +done + +echo +echo "===== 4. 公网下载整包(19MB,慢一点)=====" +info=$(curl -s --max-time 300 -o /dev/null -w '%{http_code} %{size_download}' \ + -H "User-Agent: $UA" "$PUB/download/ykxmz-agent-1.5.0.zip" 2>/dev/null) +echo " ykxmz-agent-1.5.0.zip $info" + +echo +echo "===== 5. 公网 /v1/downloads 列表 =====" +curl -s --max-time 30 -H "User-Agent: $UA" "$PUB/v1/downloads" \ + | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + print(" 读不到"); sys.exit() +for f in d.get("files", []): + mb = f["size"] / 1024 / 1024 + print(f" {f[\"name\"]:34} {mb:6.2f} MB") +' diff --git a/_build/verify_one_link.sh b/_build/verify_one_link.sh new file mode 100644 index 0000000..3d29849 --- /dev/null +++ b/_build/verify_one_link.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# 验证"一个下载链接" +set -u + +BASE=http://127.0.0.1:8080 +PUB=https://relay.xxmxxs.top +UA='LanRemote/1.4.0' +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) + +echo "===== 1. 管理页里有下载区吗 =====" +page=$(curl -s --max-time 10 -H "X-Admin: $KEY" "$BASE/" 2>/dev/null) +for n in 'id=dlPanel' '下载最新版' '/v1/downloads'; do + echo "$page" | grep -q "$n" && echo " ✓ $n" || echo " ✗ $n" +done + +echo +echo "===== 2. /v1/downloads 会挑哪个 =====" +curl -s --max-time 15 "$BASE/v1/downloads" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +files = d.get("files", []) +pick = None +for f in files: + if "agent" in f["name"].lower(): + pick = f; break +if pick is None: + for f in files: + if f["name"].lower().endswith(".zip"): + pick = f; break +if pick is None and files: + pick = files[0] +print(" 会挑:", pick["name"] if pick else "(没有文件)") +print(" 大小: %.2f MB" % (pick["size"]/1024/1024) if pick else "") +' + +echo +echo "===== 3. 本地下载(走内网,应该很快)=====" +start=$(date +%s) +info=$(curl -s --max-time 120 -o /dev/null -w '%{http_code} %{size_download}' \ + "$BASE/download/ykxmz-agent-1.5.0.zip" 2>/dev/null) +end=$(date +%s) +echo " $info 耗时 $((end-start)) 秒" + +echo +echo "===== 4. 公网下载小文件(应该秒下)=====" +for f in ykxmz-watch-1.5.0.apk ykxmz-server-1.5.0.zip; do + start=$(date +%s) + info=$(curl -s --max-time 120 -o /dev/null -w '%{http_code} %{size_download}' \ + -H "User-Agent: $UA" "$PUB/download/$f" 2>/dev/null) + end=$(date +%s) + printf " %-30s %s %s 秒\n" "$f" "$info" "$((end-start))" +done + +echo +echo "===== 5. 公网下载大包(19MB,可能超时)=====" +start=$(date +%s) +info=$(curl -s --max-time 180 -o /dev/null -w '%{http_code} %{size_download}' \ + -H "User-Agent: $UA" "$PUB/download/ykxmz-agent-1.5.0.zip" 2>/dev/null) +end=$(date +%s) +echo " $info 耗时 $((end-start)) 秒" +if [ "${info%% *}" = "200" ]; then + echo " ✓ 大包也能通过隧道下" +else + echo " ! 大包走隧道不行(Cloudflare 100 秒上限)—— 页面上已经给了 Gitee 入口" +fi diff --git a/relay/relay.py b/relay/relay.py index 130b53c..c2f9baa 100644 --- a/relay/relay.py +++ b/relay/relay.py @@ -1010,9 +1010,9 @@ class Handler(BaseHTTPRequestHandler): self._json(404, {"ok": False, "error": "没有这个文件"}) return try: - blob = target.read_bytes() + size = target.stat().st_size except OSError as exc: - self._json(500, {"ok": False, "error": f"读取失败: {exc}"}) + self._json(500, {"ok": False, "error": f"读不到: {exc}"}) return if safe.lower().endswith(".apk"): content_type = "application/vnd.android.package-archive" @@ -1024,7 +1024,7 @@ class Handler(BaseHTTPRequestHandler): content_type = "application/octet-stream" self.send_response(200) self.send_header("Content-Type", content_type) - self.send_header("Content-Length", str(len(blob))) + self.send_header("Content-Length", str(size)) # 文件名可能含中文,用 RFC 5987 编码 quoted = urllib.parse.quote(safe) self.send_header( @@ -1032,8 +1032,28 @@ class Handler(BaseHTTPRequestHandler): f"attachment; filename*=UTF-8''{quoted}") self.send_header("Cache-Control", "no-store") self.end_headers() - self.wfile.write(blob) - log(f"下载:{safe}({len(blob)} 字节)") + + # 分块流式发送,不要一次性 read_bytes 再 write —— + # 大文件(19MB)那样会先攒满内存、数据也迟迟不开始流动; + # 而 Cloudflare 有 100 秒的源站响应上限,越晚开始发越容易被掐。 + sent = 0 + try: + with target.open("rb") as stream: + while True: + chunk = stream.read(65536) + if not chunk: + break + self.wfile.write(chunk) + sent += len(chunk) + self.wfile.flush() + except (BrokenPipeError, ConnectionResetError): + # 客户端/边缘主动断开(比如大文件超时),正常现象 + log(f"下载中断:{safe}(已发 {sent}/{size} 字节)") + return + except OSError as exc: + log(f"下载出错:{safe}(已发 {sent}/{size} 字节){exc}") + return + log(f"下载:{safe}({sent} 字节)") return if path in ("/v1/health", "/health"): @@ -1602,7 +1622,7 @@ class Handler(BaseHTTPRequestHandler):
-
下载 —— 最新版本
+
下载最新版
正在读…
@@ -1830,13 +1850,7 @@ if (netBtn) netBtn.onclick = async () => {{ }} }}; -// ---------------- 下载区 ---------------- -function humanSize(bytes) {{ - if (bytes < 1024) return bytes + ' B'; - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; - return (bytes / 1024 / 1024).toFixed(2) + ' MB'; -}} - +// ---------------- 下载(就一个链接)---------------- (async () => {{ const body = document.getElementById('dlBody'); if (!body) return; @@ -1845,25 +1859,25 @@ function humanSize(bytes) {{ const j = await r.json(); const files = (j && j.files) || []; if (!files.length) {{ - body.innerHTML = '还没有上传成品。' - + '把 APK / 整包放到服务器的 downloads 目录就会出现在这里。'; + body.innerHTML = '服务器上还没有成品。'; return; }} - body.innerHTML = files.map(f => {{ - const isApk = f.name.toLowerCase().endsWith('.apk'); - const tag = isApk - ? '手表 APK' - : '整包'; - return '
' - + tag - + '' - + f.name + '' - + '' - + humanSize(f.size) + '' - + '
'; - }}).join(''); + // 优先挑被控端整包;没有就挑第一个 zip;再没有就第一个 + const pick = files.find(f => /agent/i.test(f.name)) + || files.find(f => /\.zip$/i.test(f.name)) + || files[0]; + const mb = (pick.size / 1024 / 1024).toFixed(2); + body.innerHTML = + '' + + '⬇ 下载 ' + pick.name + '' + + '
' + + mb + ' MB · 也可以去 ' + + 'Gitee 历史版本' + + '
'; }} catch (e) {{ body.innerHTML = '读不到:' + e + ''; }}