mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
中转站加下载区 + 大文件流式发送
下载区
- 管理页加一张「下载最新版」卡片,就一个按钮
自动指向 /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)
This commit is contained in:
@@ -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 = ''' <div class=panel id=dlPanel>
|
||||||
|
<div class=label>下载 —— 最新版本</div>
|
||||||
|
<div id=dlBody class=tip>正在读…</div>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
NEW = ''' <div class=panel id=dlPanel>
|
||||||
|
<div class=label>下载 —— 最新版本</div>
|
||||||
|
<div id=dlBody class=tip>正在读…</div>
|
||||||
|
<div class=tip style="margin-top:10px;padding-top:8px;border-top:1px solid #30363d">
|
||||||
|
源码仓库
|
||||||
|
<a href="''' + REPO + '''" target="_blank" rel="noopener"
|
||||||
|
style="color:#79c0ff">gitee.com/xmz7891/ykxmz</a>
|
||||||
|
·
|
||||||
|
<a href="''' + RELEASES + '''" target="_blank" rel="noopener"
|
||||||
|
style="color:#79c0ff">历史版本</a>
|
||||||
|
</div>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
@@ -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 = '<span style="color:#8b949e">服务器上还没有成品。</span>';
|
||||||
|
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 =
|
||||||
|
'<a href="/download/' + encodeURIComponent(pick.name) + '" '
|
||||||
|
+ 'style="display:inline-block;padding:10px 20px;border-radius:6px;'
|
||||||
|
+ 'background:#238636;color:#fff;text-decoration:none;'
|
||||||
|
+ 'font-weight:600;font-size:15px">'
|
||||||
|
+ '⬇ 下载 ' + pick.name + '</a>'
|
||||||
|
+ '<div style="color:#6e7681;font-size:12px;margin-top:8px">'
|
||||||
|
+ mb + ' MB · 也可以去 '
|
||||||
|
+ '<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" '
|
||||||
|
+ 'rel="noopener" style="color:#79c0ff">Gitee 历史版本</a>'
|
||||||
|
+ '</div>';
|
||||||
|
}} catch (e) {{
|
||||||
|
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
||||||
|
}}
|
||||||
|
}})();
|
||||||
|
|
||||||
|
// ---------------- 自动刷新 ----------------'''
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
@@ -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
|
||||||
|
? '<span class="badge on">手表 APK</span>'
|
||||||
|
: '<span class="badge off">整包</span>';
|
||||||
|
return '<div style="display:flex;align-items:center;gap:10px;'
|
||||||
|
+ 'margin:6px 0;flex-wrap:wrap">'
|
||||||
|
+ tag
|
||||||
|
+ '<a href="/download/' + encodeURIComponent(f.name) + '" '
|
||||||
|
+ 'style="color:#79c0ff;text-decoration:none;font-weight:500">'
|
||||||
|
+ f.name + '</a>'
|
||||||
|
+ '<span style="color:#6e7681;font-size:12px">'
|
||||||
|
+ humanSize(f.size) + '</span>'
|
||||||
|
+ '</div>';
|
||||||
|
}}).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
|
||||||
|
? '<span class="badge on">手表 APK</span>'
|
||||||
|
: '<span class="badge off">整包</span>';
|
||||||
|
// 大文件走这条 4G 隧道很慢(Cloudflare 还有 100 秒上限),
|
||||||
|
// 超过 5MB 就提示一下,并给个 Gitee 的入口。
|
||||||
|
const big = f.size > 5 * 1024 * 1024;
|
||||||
|
const hint = big
|
||||||
|
? ' <span style="color:#d29922;font-size:12px">'
|
||||||
|
+ '(较大,隧道慢,建议走 Gitee)</span>'
|
||||||
|
: '';
|
||||||
|
const alt = big
|
||||||
|
? ' <a href="' + GITEE + '" target="_blank" rel="noopener" '
|
||||||
|
+ 'style="color:#8b949e;font-size:12px">从 Gitee 下</a>'
|
||||||
|
: '';
|
||||||
|
return '<div style="display:flex;align-items:center;gap:10px;'
|
||||||
|
+ 'margin:6px 0;flex-wrap:wrap">'
|
||||||
|
+ tag
|
||||||
|
+ '<a href="/download/' + encodeURIComponent(f.name) + '" '
|
||||||
|
+ 'style="color:#79c0ff;text-decoration:none;font-weight:500">'
|
||||||
|
+ f.name + '</a>'
|
||||||
|
+ '<span style="color:#6e7681;font-size:12px">'
|
||||||
|
+ humanSize(f.size) + '</span>'
|
||||||
|
+ hint + alt
|
||||||
|
+ '</div>';
|
||||||
|
}}).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())
|
||||||
@@ -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"
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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 = ''' <div class=panel id=dlPanel>
|
||||||
|
<div class=label>下载最新版</div>
|
||||||
|
<div id=dlBody class=tip>正在读…</div>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
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 = '<span style="color:#8b949e">服务器上还没有成品。</span>';
|
||||||
|
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 =
|
||||||
|
'<a href="/download/' + encodeURIComponent(pick.name) + '" '
|
||||||
|
+ 'style="display:inline-block;padding:10px 20px;border-radius:6px;'
|
||||||
|
+ 'background:#238636;color:#fff;text-decoration:none;'
|
||||||
|
+ 'font-weight:600;font-size:15px">'
|
||||||
|
+ '⬇ 下载 ' + pick.name + '</a>'
|
||||||
|
+ '<div style="color:#6e7681;font-size:12px;margin-top:8px">'
|
||||||
|
+ mb + ' MB · 也可以去 '
|
||||||
|
+ '<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" '
|
||||||
|
+ 'rel="noopener" style="color:#79c0ff">Gitee 历史版本</a>'
|
||||||
|
+ '</div>';
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ---------------- 自动刷新 ----------------'''
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
text = RELAY.read_text(encoding="utf-8")
|
||||||
|
if "下载最新版" in text:
|
||||||
|
print(" 已经是简化版")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 1) 卡片:把原来那张(可能已经改过)整块换掉
|
||||||
|
card = re.search(
|
||||||
|
r' <div class=panel id=dlPanel>.*?\n </div>\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())
|
||||||
@@ -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")
|
||||||
|
'
|
||||||
@@ -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
|
||||||
+43
-29
@@ -1010,9 +1010,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._json(404, {"ok": False, "error": "没有这个文件"})
|
self._json(404, {"ok": False, "error": "没有这个文件"})
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
blob = target.read_bytes()
|
size = target.stat().st_size
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
self._json(500, {"ok": False, "error": f"读取失败: {exc}"})
|
self._json(500, {"ok": False, "error": f"读不到: {exc}"})
|
||||||
return
|
return
|
||||||
if safe.lower().endswith(".apk"):
|
if safe.lower().endswith(".apk"):
|
||||||
content_type = "application/vnd.android.package-archive"
|
content_type = "application/vnd.android.package-archive"
|
||||||
@@ -1024,7 +1024,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
content_type = "application/octet-stream"
|
content_type = "application/octet-stream"
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", content_type)
|
self.send_header("Content-Type", content_type)
|
||||||
self.send_header("Content-Length", str(len(blob)))
|
self.send_header("Content-Length", str(size))
|
||||||
# 文件名可能含中文,用 RFC 5987 编码
|
# 文件名可能含中文,用 RFC 5987 编码
|
||||||
quoted = urllib.parse.quote(safe)
|
quoted = urllib.parse.quote(safe)
|
||||||
self.send_header(
|
self.send_header(
|
||||||
@@ -1032,8 +1032,28 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
f"attachment; filename*=UTF-8''{quoted}")
|
f"attachment; filename*=UTF-8''{quoted}")
|
||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.end_headers()
|
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
|
return
|
||||||
|
|
||||||
if path in ("/v1/health", "/health"):
|
if path in ("/v1/health", "/health"):
|
||||||
@@ -1602,7 +1622,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=panel id=dlPanel>
|
<div class=panel id=dlPanel>
|
||||||
<div class=label>下载 —— 最新版本</div>
|
<div class=label>下载最新版</div>
|
||||||
<div id=dlBody class=tip>正在读…</div>
|
<div id=dlBody class=tip>正在读…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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 () => {{
|
(async () => {{
|
||||||
const body = document.getElementById('dlBody');
|
const body = document.getElementById('dlBody');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
@@ -1845,25 +1859,25 @@ function humanSize(bytes) {{
|
|||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
const files = (j && j.files) || [];
|
const files = (j && j.files) || [];
|
||||||
if (!files.length) {{
|
if (!files.length) {{
|
||||||
body.innerHTML = '<span style="color:#8b949e">还没有上传成品。'
|
body.innerHTML = '<span style="color:#8b949e">服务器上还没有成品。</span>';
|
||||||
+ '把 APK / 整包放到服务器的 downloads 目录就会出现在这里。</span>';
|
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
body.innerHTML = files.map(f => {{
|
// 优先挑被控端整包;没有就挑第一个 zip;再没有就第一个
|
||||||
const isApk = f.name.toLowerCase().endsWith('.apk');
|
const pick = files.find(f => /agent/i.test(f.name))
|
||||||
const tag = isApk
|
|| files.find(f => /\.zip$/i.test(f.name))
|
||||||
? '<span class="badge on">手表 APK</span>'
|
|| files[0];
|
||||||
: '<span class="badge off">整包</span>';
|
const mb = (pick.size / 1024 / 1024).toFixed(2);
|
||||||
return '<div style="display:flex;align-items:center;gap:10px;'
|
body.innerHTML =
|
||||||
+ 'margin:6px 0;flex-wrap:wrap">'
|
'<a href="/download/' + encodeURIComponent(pick.name) + '" '
|
||||||
+ tag
|
+ 'style="display:inline-block;padding:10px 20px;border-radius:6px;'
|
||||||
+ '<a href="/download/' + encodeURIComponent(f.name) + '" '
|
+ 'background:#238636;color:#fff;text-decoration:none;'
|
||||||
+ 'style="color:#79c0ff;text-decoration:none;font-weight:500">'
|
+ 'font-weight:600;font-size:15px">'
|
||||||
+ f.name + '</a>'
|
+ '⬇ 下载 ' + pick.name + '</a>'
|
||||||
+ '<span style="color:#6e7681;font-size:12px">'
|
+ '<div style="color:#6e7681;font-size:12px;margin-top:8px">'
|
||||||
+ humanSize(f.size) + '</span>'
|
+ mb + ' MB · 也可以去 '
|
||||||
|
+ '<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" '
|
||||||
|
+ 'rel="noopener" style="color:#79c0ff">Gitee 历史版本</a>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}}).join('');
|
|
||||||
}} catch (e) {{
|
}} catch (e) {{
|
||||||
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user