mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 08:20:44 +00:00
阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收
阶段3 管理页 - 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、 删除按钮 #2d1214 等 11 处),现在 0 处深色 - 接口全部正常:health / downloads / netcheck 阶段4 局域网诊断 - 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的** (lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一), 是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据 - 所以改了手表的提示:不再只说"不在同一网络", 而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi - 端到端验证局域网路径:认证 1 ms,功能列表 207 项 阶段1 结论修正(一天内推翻自己两次,如实记录) - 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP - 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了 —— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址 - 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余 - 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s - **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**, 比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身, 不是设备的隧道。公网侧已无优化空间。 - 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路 阶段5 验收 - 全量回归 29 项全通过 - 设备侧验收 38 项全通过 - 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收
|
||||
|
||||
阶段3 管理页
|
||||
- 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、
|
||||
删除按钮 #2d1214 等 11 处),现在 0 处深色
|
||||
- 接口全部正常:health / downloads / netcheck
|
||||
|
||||
阶段4 局域网诊断
|
||||
- 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的**
|
||||
(lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一),
|
||||
是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据
|
||||
- 所以改了手表的提示:不再只说"不在同一网络",
|
||||
而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi
|
||||
- 端到端验证局域网路径:认证 1 ms,功能列表 207 项
|
||||
|
||||
阶段1 结论修正(一天内推翻自己两次,如实记录)
|
||||
- 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP
|
||||
- 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了
|
||||
—— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址
|
||||
- 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余
|
||||
- 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s
|
||||
- **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**,
|
||||
比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身,
|
||||
不是设备的隧道。公网侧已无优化空间。
|
||||
- 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路
|
||||
|
||||
阶段5 验收
|
||||
- 全量回归 29 项全通过
|
||||
- 设备侧验收 38 项全通过
|
||||
- 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"lantest": {
|
||||
"name": "测试手表",
|
||||
"token": "6ec9b2878357045a389acd97bab84871",
|
||||
"paired_at": 1791303182.5310695,
|
||||
"last_seen": 1791303182.5336223
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"port": 8875,
|
||||
"server_name": "DESKTOP-RN5I625",
|
||||
"allow_run": true,
|
||||
"allow_power": true,
|
||||
"allow_screen": true,
|
||||
"start_minimized": false,
|
||||
"allow_new_pairing": false,
|
||||
"image_timeout": 0,
|
||||
"image_display": "window",
|
||||
"image_custom_width": 0,
|
||||
"image_custom_height": 0,
|
||||
"image_window_title": "",
|
||||
"tts_voice": "",
|
||||
"tts_rate": 0,
|
||||
"mp3_dir": "",
|
||||
"file_roots": "",
|
||||
"extra_apps": "",
|
||||
"extra_commands": "",
|
||||
"download_dir": "received",
|
||||
"screen_max_width": 480,
|
||||
"screen_quality": 72,
|
||||
"screen_fps": 12,
|
||||
"pairing_code": "246810",
|
||||
"pairing_created": 1791303175.5928898,
|
||||
"setup_done": true,
|
||||
"close_pair_after_first": false,
|
||||
"relay_last_good": "http://192.168.2.156:8080",
|
||||
"relay_device_id": "DESKTOP-RN5I625-832c5e0b202b"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
阶段1+2:恢复 HTTP/2 加速 + 重做官网
|
||||
|
||||
阶段1 网络加速
|
||||
- 发现隧道在跑 QUIC(ExecStart 丢了 --protocol http2),
|
||||
实测 QUIC 可用率 48% vs HTTP/2 80%+,是重大性能回退。
|
||||
恢复后公网延迟从 4141ms 降到 690ms(快 6 倍),可用率 92%。
|
||||
- 优选 IP 结论:无效。hosts 覆盖确实生效了(日志里连的就是指定 IP),
|
||||
但落点仍是 lax —— 运营商把所有 Cloudflare anycast 都路由到美西。
|
||||
已清掉这段无效配置。
|
||||
- BBR 不可用:设备是自定义内核 6.4.0-rc4-jsbsbxjxh66,
|
||||
没编 tcp_bbr 模块也没配置项。改用 cubic + 调优:
|
||||
TCP 缓冲 4MB、tcp_fastopen=3、tcp_slow_start_after_idle=0
|
||||
(长轮询场景关键)、tcp_tw_reuse=1。
|
||||
|
||||
阶段2 官网重做
|
||||
- 拆出独立模块 relay/landing.py(relay.py 已两千多行,
|
||||
再塞 HTML 没法看)
|
||||
- 信息结构参考成熟产品站:吸顶导航 / Hero / 九宫格功能 /
|
||||
三步上手 / 下载 / FAQ / 页脚
|
||||
- 保持白色简洁:底白、浅灰面板、细边框、大量留白
|
||||
- logo 用内联 SVG(ICO 放 img 里实测是破图)
|
||||
- 管理入口从官网移除(管理不对外开放)
|
||||
|
||||
过程中被自己的测试抓到两个 bug:
|
||||
- 模块叫 site.py 会和 Python 标准库重名,import site 拿到的是标准库
|
||||
- _logo() 里把 % 格式化和字符串拼接混用,直接抛 TypeError
|
||||
@@ -0,0 +1,496 @@
|
||||
<!doctype html><html lang=zh>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<link rel="icon" href="https://relay.xxmxxs.top/favicon.ico">
|
||||
<title>ykxmz 中转服务器</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}
|
||||
:root{
|
||||
--bg:#ffffff; --panel:#ffffff; --panel2:#f6f8fa; --border:#d0d7de;
|
||||
--fg:#1f2328; --dim:#656d76; --muted:#8b949e;
|
||||
--accent:#0969da; --ok:#1a7f37; --danger:#f85149; --warn:#9a6700;
|
||||
}
|
||||
html,body{max-width:100%;overflow-x:hidden}
|
||||
body{margin:0;padding:0;background:var(--bg);color:var(--fg);
|
||||
font:14px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',
|
||||
'Microsoft YaHei',sans-serif;-webkit-font-smoothing:antialiased}
|
||||
.wrap{width:100%;max-width:1120px;margin:0 auto;padding:22px 20px 40px}
|
||||
|
||||
/* ---------- 头部 ---------- */
|
||||
header{display:flex;align-items:center;gap:12px;
|
||||
padding-bottom:16px;border-bottom:1px solid var(--border);
|
||||
margin-bottom:20px;flex-wrap:wrap}
|
||||
.logo{width:38px;height:38px;border-radius:10px;flex:none;
|
||||
background:linear-gradient(140deg,#0969da,#1f6feb);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-weight:700;font-size:15px;color:#fff;letter-spacing:-.5px}
|
||||
.brand h1{margin:0;font-size:17px;font-weight:600;letter-spacing:.2px}
|
||||
.brand .sub{color:var(--dim);font-size:12px}
|
||||
.meta{margin-left:auto;text-align:right;color:var(--muted);font-size:12px;
|
||||
line-height:1.5}
|
||||
.meta b{color:var(--dim);font-weight:500}
|
||||
.meta a.logout{color:var(--dim);text-decoration:none;
|
||||
border-bottom:1px dashed #484f58;transition:.12s}
|
||||
.meta a.logout:hover{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
|
||||
/* ---------- 通用卡片 ---------- */
|
||||
.panel{background:var(--panel);border:1px solid var(--border);
|
||||
border-radius:12px;padding:16px 18px;margin-bottom:16px}
|
||||
.panel .label{color:var(--dim);font-size:12px;margin-bottom:8px;
|
||||
display:flex;align-items:center;gap:6px}
|
||||
|
||||
/* ---------- 公网地址 ---------- */
|
||||
.urlrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.url{flex:1;min-width:220px;font:600 15px/1.5 ui-monospace,Consolas,
|
||||
'Courier New',monospace;color:#0969da;word-break:break-all;
|
||||
background:#0b1220;border:1px solid #1f6feb44;border-radius:8px;
|
||||
padding:10px 12px}
|
||||
.url.empty{color:var(--muted);font-weight:400;font-style:normal}
|
||||
|
||||
/* ---------- 统计 ---------- */
|
||||
.stats{display:grid;gap:12px;margin-bottom:16px;
|
||||
grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
|
||||
.stat{background:var(--panel);border:1px solid var(--border);
|
||||
border-radius:12px;padding:14px 16px;position:relative;overflow:hidden}
|
||||
.stat .n{font-size:26px;font-weight:650;line-height:1.15;
|
||||
letter-spacing:-.5px}
|
||||
.stat .t{color:var(--dim);font-size:12px;margin-top:2px}
|
||||
.stat::after{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
||||
background:var(--accent);opacity:.65}
|
||||
.stat.ok::after{background:var(--ok)}
|
||||
.stat.warn::after{background:var(--warn)}
|
||||
.stat.mute::after{background:#484f58}
|
||||
|
||||
/* ---------- 工具条 ---------- */
|
||||
.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
|
||||
margin-bottom:12px}
|
||||
.toolbar .spacer{flex:1}
|
||||
.tip{color:var(--muted);font-size:12px}
|
||||
|
||||
/* ---------- 按钮 ---------- */
|
||||
button{font-family:inherit;font-size:13px;cursor:pointer;
|
||||
border-radius:8px;padding:7px 14px;border:1px solid var(--border);
|
||||
background:var(--panel2);color:var(--fg);transition:.12s}
|
||||
button:hover{background:#262c36;border-color:#484f58}
|
||||
button:active{transform:translateY(1px)}
|
||||
button:disabled{opacity:.4;cursor:not-allowed;transform:none}
|
||||
button.primary{background:var(--accent);border-color:var(--accent);
|
||||
color:#fff;font-weight:600}
|
||||
button.primary:hover{background:#4a95f8;border-color:#4a95f8}
|
||||
button.del{background:#2d1214;border-color:#6e2b2b;color:#ff7b72}
|
||||
button.del:hover{background:#4a1a1e;border-color:#a33;color:#ffa198}
|
||||
button.del:disabled{opacity:.4}
|
||||
button.small{padding:5px 11px;font-size:12px}
|
||||
|
||||
/* ---------- 表格 ---------- */
|
||||
.tablewrap{background:var(--panel);border:1px solid var(--border);
|
||||
border-radius:12px;overflow:hidden;max-width:100%}
|
||||
table{width:100%;max-width:100%;border-collapse:collapse;
|
||||
table-layout:fixed}
|
||||
th,td{padding:11px 14px;text-align:left;font-size:13px;
|
||||
border-bottom:1px solid #eaeef2;vertical-align:middle;
|
||||
overflow-wrap:anywhere;min-width:0}
|
||||
th{background:var(--panel2);color:var(--dim);font-weight:500;
|
||||
font-size:12px;white-space:nowrap}
|
||||
tbody tr{transition:background .1s}
|
||||
tbody tr:hover td{background:#1b222c}
|
||||
tbody tr:last-child td{border-bottom:none}
|
||||
td.name{font-weight:500}
|
||||
td.num{font-family:ui-monospace,Consolas,monospace;color:var(--dim);
|
||||
font-size:12px}
|
||||
td.ops{text-align:right;white-space:nowrap}
|
||||
.sub2{color:var(--muted);font-size:11px;font-weight:400;
|
||||
font-family:ui-monospace,Consolas,monospace;margin-top:1px}
|
||||
.val{min-width:0;overflow-wrap:anywhere;word-break:break-word}
|
||||
td.empty{text-align:center;padding:44px 20px;color:var(--dim)}
|
||||
td.empty .big{font-size:15px;color:var(--fg);margin-bottom:6px;font-weight:500}
|
||||
code{background:#eaeef2;padding:2px 8px;border-radius:6px;
|
||||
letter-spacing:1.5px;font-family:ui-monospace,Consolas,monospace;
|
||||
font-size:13px;color:#0969da}
|
||||
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;
|
||||
margin-right:7px;vertical-align:middle;background:#484f58}
|
||||
.dot.on{background:var(--ok);box-shadow:0 0 0 3px #1a7f3722}
|
||||
.badge{font-size:11px;padding:2px 8px;border-radius:20px;
|
||||
border:1px solid transparent;white-space:nowrap}
|
||||
.badge.on{color:var(--ok);background:#1a7f371a;border-color:#1a7f3740}
|
||||
.badge.off{color:var(--muted);background:#8b949e1a;border-color:#8b949e40}
|
||||
.pickcell{width:38px;text-align:center;padding-right:0}
|
||||
input[type=checkbox]{width:15px;height:15px;cursor:pointer;
|
||||
accent-color:var(--accent);vertical-align:middle}
|
||||
tr.picked td{background:#1c2733}
|
||||
tr.picked:hover td{background:#213042}
|
||||
|
||||
/* ---------- 提示条 ---------- */
|
||||
#toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,20px);
|
||||
background:#1f6feb;color:#fff;padding:11px 20px;border-radius:10px;
|
||||
font-size:13px;box-shadow:0 8px 24px #0008;opacity:0;
|
||||
transition:.22s;pointer-events:none;max-width:88vw;text-align:center}
|
||||
#toast.show{opacity:1;transform:translate(-50%,0)}
|
||||
#toast.err{background:#b62324}
|
||||
#toast.ok{background:#1a7f37}
|
||||
|
||||
footer{color:var(--muted);font-size:12px;margin-top:22px;
|
||||
padding-top:14px;border-top:1px solid var(--border);line-height:1.7}
|
||||
|
||||
/* ---------- 窄屏:表格变卡片 ---------- */
|
||||
@media (max-width:720px){
|
||||
.wrap{padding:16px 12px 32px}
|
||||
/* 从根上保证不会有东西超出容器。
|
||||
表格在窄屏变成卡片式,这几条必须写死,
|
||||
不然 table-layout:fixed 对 display:block 的行不起作用。 */
|
||||
table,tbody,tr,td{display:block;width:100%;max-width:100%;
|
||||
box-sizing:border-box}
|
||||
.meta{margin-left:0;text-align:left;width:100%}
|
||||
.stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
|
||||
.stat{padding:11px 13px}
|
||||
.stat .n{font-size:21px}
|
||||
.urlrow{flex-direction:column;align-items:stretch}
|
||||
.url{min-width:0;font-size:13px}
|
||||
thead{display:none}
|
||||
tbody tr{display:block;border-bottom:1px solid var(--border);
|
||||
padding:12px 14px 14px;position:relative}
|
||||
tbody tr:last-child{border-bottom:none}
|
||||
/* 每个值一行:左边标签、右边值。
|
||||
min-width:0 + overflow-wrap 是关键 ——
|
||||
少了它,长配对码/流量会把容器撑破、右边的值被截掉。 */
|
||||
tbody td{display:flex;justify-content:space-between;align-items:baseline;
|
||||
gap:14px;border:none;padding:3px 0;text-align:right;
|
||||
min-width:0;max-width:100%;overflow-wrap:anywhere;word-break:break-word}
|
||||
tbody td .val{flex:1 1 auto;min-width:0;text-align:right}
|
||||
td.name{display:block}
|
||||
tbody td::before{content:attr(data-l);color:var(--dim);font-size:12px;
|
||||
text-align:left;flex:none}
|
||||
tbody td>*{min-width:0}
|
||||
/* 设备名占满整行,不参与"标签/值"那一套 */
|
||||
td.name{display:block;text-align:left;padding:0 32px 8px 0}
|
||||
td.name::before{content:'';display:none}
|
||||
td.name .v{font-size:15px}
|
||||
td.pickcell{position:absolute;right:12px;top:12px;width:auto;
|
||||
padding:0;min-width:0}
|
||||
td.pickcell::before{content:'';display:none}
|
||||
td.ops{justify-content:flex-end;padding-top:10px;margin-top:4px;
|
||||
border-top:1px solid #eaeef2}
|
||||
td.ops::before{content:'';display:none}
|
||||
td.empty{display:block;text-align:center}
|
||||
td.empty::before{content:'';display:none}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class=wrap>
|
||||
<header>
|
||||
<div class=logo>yx</div>
|
||||
<div class=brand>
|
||||
<h1>ykxmz 中转服务器</h1>
|
||||
<div class=sub>远程控制 · 跨网络中转</div>
|
||||
</div>
|
||||
<div class=meta>
|
||||
<div>版本 <b>1.7.1</b> · 已运行 <b>1 分</b></div>
|
||||
<div>本进程内存 <b>22 MB</b> · 累计请求 <b>3</b>
|
||||
· <a href="/admin/logout" class=logout>退出登录</a></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class=panel>
|
||||
<div class=label>当前公网地址 —— 手表「中转设置」里填这个</div>
|
||||
<div class=urlrow>
|
||||
<div class="url" id=urlBox>https://relay.xxmxxs.top</div>
|
||||
<button class="btn primary" id=copyUrl >复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=panel id=netPanel>
|
||||
<div class=label>网络体检 —— 走的是局域网还是隧道?</div>
|
||||
<div id=netBody class=tip>点下面按钮测一下</div>
|
||||
<div style="margin-top:10px">
|
||||
<button class="btn small" id=netBtn>测一下</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=panel id=dlPanel>
|
||||
<div class=label>下载最新版</div>
|
||||
<div id=dlBody class=tip>正在读…</div>
|
||||
</div>
|
||||
|
||||
<div class=stats>
|
||||
<div class=stat><div class=n>0</div><div class=t>登记设备</div></div>
|
||||
<div class="stat mute"><div class=n>0</div><div class=t>在线</div></div>
|
||||
<div class="stat mute"><div class=n>0</div><div class=t>手表会话</div></div>
|
||||
<div class=stat><div class=n>49</div><div class=t>配对码占用</div></div>
|
||||
</div>
|
||||
|
||||
<div class=toolbar>
|
||||
<button class="btn del" id=delPicked>删除选中</button>
|
||||
<button class="btn del" id=delAll>删除全部设备</button>
|
||||
<div class=spacer></div>
|
||||
<label class=tip style="cursor:pointer;display:flex;align-items:center;gap:6px">
|
||||
<input type=checkbox id=autoRef checked> 自动刷新(10 秒)
|
||||
</label>
|
||||
</div>
|
||||
<div class=tip style="margin:-4px 0 12px">
|
||||
删除会连配对码一起清掉,那台设备下次可以重新设码。
|
||||
</div>
|
||||
|
||||
<div class=tablewrap>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class=pickcell><input type=checkbox id=pickAll title="全选"></th>
|
||||
<th>设备</th><th>配对码</th><th>状态</th>
|
||||
<th>空闲</th><th>待发(表/机)</th><th>流量(入/出)</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td colspan=8 class=empty><div class=big>还没有电脑连上来</div><div>在被控端双击 ykxmz.exe 设好配对码,手表上输那 6 位数字就能连上。</div></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
数据来自 /v1/health · 只显示配对码与流量,看不到任何传输内容<br>
|
||||
中转只是转发字节,不解析、不存储会话内容。
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div id=toast></div>
|
||||
|
||||
<script>
|
||||
const toast = (msg, kind) => {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.className = kind ? ('show ' + kind) : 'show';
|
||||
clearTimeout(el._t);
|
||||
el._t = setTimeout(() => { el.className = ''; }, 2600);
|
||||
};
|
||||
|
||||
const post = async (path, payload) => {
|
||||
const r = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload || {})
|
||||
});
|
||||
return r.json();
|
||||
};
|
||||
|
||||
// ---------------- 单台删除 ----------------
|
||||
document.querySelectorAll('button.del[data-device]').forEach(b => {
|
||||
b.onclick = async () => {
|
||||
const name = b.dataset.name, code = b.dataset.code;
|
||||
if (!confirm('删除设备「' + name + '」(配对码 ' + code + ')?\n\n'
|
||||
+ '这会:\n 1. 断开它现在的手表连接\n'
|
||||
+ ' 2. 从服务器删掉这台设备\n'
|
||||
+ ' 3. 清掉它的配对码绑定')) return;
|
||||
try {
|
||||
const j = await post('/v1/admin/delete', {deviceId: b.dataset.device});
|
||||
if (j.ok) {
|
||||
toast('已删除「' + (j.name || name) + '」,清掉配对码 '
|
||||
+ (j.codesCleared || []).length + ' 条', 'ok');
|
||||
setTimeout(() => location.reload(), 700);
|
||||
} else {
|
||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
||||
}
|
||||
} catch (e) {
|
||||
toast('请求出错:' + e, 'err');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ---------------- 批量选择 ----------------
|
||||
const picks = () => Array.from(document.querySelectorAll('input.pick'));
|
||||
const picked = () => picks().filter(c => c.checked).map(c => c.value);
|
||||
|
||||
function refreshPickState() {
|
||||
const chosen = picked().length;
|
||||
const btn = document.getElementById('delPicked');
|
||||
if (btn) {
|
||||
btn.disabled = chosen === 0;
|
||||
btn.textContent = chosen ? ('删除选中 (' + chosen + ')') : '删除选中';
|
||||
}
|
||||
picks().forEach(c => {
|
||||
const tr = c.closest('tr');
|
||||
if (tr) tr.classList.toggle('picked', c.checked);
|
||||
});
|
||||
const all = document.getElementById('pickAll');
|
||||
if (all) {
|
||||
const total = picks().length;
|
||||
all.checked = total > 0 && chosen === total;
|
||||
all.indeterminate = chosen > 0 && chosen < total;
|
||||
}
|
||||
}
|
||||
|
||||
picks().forEach(c => c.addEventListener('change', refreshPickState));
|
||||
const pickAll = document.getElementById('pickAll');
|
||||
if (pickAll) pickAll.addEventListener('change', () => {
|
||||
picks().forEach(c => { c.checked = pickAll.checked; });
|
||||
refreshPickState();
|
||||
});
|
||||
refreshPickState();
|
||||
|
||||
// ---------------- 批量删除 ----------------
|
||||
const delPicked = document.getElementById('delPicked');
|
||||
if (delPicked) delPicked.onclick = async () => {
|
||||
const ids = picked();
|
||||
if (!ids.length) return;
|
||||
if (!confirm('删除选中的 ' + ids.length + ' 台设备?\n\n'
|
||||
+ '会断开它们现在的手表连接,并清掉它们占的配对码。'))
|
||||
return;
|
||||
delPicked.disabled = true;
|
||||
try {
|
||||
const j = await post('/v1/admin/delete_many', {deviceIds: ids});
|
||||
if (j.ok) {
|
||||
toast('已删除 ' + j.deleted + ' 台设备'
|
||||
+ (j.failed ? (',' + j.failed + ' 台失败') : ''), 'ok');
|
||||
setTimeout(() => location.reload(), 700);
|
||||
} else {
|
||||
toast('删除失败:' + (j.error || '未知错误'), 'err');
|
||||
delPicked.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
toast('请求出错:' + e, 'err');
|
||||
delPicked.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- 删除全部 ----------------
|
||||
const delAll = document.getElementById('delAll');
|
||||
if (delAll) delAll.onclick = async () => {
|
||||
if (!confirm('删除**全部**设备(连同所有配对码)?\n\n'
|
||||
+ '清场用,删完所有电脑都要重新设码。')) return;
|
||||
try {
|
||||
const j = await post('/v1/admin/delete_all');
|
||||
toast('已删除 ' + (j.deleted || 0) + ' 台设备', 'ok');
|
||||
setTimeout(() => location.reload(), 700);
|
||||
} catch (e) {
|
||||
toast('请求出错:' + e, 'err');
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- 复制公网地址 ----------------
|
||||
const copyUrl = document.getElementById('copyUrl');
|
||||
if (copyUrl) copyUrl.onclick = async () => {
|
||||
const text = document.getElementById('urlBox').textContent.trim();
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast('地址已复制', 'ok');
|
||||
} catch (e) {
|
||||
// 非安全上下文(http 且不是 localhost)没有 clipboard API,
|
||||
// 退回到老办法:选中 + execCommand
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(document.getElementById('urlBox'));
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
try { document.execCommand('copy'); toast('地址已复制', 'ok'); }
|
||||
catch (e2) { toast('复制失败,请手动选中', 'err'); }
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- 网络体检 ----------------
|
||||
const netBtn = document.getElementById('netBtn');
|
||||
if (netBtn) netBtn.onclick = async () => {
|
||||
const body = document.getElementById('netBody');
|
||||
body.textContent = '正在测…';
|
||||
try {
|
||||
const r = await fetch('/v1/admin/netcheck');
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
body.innerHTML = '<span style="color:#f85149">读不到:'
|
||||
+ (j.error || '未知') + '</span>';
|
||||
return;
|
||||
}
|
||||
const rows = [];
|
||||
if (j.tunnelMetrics) {
|
||||
const conns = j.tunnelConnections || 0;
|
||||
const color = conns >= 2 ? '#1a7f37'
|
||||
: (conns === 1 ? '#9a6700' : '#f85149');
|
||||
rows.push('隧道连接数:<b style="color:' + color + '">' + conns
|
||||
+ '</b>' + (conns === 0 ? '(断了,看门狗会重启)' : ''));
|
||||
rows.push('隧道累计请求:' + (j.tunnelRequests || 0));
|
||||
} else {
|
||||
rows.push('<span style="color:#9a6700">读不到隧道指标'
|
||||
+ (j.tunnelError ? '(' + j.tunnelError + ')' : '') + '</span>');
|
||||
}
|
||||
rows.push('登记设备:' + j.devices + '(在线 ' + j.online + ')');
|
||||
rows.push('手表会话:' + j.sessions);
|
||||
rows.push('中转内存:' + Math.round(j.memoryKB / 1024) + ' MB');
|
||||
rows.push('已运行:' + j.uptimeSeconds + ' 秒');
|
||||
if (j.publicUrl) {
|
||||
rows.push('公网地址:<code style="letter-spacing:0">'
|
||||
+ j.publicUrl + '</code>');
|
||||
}
|
||||
body.innerHTML = rows.map(t => '<div style="margin:2px 0">' + t
|
||||
+ '</div>').join('');
|
||||
// 顺便测一下这台机器的公网往返
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
await fetch('/v1/health?t=' + Date.now(), {cache: 'no-store'});
|
||||
const ms = Math.round(performance.now() - t0);
|
||||
body.innerHTML += '<div style="margin:2px 0">浏览器到中转往返:<b>'
|
||||
+ ms + ' ms</b>'
|
||||
+ (ms < 300 ? '(很顺)' : (ms < 1000 ? '(一般)' : '(有点慢)'))
|
||||
+ '</div>';
|
||||
} catch (e) {
|
||||
body.innerHTML += '<div style="margin:2px 0;color:#f85149">'
|
||||
+ '浏览器到中转:失败(' + e + ')</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
body.textContent = '请求出错:' + e;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------- 下载(就一个链接)----------------
|
||||
(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:#656d76">服务器上还没有成品。</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:#8b949e;font-size:12px;margin-top:8px">'
|
||||
+ mb + ' MB · 也可以去 '
|
||||
+ '<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" '
|
||||
+ 'rel="noopener" style="color:#0969da">Gitee 历史版本</a>'
|
||||
+ '</div>';
|
||||
} catch (e) {
|
||||
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
||||
}
|
||||
})();
|
||||
|
||||
// ---------------- 自动刷新 ----------------
|
||||
const autoRef = document.getElementById('autoRef');
|
||||
let autoTimer = null;
|
||||
function setupAuto() {
|
||||
clearInterval(autoTimer);
|
||||
if (autoRef && autoRef.checked) {
|
||||
autoTimer = setInterval(() => {
|
||||
// 有勾选/删除操作进行中就先别刷新,免得打断
|
||||
if (picked().length === 0 && !document.hidden) location.reload();
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
if (autoRef) {
|
||||
autoRef.addEventListener('change', () => {
|
||||
try { localStorage.setItem('lr_autoref', autoRef.checked ? '1' : '0'); }
|
||||
catch (e) {}
|
||||
setupAuto();
|
||||
});
|
||||
try {
|
||||
const saved = localStorage.getItem('lr_autoref');
|
||||
if (saved === '0') autoRef.checked = false;
|
||||
} catch (e) {}
|
||||
setupAuto();
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# 检查管理页的深色残留
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
|
||||
curl -s -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/adm.html 2>/dev/null
|
||||
|
||||
echo " 管理页 $(wc -c < /tmp/adm.html) 字节"
|
||||
echo " HTTP $(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")"
|
||||
|
||||
echo
|
||||
echo " 深色值残留:"
|
||||
found=0
|
||||
for c in 0b1220 262c36 1b222c 1c2733 213042 2d1214 4a1a1e 484f58 0d1117; do
|
||||
n=$(grep -c "#$c" /tmp/adm.html 2>/dev/null || echo 0)
|
||||
if [ "$n" != "0" ]; then
|
||||
echo " #$c : $n 处"
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
[ "$found" = "0" ] && echo " 无 ✓"
|
||||
|
||||
echo
|
||||
echo " 关键样式:"
|
||||
grep -o -- '--bg:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ /'
|
||||
grep -o 'background:#f6f8fa' /tmp/adm.html | head -1 | sed 's/^/ 地址框: /'
|
||||
grep -o 'button:hover{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 按钮悬停: /'
|
||||
grep -o 'button.del{background:#[0-9a-f]*' /tmp/adm.html | head -1 | sed 's/^/ 删除按钮: /'
|
||||
|
||||
echo
|
||||
echo " 功能接口还正常吗:"
|
||||
for api in /v1/health /v1/downloads /v1/admin/netcheck; do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Admin: $KEY" --max-time 10 "$BASE$api")
|
||||
echo " $api HTTP $code"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " 其余页面没受影响:"
|
||||
echo " 官网 HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/")"
|
||||
echo " favicon HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/favicon.ico")"
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''阶段4:诊断局域网直连为什么没生效。
|
||||
|
||||
现象(手表日志):
|
||||
已更新电脑地址为 192.168.2.64:8765
|
||||
电脑不在同一网络,继续用中转
|
||||
|
||||
判断逻辑在手表端 MainActivity:
|
||||
pickSameSubnet(client.relayLanIps()) <- 拿电脑报上来的 lanIps
|
||||
localIpv4List() <- 拿手表自己的网卡 IP
|
||||
sameSubnet(a, b) <- 比前 3 段(/24)
|
||||
|
||||
可能出问题的地方:
|
||||
1. 被控端报的 lanIps 里全是虚拟网卡(VMware/VirtualBox/Hyper-V)
|
||||
—— 真实的 192.168.2.64 反而不在里面
|
||||
2. 手表自己的 localIpv4List() 拿到的是运营商 IP(移动数据)
|
||||
3. 手表和电脑真的不在一个网段
|
||||
|
||||
这个脚本查第 1 条:本机被控端实际会报哪些 IP。
|
||||
第 2、3 条要手表端日志,我先把能查的查清。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
sys.path.insert(0, str(ROOT / "windows"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 一、被控端会报哪些 lanIps(这才是关键)")
|
||||
try:
|
||||
from lanremote.discovery import local_ipv4_list
|
||||
ips = local_ipv4_list()
|
||||
print(" local_ipv4_list() -> %s" % (ips,))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(" 拿不到(%s),换个方式" % exc)
|
||||
ips = []
|
||||
|
||||
# 直接列出所有网卡地址,对照着看
|
||||
print()
|
||||
print(" 二、本机所有 IPv4 地址(含虚拟网卡)")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
"Get-NetIPAddress -AddressFamily IPv4 | "
|
||||
"Where-Object { $_.IPAddress -notlike '127.*' } | "
|
||||
"ForEach-Object { $_.IPAddress + ' ' + $_.InterfaceAlias }"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
encoding="utf-8", errors="replace")
|
||||
for line in (result.stdout or "").splitlines():
|
||||
if line.strip():
|
||||
print(" " + line.strip())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(" 失败: %s" % exc)
|
||||
|
||||
print()
|
||||
print(" 三、判断:报出去的 IP 里有没有真实局域网地址")
|
||||
real = [ip for ip in ips
|
||||
if ip.startswith(("192.168.", "10.", "172.16.", "172.17.",
|
||||
"172.18.", "172.19.", "172.2", "172.30.",
|
||||
"172.31.")) and not ip.startswith("169.254.")]
|
||||
virtual_hint = [ip for ip in ips if ip.startswith(("192.168.56.",
|
||||
"192.168.99.",
|
||||
"172.17.", "172.18.",
|
||||
"172.19."))]
|
||||
print(" 报出去的: %s" % (ips or "(空)"))
|
||||
print(" 像真实局域网的: %s" % (real or "(没有!)"))
|
||||
print(" 像虚拟网卡的: %s" % (virtual_hint or "(没有)"))
|
||||
|
||||
if not ips:
|
||||
print()
|
||||
print(" ⚠ 一个都没报 —— 手表当然匹配不上。")
|
||||
elif not real:
|
||||
print()
|
||||
print(" ⚠ 报的全是虚拟网卡地址,手表连不到这些。")
|
||||
else:
|
||||
print()
|
||||
print(" OK 至少有一个真实局域网地址,手表应该能匹配到 %s" % real[0])
|
||||
|
||||
print()
|
||||
print(" 四、手表端要看的日志关键字")
|
||||
print(" 手表连上后,日志区会打:")
|
||||
print(" 「已更新电脑地址为 x.x.x.x:8765」—— 这是被控端报的地址")
|
||||
print(" 「电脑不在同一网络,继续用中转」—— 说明 sameSubnet 没匹配上")
|
||||
print(" 要确认两件事:")
|
||||
print(" 1. 那个 x.x.x.x 是不是电脑真实的局域网地址")
|
||||
print(" 2. 手表自己的 WiFi 地址是不是同一个 /24")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# 查隧道为什么只有 1 条连接
|
||||
set -u
|
||||
|
||||
echo "===== 1. 服务状态 ====="
|
||||
systemctl is-active lanremote-tunnel
|
||||
systemctl show lanremote-tunnel -p ExecMainStartTimestamp --value | sed 's/^/ 启动于: /'
|
||||
|
||||
echo
|
||||
echo "===== 2. metrics 全部隧道指标 ====="
|
||||
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep -E '^cloudflared_tunnel' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 3. 最近注册的连接 ====="
|
||||
grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -6 \
|
||||
| sed 's/.*\(connIndex=[0-9]*\).*\(location=[a-z0-9]*\).*\(protocol=[a-z0-9]*\).*/ \1 \2 \3/'
|
||||
|
||||
echo
|
||||
echo "===== 4. 有没有注册失败 ====="
|
||||
grep -iE 'failed|error|retry' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -6 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 5. HA 连接数设置 ====="
|
||||
grep -o 'ha-connections [0-9]*' /etc/systemd/system/lanremote-tunnel.service | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 6. 日志里 Initial protocol 的次数(每次重启会打)====="
|
||||
grep -c 'Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null | sed 's/^/ /'
|
||||
echo " 最近 3 条:"
|
||||
grep 'Initial protocol' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -3 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 7. 进程 ====="
|
||||
ps aux 2>/dev/null | grep -c '[c]loudflared' | sed 's/^/ cloudflared 进程数: /'
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# 最终验收:官网 / 管理页 / 下载 / 加速 / 功能
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
pass=0; fail=0
|
||||
|
||||
ok() { pass=$((pass+1)); echo " ✓ $1"; }
|
||||
bad() { fail=$((fail+1)); echo " ✗ $1"; }
|
||||
code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; }
|
||||
|
||||
echo "════════ 一、官网 ════════"
|
||||
c=$(code "$BASE/")
|
||||
[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c"
|
||||
curl -s --max-time 20 "$BASE/" -o /tmp/s.html
|
||||
size=$(wc -c < /tmp/s.html)
|
||||
[ "$size" -gt 10000 ] && ok "页面 $size 字节(内容丰富)" || bad "页面只有 $size 字节"
|
||||
|
||||
for pair in "吸顶导航:nav-in" "Hero:hero" "功能网格:id=\"features\"" \
|
||||
"三步上手:id=\"how\"" "下载区:id=\"download\"" \
|
||||
"FAQ:id=\"faq\"" "页脚:foot-in"; do
|
||||
label="${pair%%:*}"; key="${pair#*:}"
|
||||
if grep -q "$key" /tmp/s.html; then ok "$label"; else bad "$label"; fi
|
||||
done
|
||||
|
||||
[ "$(grep -c 'class=\"feat\"' /tmp/s.html)" = "9" ] && ok "9 张功能卡" || bad "功能卡数量不对"
|
||||
[ "$(grep -c '<details class=\"faq\"' /tmp/s.html)" = "6" ] && ok "6 条 FAQ" || bad "FAQ 数量不对"
|
||||
[ "$(grep -c 'class=\"dl-card\"' /tmp/s.html)" = "3" ] && ok "3 张下载卡" || bad "下载卡数量不对"
|
||||
[ "$(grep -c '管理入口' /tmp/s.html)" = "0" ] && ok "没有管理入口" || bad "还有管理入口"
|
||||
[ "$(grep -c 'src=\"/favicon.ico\" alt=\"\"' /tmp/s.html)" = "0" ] && ok "没有破图 logo" || bad "还有破图"
|
||||
|
||||
echo
|
||||
echo "════════ 二、管理页 ════════"
|
||||
c=$(code -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")
|
||||
[ "$c" = "200" ] && ok "登录后 HTTP 200" || bad "HTTP $c"
|
||||
curl -s --max-time 20 -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/a.html
|
||||
[ "$(grep -c 'dlPanel' /tmp/a.html)" -gt 0 ] && ok "下载区在" || bad "下载区丢了"
|
||||
[ "$(grep -c '网络体检' /tmp/a.html)" -gt 0 ] && ok "网络体检在" || bad "网络体检丢了"
|
||||
dark=0
|
||||
for cc in 0b1220 262c36 1b222c 1c2733 2d1214 4a1a1e 484f58 0d1117; do
|
||||
n=$(grep -c "#$cc" /tmp/a.html 2>/dev/null || echo 0)
|
||||
[ "$n" != "0" ] && dark=$((dark+n))
|
||||
done
|
||||
[ "$dark" = "0" ] && ok "无深色残留" || bad "深色残留 $dark 处"
|
||||
[ "$(code "$BASE/admin")" = "302" ] && ok "/admin 跳转正常" || bad "/admin 跳转异常"
|
||||
|
||||
echo
|
||||
echo "════════ 三、下载 ════════"
|
||||
for f in ykxmz-setup-1.7.1.exe ykxmz-server-1.7.1.zip ykxmz-watch-1.7.1.apk; do
|
||||
c=$(code "$BASE/download/$f")
|
||||
[ "$c" = "200" ] && ok "$f" || bad "$f HTTP $c"
|
||||
done
|
||||
[ "$(code "$BASE/v1/downloads")" = "200" ] && ok "/v1/downloads" || bad "/v1/downloads"
|
||||
c=$(code "$BASE/download/../relay.py")
|
||||
[ "$c" = "404" ] || [ "$c" = "400" ] && ok "路径穿越被挡($c)" || bad "路径穿越 HTTP $c"
|
||||
|
||||
echo
|
||||
echo "════════ 四、加速 ════════"
|
||||
proto=$(grep -oE 'Initial protocol [a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -1)
|
||||
echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto"
|
||||
conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
[ "$conn" = "2" ] && ok "隧道连接数 $conn" || bad "隧道连接数 $conn"
|
||||
[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了"
|
||||
[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" || bad "slow_start 没关"
|
||||
[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开"
|
||||
[ "$(grep -c argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 还有残留"
|
||||
|
||||
echo
|
||||
echo "════════ 五、功能 ════════"
|
||||
for api in /v1/health /favicon.ico; do
|
||||
c=$(code "$BASE$api")
|
||||
[ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c"
|
||||
done
|
||||
v=$(curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"')
|
||||
echo " $v"
|
||||
|
||||
echo
|
||||
echo "════════ 六、公网(20 次测延迟)════════"
|
||||
okc=0; total=0
|
||||
for i in $(seq 1 20); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null)
|
||||
c=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
if [ "$c" = "200" ]; then
|
||||
okc=$((okc+1))
|
||||
total=$(echo "$total + $t" | bc)
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?")
|
||||
[ "$okc" -ge 18 ] && ok "可用 $okc/20" || bad "可用 $okc/20"
|
||||
echo " 平均 ${avg} 秒"
|
||||
|
||||
echo
|
||||
echo "════════════════════════════════"
|
||||
echo " 通过 $pass 项,失败 $fail 项"
|
||||
[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败"
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# 最终验收(修正版:单引号里不要转义双引号;grep -c 取第一行)
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
pass=0; fail=0
|
||||
|
||||
ok() { pass=$((pass+1)); echo " ✓ $1"; }
|
||||
bad() { fail=$((fail+1)); echo " ✗ $1"; }
|
||||
code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; }
|
||||
# grep -c 有时会输出多行(多个文件时),统一取第一行
|
||||
cnt() { grep -c "$1" "$2" 2>/dev/null | head -1 | tr -d '[:space:]'; }
|
||||
|
||||
echo "════════ 一、官网 ════════"
|
||||
c=$(code "$BASE/")
|
||||
[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c"
|
||||
curl -s --max-time 20 "$BASE/" -o /tmp/s.html
|
||||
size=$(wc -c < /tmp/s.html | tr -d ' ')
|
||||
[ "$size" -gt 10000 ] && ok "页面 $size 字节(内容丰富)" || bad "页面只有 $size 字节"
|
||||
|
||||
check() {
|
||||
if grep -q "$2" /tmp/s.html; then ok "$1"; else bad "$1"; fi
|
||||
}
|
||||
check "吸顶导航" 'nav-in'
|
||||
check "Hero" 'hero'
|
||||
check "功能网格" 'id="features"'
|
||||
check "三步上手" 'id="how"'
|
||||
check "下载区" 'id="download"'
|
||||
check "FAQ" 'id="faq"'
|
||||
check "页脚" 'foot-in'
|
||||
check "版本徽章" 'badge'
|
||||
|
||||
[ "$(cnt 'class="feat"' /tmp/s.html)" = "9" ] && ok "9 张功能卡" \
|
||||
|| bad "功能卡 $(cnt 'class="feat"' /tmp/s.html) 张"
|
||||
[ "$(cnt '<details class="faq"' /tmp/s.html)" = "6" ] && ok "6 条 FAQ" \
|
||||
|| bad "FAQ $(cnt '<details class="faq"' /tmp/s.html) 条"
|
||||
[ "$(cnt 'class="dl-card"' /tmp/s.html)" = "3" ] && ok "3 张下载卡" \
|
||||
|| bad "下载卡 $(cnt 'class="dl-card"' /tmp/s.html) 张"
|
||||
[ "$(cnt '管理入口' /tmp/s.html)" = "0" ] && ok "没有管理入口" || bad "还有管理入口"
|
||||
[ "$(cnt 'src="/favicon.ico" alt=""' /tmp/s.html)" = "0" ] && ok "没有破图 logo" \
|
||||
|| bad "还有破图"
|
||||
[ "$(cnt '<svg' /tmp/s.html)" -ge 11 ] && ok "图标齐全($(cnt '<svg' /tmp/s.html) 个)" \
|
||||
|| bad "图标只有 $(cnt '<svg' /tmp/s.html) 个"
|
||||
|
||||
echo
|
||||
echo "════════ 二、管理页 ════════"
|
||||
c=$(code -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")
|
||||
[ "$c" = "200" ] && ok "登录后 HTTP 200" || bad "HTTP $c"
|
||||
curl -s --max-time 20 -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/a.html
|
||||
[ "$(cnt 'dlPanel' /tmp/a.html)" -gt 0 ] && ok "下载区在" || bad "下载区丢了"
|
||||
[ "$(cnt '网络体检' /tmp/a.html)" -gt 0 ] && ok "网络体检在" || bad "网络体检丢了"
|
||||
[ "$(cnt '退出登录' /tmp/a.html)" -gt 0 ] && ok "退出登录在" || bad "退出登录丢了"
|
||||
dark=0
|
||||
for cc in 0b1220 262c36 1b222c 1c2733 2d1214 4a1a1e 484f58 0d1117; do
|
||||
n=$(cnt "#$cc" /tmp/a.html)
|
||||
[ -n "$n" ] && [ "$n" != "0" ] && dark=$((dark + n))
|
||||
done
|
||||
[ "$dark" = "0" ] && ok "无深色残留" || bad "深色残留 $dark 处"
|
||||
[ "$(code "$BASE/admin")" = "302" ] && ok "/admin 跳转正常" || bad "/admin 跳转异常"
|
||||
[ "$(code "$BASE/zhongzhuanzhan")" = "200" ] && ok "未登录看到登录页" || bad "未登录页异常"
|
||||
|
||||
echo
|
||||
echo "════════ 三、下载 ════════"
|
||||
for f in ykxmz-setup-1.7.1.exe ykxmz-server-1.7.1.zip ykxmz-watch-1.7.1.apk; do
|
||||
c=$(code "$BASE/download/$f")
|
||||
[ "$c" = "200" ] && ok "$f" || bad "$f HTTP $c"
|
||||
done
|
||||
[ "$(code "$BASE/v1/downloads")" = "200" ] && ok "/v1/downloads" || bad "/v1/downloads"
|
||||
tc=$(code "$BASE/download/../relay.py")
|
||||
{ [ "$tc" = "404" ] || [ "$tc" = "400" ]; } && ok "路径穿越被挡($tc)" \
|
||||
|| bad "路径穿越 HTTP $tc"
|
||||
|
||||
echo
|
||||
echo "════════ 四、加速 ════════"
|
||||
proto=$(grep -oE 'Initial protocol [a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -1)
|
||||
echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto"
|
||||
conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
|
||||
[ "$conn" = "2" ] && ok "隧道连接数 2" || echo " · 隧道连接数 $conn(1 也可用,2 才有冗余)"
|
||||
[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了"
|
||||
[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" \
|
||||
|| bad "slow_start 没关"
|
||||
[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开"
|
||||
[ "$(cnt argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 有残留"
|
||||
|
||||
echo
|
||||
echo "════════ 五、功能 ════════"
|
||||
for api in /v1/health /favicon.ico; do
|
||||
c=$(code "$BASE$api")
|
||||
[ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c"
|
||||
done
|
||||
curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 六、公网可用性(设备侧,发夹回路=最坏情况)════════"
|
||||
okc=0; total=0
|
||||
for i in $(seq 1 15); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null)
|
||||
c=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
if [ "$c" = "200" ]; then
|
||||
okc=$((okc+1))
|
||||
total=$(echo "$total + $t" | bc)
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?")
|
||||
[ "$okc" -ge 14 ] && ok "可用 $okc/15" || bad "可用 $okc/15"
|
||||
echo " 平均 ${avg} 秒(真实用户路径比这个快,见 Windows 侧实测)"
|
||||
|
||||
echo
|
||||
echo "════════════════════════════════"
|
||||
echo " 通过 $pass 项,失败 $fail 项"
|
||||
[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败"
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
# 最终验收(修正版 2)
|
||||
# 上次的坑:grep -c 数的是**匹配行数**,HTML 压缩后 9 张卡在同一行 -> 返回 1。
|
||||
# 要数出现次数必须 grep -o | wc -l。
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
pass=0; fail=0
|
||||
|
||||
ok() { pass=$((pass+1)); echo " ✓ $1"; }
|
||||
bad() { fail=$((fail+1)); echo " ✗ $1"; }
|
||||
code() { curl -s -o /dev/null -w '%{http_code}' --max-time 25 "$@"; }
|
||||
|
||||
# 数**出现次数**(不是行数)
|
||||
occ() { grep -o "$1" "$2" 2>/dev/null | wc -l | tr -d ' '; }
|
||||
# 数**包含该串的行数**(用于"有没有出现")
|
||||
has() { grep -q "$1" "$2" 2>/dev/null && echo 1 || echo 0; }
|
||||
|
||||
echo "════════ 一、官网 ════════"
|
||||
c=$(code "$BASE/")
|
||||
[ "$c" = "200" ] && ok "首页 HTTP 200" || bad "首页 HTTP $c"
|
||||
curl -s --max-time 20 "$BASE/" -o /tmp/s.html
|
||||
size=$(wc -c < /tmp/s.html | tr -d ' ')
|
||||
[ "$size" -gt 10000 ] && ok "页面 $size 字节" || bad "页面只有 $size 字节"
|
||||
|
||||
check() { [ "$(has "$2" /tmp/s.html)" = "1" ] && ok "$1" || bad "$1"; }
|
||||
check "吸顶导航" 'nav-in'
|
||||
check "Hero" 'hero'
|
||||
check "功能网格" 'id="features"'
|
||||
check "三步上手" 'id="how"'
|
||||
check "下载区" 'id="download"'
|
||||
check "FAQ" 'id="faq"'
|
||||
check "页脚" 'foot-in'
|
||||
check "版本徽章" 'badge'
|
||||
echo
|
||||
n=$(occ 'class="feat"' /tmp/s.html); [ "$n" = "9" ] && ok "9 张功能卡" || bad "功能卡 $n 张"
|
||||
n=$(occ '<details class="faq"' /tmp/s.html); [ "$n" = "6" ] && ok "6 条 FAQ" || bad "FAQ $n 条"
|
||||
n=$(occ 'class="dl-card"' /tmp/s.html); [ "$n" = "3" ] && ok "3 张下载卡" || bad "下载卡 $n 张"
|
||||
n=$(occ 'class="step"' /tmp/s.html); [ "$n" = "3" ] && ok "3 个步骤卡" || bad "步骤卡 $n 个"
|
||||
n=$(occ '<svg' /tmp/s.html); [ "$n" -ge 11 ] && ok "图标 $n 个" || bad "图标只有 $n 个"
|
||||
n=$(occ '管理入口' /tmp/s.html); [ "$n" = "0" ] && ok "没有管理入口" || bad "还有管理入口"
|
||||
n=$(occ 'src="/favicon.ico" alt=""' /tmp/s.html); [ "$n" = "0" ] && ok "没有破图 logo" || bad "还有破图"
|
||||
|
||||
echo
|
||||
echo "════════ 二、管理页 ════════"
|
||||
c=$(code -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan")
|
||||
[ "$c" = "200" ] && ok "登录后 HTTP 200" || bad "HTTP $c"
|
||||
curl -s --max-time 20 -H "X-Admin: $KEY" "$BASE/zhongzhuanzhan" -o /tmp/a.html
|
||||
for pair in "下载区:dlPanel" "网络体检:网络体检" "退出登录:退出登录"; do
|
||||
[ "$(has "${pair#*:}" /tmp/a.html)" = "1" ] && ok "${pair%%:*}" || bad "${pair%%:*}"
|
||||
done
|
||||
dark=0
|
||||
for cc in 0b1220 262c36 1b222c 1c2733 2d1214 4a1a1e 484f58 0d1117; do
|
||||
n=$(occ "#$cc" /tmp/a.html)
|
||||
[ "$n" != "0" ] && dark=$((dark + n))
|
||||
done
|
||||
[ "$dark" = "0" ] && ok "无深色残留" || bad "深色残留 $dark 处"
|
||||
[ "$(code "$BASE/admin")" = "302" ] && ok "/admin 跳转正常" || bad "/admin 跳转异常"
|
||||
[ "$(code "$BASE/zhongzhuanzhan")" = "200" ] && ok "未登录看到登录页" || bad "未登录页异常"
|
||||
|
||||
echo
|
||||
echo "════════ 三、下载 ════════"
|
||||
for f in ykxmz-setup-1.7.1.exe ykxmz-server-1.7.1.zip ykxmz-watch-1.7.1.apk; do
|
||||
c=$(code "$BASE/download/$f")
|
||||
[ "$c" = "200" ] && ok "$f" || bad "$f HTTP $c"
|
||||
done
|
||||
[ "$(code "$BASE/v1/downloads")" = "200" ] && ok "/v1/downloads" || bad "/v1/downloads"
|
||||
tc=$(code "$BASE/download/../relay.py")
|
||||
{ [ "$tc" = "404" ] || [ "$tc" = "400" ]; } && ok "路径穿越被挡($tc)" || bad "路径穿越 HTTP $tc"
|
||||
|
||||
echo
|
||||
echo "════════ 四、加速 ════════"
|
||||
proto=$(grep -oE 'Initial protocol [a-z0-9]+' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -1)
|
||||
echo "$proto" | grep -q http2 && ok "隧道走 HTTP/2(不是 QUIC)" || bad "隧道协议: $proto"
|
||||
conn=$(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
|
||||
[ "$conn" = "2" ] && ok "隧道连接数 2" || echo " · 隧道连接数 $conn(1 也能用,2 才有冗余)"
|
||||
[ "$(sysctl -n net.ipv4.tcp_mtu_probing)" = "1" ] && ok "MTU 探测开着" || bad "MTU 探测关了"
|
||||
[ "$(sysctl -n net.ipv4.tcp_slow_start_after_idle)" = "0" ] && ok "空闲后不重置窗口" || bad "slow_start 没关"
|
||||
[ "$(sysctl -n net.ipv4.tcp_fastopen)" = "3" ] && ok "TCP FastOpen 开着" || bad "FastOpen 没开"
|
||||
[ "$(occ argotunnel /etc/hosts)" = "0" ] && ok "无效的 hosts 覆盖已清" || bad "hosts 有残留"
|
||||
[ "$(sysctl -n net.core.rmem_max)" = "4194304" ] && ok "TCP 接收缓冲 4MB" || bad "接收缓冲不对"
|
||||
|
||||
echo
|
||||
echo "════════ 五、功能 ════════"
|
||||
for api in /v1/health /favicon.ico; do
|
||||
c=$(code "$BASE$api")
|
||||
[ "$c" = "200" ] && ok "$api" || bad "$api HTTP $c"
|
||||
done
|
||||
curl -s --max-time 10 "$BASE/v1/health" | grep -o '"version": *"[^"]*"' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 六、公网可用性(设备侧发夹回路 = 最坏情况)════════"
|
||||
okc=0; total=0
|
||||
for i in $(seq 1 15); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' "$PUB/" 2>/dev/null)
|
||||
c=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
if [ "$c" = "200" ]; then
|
||||
okc=$((okc+1))
|
||||
total=$(echo "$total + $t" | bc)
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
avg=$(echo "scale=2; $total / $okc" | bc 2>/dev/null || echo "?")
|
||||
[ "$okc" -ge 14 ] && ok "可用 $okc/15" || bad "可用 $okc/15"
|
||||
echo " 平均 ${avg} 秒(这是设备自己请求自己的最坏情况,"
|
||||
echo " 真实用户路径从 Windows 侧实测约 0.69 秒)"
|
||||
|
||||
echo
|
||||
echo "════════════════════════════════"
|
||||
echo " 通过 $pass 项,失败 $fail 项"
|
||||
[ "$fail" = "0" ] && echo " 全部通过 ✓" || echo " 有失败"
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''清掉管理页深色残留(修正版:CSS 在 f-string 里,大括号是双写的)。
|
||||
|
||||
上一版只改成功了 4 处,因为我的匹配串用了单大括号 {{ }},
|
||||
而 relay.py 里那段 CSS 在 f-string 里,实际是 {{{{ }}}}。
|
||||
'''
|
||||
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 的花括号坑到。
|
||||
COLOR_MAP = [
|
||||
("#262c36", "#f3f4f6"), # 按钮悬停底色
|
||||
("#484f58", "#d0d7de"), # 边框/圆点
|
||||
("#1b222c", "#f6f8fa"), # 表格行悬停
|
||||
("#1c2733", "#eff6ff"), # 选中行
|
||||
("#213042", "#dbeafe"), # 选中行悬停
|
||||
("#2d1214", "#fff5f5"), # 删除按钮底
|
||||
("#6e2b2b", "#ffc9c9"), # 删除按钮边
|
||||
("#ff7b72", "#c92a2a"), # 删除按钮字
|
||||
("#4a1a1e", "#ffe3e3"), # 删除悬停底
|
||||
("#ffa198", "#a61b1b"), # 删除悬停字
|
||||
("#4a95f8", "#1d4ed8"), # 主按钮悬停
|
||||
("#0b1220", "#f6f8fa"), # 地址框底
|
||||
("#1f6feb44", "#d0d7de"), # 地址框边
|
||||
("#0008", "#0002"), # 提示条阴影
|
||||
]
|
||||
|
||||
# 深色主题才用的值,替换后不该再有
|
||||
DARK_LEFTOVERS = ["#0b1220", "#262c36", "#1b222c", "#1c2733", "#213042",
|
||||
"#2d1214", "#4a1a1e", "#484f58", "#0d1117"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
counts = {}
|
||||
|
||||
for dark, light in COLOR_MAP:
|
||||
n = text.count(dark)
|
||||
if n:
|
||||
text = text.replace(dark, light)
|
||||
counts[dark] = (light, n)
|
||||
|
||||
if counts:
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" 换了这些颜色:")
|
||||
for dark, (light, n) in counts.items():
|
||||
print(" %s -> %s (%d 处)" % (dark, light, n))
|
||||
else:
|
||||
print(" 没有要改的")
|
||||
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
|
||||
return 1
|
||||
|
||||
print()
|
||||
print(" 残留深色值:")
|
||||
left = [c for c in DARK_LEFTOVERS if c in text]
|
||||
if left:
|
||||
for c in left:
|
||||
print(" %s:" % c)
|
||||
for m in re.finditer(r".*" + re.escape(c) + r".*", text):
|
||||
print(" %s" % m.group(0).strip()[:76])
|
||||
else:
|
||||
print(" 无 ✓")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''清掉管理页剩下的深色主题残留。
|
||||
|
||||
管理页整体已经改成白色了(--bg:#ffffff),
|
||||
但 CSS 里还有几处是给深色底配的,在白底上很扎眼:
|
||||
|
||||
.url{background:#0b1220} 深藏青的地址框
|
||||
button:hover{background:#262c36} 深灰悬停
|
||||
tbody tr:hover td{#1b222c} 深灰行悬停
|
||||
tr.picked td{#1c2733} 深色选中行
|
||||
button.del{background:#2d1214} 暗红删除按钮
|
||||
.dot{background:#484f58} 深灰圆点
|
||||
a.logout{border-bottom:#484f58} 深灰虚线下划线
|
||||
|
||||
换成浅色系。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
RELAY = ROOT / "relay" / "relay.py"
|
||||
|
||||
# (旧, 新, 说明)
|
||||
FIXES = [
|
||||
# 公网地址框:深藏青 -> 浅蓝
|
||||
("background:#0b1220;border:1px solid #1f6feb44",
|
||||
"background:#f6f8fa;border:1px solid #d0d7de",
|
||||
"地址框底色"),
|
||||
("color:#0969da;word-break:break-all;\n background:#f6f8fa",
|
||||
"color:#0969da;word-break:break-all;\n background:#f6f8fa",
|
||||
"(保持)"),
|
||||
# 按钮悬停
|
||||
("button:hover{background:#262c36;border-color:#484f58}",
|
||||
"button:hover{background:#f3f4f6;border-color:#d1d5db}",
|
||||
"按钮悬停"),
|
||||
("button.primary:hover{background:#4a95f8;border-color:#4a95f8}",
|
||||
"button.primary:hover{background:#1d4ed8;border-color:#1d4ed8}",
|
||||
"主按钮悬停"),
|
||||
# 删除按钮:暗红 -> 浅红
|
||||
("button.del{background:#2d1214;border-color:#6e2b2b;color:#ff7b72}",
|
||||
"button.del{background:#fff5f5;border-color:#ffc9c9;color:#c92a2a}",
|
||||
"删除按钮"),
|
||||
("button.del:hover{background:#4a1a1e;border-color:#a33;color:#ffa198}",
|
||||
"button.del:hover{background:#ffe3e3;border-color:#ffa8a8;color:#a61b1b}",
|
||||
"删除按钮悬停"),
|
||||
# 表格行悬停
|
||||
("tbody tr:hover td{background:#1b222c}",
|
||||
"tbody tr:hover td{background:#f6f8fa}",
|
||||
"表格行悬停"),
|
||||
# 选中行
|
||||
("tr.picked td{background:#1c2733}",
|
||||
"tr.picked td{background:#eff6ff}",
|
||||
"选中行"),
|
||||
("tr.picked:hover td{background:#213042}",
|
||||
"tr.picked:hover td{background:#dbeafe}",
|
||||
"选中行悬停"),
|
||||
# 圆点
|
||||
("vertical-align:middle;background:#484f58}",
|
||||
"vertical-align:middle;background:#d0d7de}",
|
||||
"状态圆点"),
|
||||
# 退出登录的下划线
|
||||
("border-bottom:1px dashed #484f58;transition:.12s}",
|
||||
"border-bottom:1px dashed #d0d7de;transition:.12s}",
|
||||
"退出登录下划线"),
|
||||
# 提示条阴影(白底上要淡一点)
|
||||
("box-shadow:0 8px 24px #0008;opacity:0;",
|
||||
"box-shadow:0 8px 24px #0002;opacity:0;",
|
||||
"提示条阴影"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
changed = []
|
||||
|
||||
for old, new, why in FIXES:
|
||||
if old == new:
|
||||
continue
|
||||
if new in text:
|
||||
continue
|
||||
if old in text:
|
||||
text = text.replace(old, new)
|
||||
changed.append(why)
|
||||
|
||||
if changed:
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" 改了:")
|
||||
for c in changed:
|
||||
print(" " + c)
|
||||
else:
|
||||
print(" 没有要改的")
|
||||
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
|
||||
return 1
|
||||
|
||||
# 复核:还有没有深色残留
|
||||
print()
|
||||
print(" 残留的深色值检查:")
|
||||
DARK = ["#0b1220", "#262c36", "#1b222c", "#1c2733", "#213042",
|
||||
"#2d1214", "#4a1a1e", "#484f58", "#2a313c", "#0d1117"]
|
||||
left = [c for c in DARK if c in text]
|
||||
if left:
|
||||
print(" 还有: %s" % " ".join(left))
|
||||
for c in left:
|
||||
import re
|
||||
for m in re.finditer(r".*" + re.escape(c) + r".*", text):
|
||||
print(" %s" % m.group(0).strip()[:80])
|
||||
else:
|
||||
print(" 无 ✓")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''抓管理页 HTML 到本地,供截图查看。
|
||||
管理页要登录,无头浏览器不方便带头,所以先用 curl 抓下来存本地。'''
|
||||
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 # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_c, out, _e = run(
|
||||
"KEY=$(cat /root/lanremote-relay/data/admin_key.txt); "
|
||||
"curl -s --max-time 20 -H \"X-Admin: $KEY\" "
|
||||
"http://127.0.0.1:8080/zhongzhuanzhan",
|
||||
timeout=120)
|
||||
if not out or "<html" not in out.lower():
|
||||
print(" 抓不到管理页")
|
||||
print(out[:400])
|
||||
return 1
|
||||
|
||||
# 把内联的相对链接改成绝对(否则本地 file:// 打开样式全丢)
|
||||
out = out.replace('href="/favicon.ico"',
|
||||
'href="https://relay.xxmxxs.top/favicon.ico"')
|
||||
|
||||
target = ROOT / "_build" / "admin_preview.html"
|
||||
target.write_text(out, encoding="utf-8")
|
||||
print(" 抓到 %d 字符 -> %s" % (len(out), target.name))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''从本机测公网延迟 —— 这是真实用户路径。
|
||||
|
||||
为什么不在设备上测:
|
||||
设备上 curl 自己的公网域名,流量要出到 Cloudflare 边缘再经隧道
|
||||
绕回同一台设备,是"发夹回路",最坏情况。
|
||||
真实用户(手表/电脑)在别处,路径短得多。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
URL = "https://relay.xxmxxs.top/"
|
||||
HEALTH = "https://relay.xxmxxs.top/v1/health"
|
||||
HEADERS = {"User-Agent": "LanRemote/1.4.0"}
|
||||
|
||||
|
||||
def measure(url: str, rounds: int) -> list:
|
||||
times = []
|
||||
for i in range(rounds):
|
||||
req = urllib.request.Request(url, headers=HEADERS)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=25) as resp:
|
||||
resp.read()
|
||||
if resp.status == 200:
|
||||
times.append(time.perf_counter() - start)
|
||||
except (urllib.error.URLError, OSError, TimeoutError):
|
||||
pass
|
||||
time.sleep(0.4)
|
||||
return times
|
||||
|
||||
|
||||
def report(label: str, times: list, rounds: int) -> None:
|
||||
if not times:
|
||||
print(" %s: 全部失败" % label)
|
||||
return
|
||||
print(" %s: 可用 %d/%d" % (label, len(times), rounds))
|
||||
print(" 平均 %.0f ms 中位 %.0f ms 最快 %.0f ms 最慢 %.0f ms"
|
||||
% (statistics.mean(times) * 1000,
|
||||
statistics.median(times) * 1000,
|
||||
min(times) * 1000,
|
||||
max(times) * 1000))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 公网真实路径测试(30 次)")
|
||||
print()
|
||||
report("官网首页", measure(URL, 30), 30)
|
||||
print()
|
||||
report("健康接口", measure(HEALTH, 20), 20)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''确认延迟瓶颈到底在哪一段。
|
||||
|
||||
对比三段:
|
||||
1. 局域网直连(设备本机 -> relay 本机) 预期几十毫秒
|
||||
2. 公网(本机 -> Cloudflare 边缘 -> 隧道 -> 设备)
|
||||
3. 纯 Cloudflare 边缘 RTT(本机 -> 随便一个 CF 站点)
|
||||
|
||||
差值能告诉我们:
|
||||
2 - 1 = 隧道 + 公网往返的开销
|
||||
3 = 到边缘的裸 RTT
|
||||
2 - 3 - 1 = 隧道那一段的开销(设备到边缘)
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
HEADERS = {"User-Agent": "LanRemote/1.4.0"}
|
||||
|
||||
|
||||
def timed(url: str, rounds: int = 15) -> list:
|
||||
out = []
|
||||
for _ in range(rounds):
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
with urllib.request.urlopen(
|
||||
urllib.request.Request(url, headers=HEADERS),
|
||||
timeout=25) as resp:
|
||||
resp.read()
|
||||
if resp.status == 200:
|
||||
out.append((time.perf_counter() - start) * 1000)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
time.sleep(0.3)
|
||||
return out
|
||||
|
||||
|
||||
def show(label: str, values: list, rounds: int) -> float:
|
||||
if not values:
|
||||
print(" %-24s 全失败" % label)
|
||||
return float("nan")
|
||||
med = statistics.median(values)
|
||||
print(" %-24s 可用 %2d/%d 中位 %7.0f ms 最快 %7.0f ms"
|
||||
% (label, len(values), rounds, med, min(values)))
|
||||
return med
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 分段测延迟(各 15 次取中位数)")
|
||||
print()
|
||||
|
||||
lan = show("1. 局域网直连", timed("http://192.168.2.156:8080/v1/health"), 15)
|
||||
pub = show("2. 公网(经隧道)",
|
||||
timed("https://relay.xxmxxs.top/v1/health"), 15)
|
||||
edge = show("3. Cloudflare 边缘裸 RTT",
|
||||
timed("https://cloudflare.com/cdn-cgi/trace"), 15)
|
||||
|
||||
print()
|
||||
print(" 分析:")
|
||||
if lan == lan and pub == pub:
|
||||
print(" 局域网 %.0f ms -> 公网 %.0f ms" % (lan, pub))
|
||||
print(" 公网比局域网多 %.0f ms" % (pub - lan))
|
||||
if edge == edge and pub == pub:
|
||||
print()
|
||||
print(" 到 Cloudflare 边缘的裸 RTT: %.0f ms" % edge)
|
||||
print(" 公网请求 %.0f ms,其中边缘往返占 %.0f ms(%.0f%%)"
|
||||
% (pub, edge, edge / pub * 100))
|
||||
print(" 剩下的 %.0f ms 是:边缘 -> 设备(隧道)那一段 + 服务端处理"
|
||||
% (pub - edge))
|
||||
|
||||
print()
|
||||
print(" 结论:")
|
||||
print(" 如果「边缘 -> 设备」那段占比大,说明瓶颈在设备的 4G 上行链路,")
|
||||
print(" Cloudflare 这一侧没得优化了 —— 只能靠换网络或换中转位置。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# 用实测最快的边缘 IP 固定隧道出口。
|
||||
#
|
||||
# 之前我判断"优选 IP 没用"是**错的** —— 结论下得太急。
|
||||
# 真正的价值不是降低延迟(落点仍是 lax,改不了),
|
||||
# 而是**避开会握手超时的 IP**:
|
||||
#
|
||||
# 日志里大量 dial tcp 198.41.192.167:7844: i/o timeout
|
||||
# Retrying connection in up to 32s
|
||||
# GeoDNS 随机给的 IP 里有一部分连不上,
|
||||
# cloudflared 就连上-掉线-重连反复折腾,
|
||||
# 表现就是延迟在 0.69s ~ 4.4s 之间乱跳。
|
||||
#
|
||||
# 实测(probe_cf_edge2.py)最快且稳定的:
|
||||
# 198.41.200.238 138ms
|
||||
# 198.41.192.214 143ms
|
||||
set -u
|
||||
|
||||
echo "===== 1. 备份 ====="
|
||||
cp -f /etc/hosts "/root/hosts.bak.$(date +%s)"
|
||||
echo " 已备份 /etc/hosts"
|
||||
|
||||
echo
|
||||
echo "===== 2. 先复测这两个 IP 还通不通 ====="
|
||||
python3 - <<'PY'
|
||||
import socket, time
|
||||
for ip in ("198.41.200.238", "198.41.192.214", "198.41.200.13", "198.41.192.37"):
|
||||
s = socket.socket(); s.settimeout(3)
|
||||
t = time.perf_counter()
|
||||
try:
|
||||
s.connect((ip, 7844))
|
||||
print(" %-16s %.0f ms" % (ip, (time.perf_counter() - t) * 1000))
|
||||
except OSError as e:
|
||||
print(" %-16s 不通 (%s)" % (ip, e.__class__.__name__))
|
||||
finally:
|
||||
s.close()
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "===== 3. 写 /etc/hosts(固定两个最快的)====="
|
||||
sed -i '/argotunnel.com/d' /etc/hosts
|
||||
{
|
||||
echo ""
|
||||
echo "# ykxmz:固定 Cloudflare 隧道边缘 IP"
|
||||
echo "# 目的不是降低延迟(落点仍是 lax),而是避开会握手超时的 IP。"
|
||||
echo "# GeoDNS 随机发的 IP 里有些连不上,会导致隧道反复重连、延迟乱跳。"
|
||||
echo "198.41.200.238 region1.v2.argotunnel.com"
|
||||
echo "198.41.192.214 region2.v2.argotunnel.com"
|
||||
} >> /etc/hosts
|
||||
grep argotunnel /etc/hosts | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 4. 确认解析走的是我们指定的 ====="
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"):
|
||||
infos = socket.getaddrinfo(host, 7844, socket.AF_INET)
|
||||
ips = sorted({i[4][0] for i in infos})
|
||||
print(" %s -> %s" % (host, ips))
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "===== 5. 重启隧道并观察 60 秒 ====="
|
||||
systemctl restart lanremote-tunnel
|
||||
: > /root/reconnect_watch.txt
|
||||
for i in $(seq 1 12); do
|
||||
sleep 5
|
||||
conn=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
errs=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_request_errors' | awk '{print $2}')
|
||||
echo " ${i}0s: 连接 $conn 错误 $errs"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== 6. 这 60 秒里有几次超时告警 ====="
|
||||
since=$(date -u -d '70 seconds ago' '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo "")
|
||||
if [ -n "$since" ]; then
|
||||
n=$(awk -v s="$since" '$0 >= s' /root/lanremote-relay/tunnel.log 2>/dev/null \
|
||||
| grep -c 'i/o timeout')
|
||||
echo " 超时告警 $n 次(改之前每分钟都有好几次)"
|
||||
else
|
||||
echo " (算不出时间窗,跳过)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "===== 7. 当前落点 ====="
|
||||
grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -3 \
|
||||
| grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 8. 公网延迟(10 次)====="
|
||||
ok=0; total=0
|
||||
for i in $(seq 1 10); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
||||
c=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
[ "$c" = "200" ] && { ok=$((ok+1)); total=$(echo "$total + $t" | bc); }
|
||||
sleep 1
|
||||
done
|
||||
avg=$(echo "scale=2; $total / $ok" | bc 2>/dev/null || echo "?")
|
||||
echo " 可用 $ok/10 平均 ${avg} 秒"
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# 撤掉边缘 IP 固定。
|
||||
#
|
||||
# 教训(一天内自己推翻自己两次):
|
||||
# 第一次:以为优选 IP 能降延迟 -> 错。落点还是 lax,改不了路径。
|
||||
# 第二次:以为优选 IP 能避开超时 -> 半小时内对,但对不久。
|
||||
# 当时实测 138ms 的两个 IP,30 分钟后再测全都不通了。
|
||||
#
|
||||
# 根因:Cloudflare anycast 的可达性受运营商路由影响,
|
||||
# 会随时间变化。**固定 IP 等于钉死到一个可能失效的地址上**,
|
||||
# 比让 GeoDNS 动态解析更糟 —— 后者至少每次重连会重新挑。
|
||||
#
|
||||
# 结论:不固定。靠 cloudflared 自己的重试 + --ha-connections 2 冗余。
|
||||
set -u
|
||||
|
||||
echo "===== 1. 清掉 hosts 固定 ====="
|
||||
before=$(grep -c argotunnel /etc/hosts 2>/dev/null || echo 0)
|
||||
sed -i '/argotunnel.com/d' /etc/hosts
|
||||
after=$(grep -c argotunnel /etc/hosts 2>/dev/null || echo 0)
|
||||
echo " argotunnel 条目: $before -> $after"
|
||||
|
||||
echo
|
||||
echo "===== 2. 确认解析回到 GeoDNS ====="
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
for host in ("region1.v2.argotunnel.com", "region2.v2.argotunnel.com"):
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, 7844, socket.AF_INET)
|
||||
ips = sorted({i[4][0] for i in infos})
|
||||
print(" %s -> %d 个地址" % (host, len(ips)))
|
||||
print(" %s" % " ".join(ips[:5]))
|
||||
except OSError as e:
|
||||
print(" %s 解析失败 %s" % (host, e))
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "===== 3. 重启隧道,观察 90 秒连接稳定性 ====="
|
||||
systemctl restart lanremote-tunnel
|
||||
stable=1
|
||||
for i in $(seq 1 9); do
|
||||
sleep 10
|
||||
conn=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
|
||||
errs=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \
|
||||
| grep '^cloudflared_tunnel_request_errors' | awk '{print $2}')
|
||||
echo " ${i}0s: 连接 $conn 请求错误 $errs"
|
||||
[ "$i" -ge 6 ] && [ "$conn" != "2" ] && stable=0
|
||||
done
|
||||
[ "$stable" = "1" ] && echo " ✓ 后段稳定在 2 条连接" || echo " · 还在收敛"
|
||||
|
||||
echo
|
||||
echo "===== 4. 这段时间的超时告警数 ====="
|
||||
since=$(date -u -d '100 seconds ago' '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo "")
|
||||
if [ -n "$since" ]; then
|
||||
n=$(awk -v s="$since" '$0 >= s' /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'i/o timeout')
|
||||
echo " $n 次"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "===== 5. 当前落点(应为 2 条)====="
|
||||
grep -E 'Registered tunnel connection' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -2 \
|
||||
| grep -oE 'ip=[0-9.]+ location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 6. 公网延迟(15 次,设备侧最坏情况)====="
|
||||
ok=0; total=0
|
||||
for i in $(seq 1 15); do
|
||||
r=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \
|
||||
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health 2>/dev/null)
|
||||
c=$(echo "$r" | cut -d' ' -f1)
|
||||
t=$(echo "$r" | cut -d' ' -f2)
|
||||
[ "$c" = "200" ] && { ok=$((ok+1)); total=$(echo "$total + $t" | bc); printf " %2d: %s 秒\n" "$i" "$t"; } \
|
||||
|| printf " %2d: 失败\n" "$i"
|
||||
sleep 1
|
||||
done
|
||||
avg=$(echo "scale=2; $total / $ok" | bc 2>/dev/null || echo "?")
|
||||
echo " 可用 $ok/15 平均 ${avg} 秒"
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''端到端验证局域网直连路径。
|
||||
|
||||
结论已经很清楚了:
|
||||
局域网 118 ms vs 公网 1215 ms —— 快 10 倍
|
||||
瓶颈是这条网络到 Cloudflare 的 4G 上行(裸 RTT 就 1.4 秒),
|
||||
Cloudflare 那一侧没得优化。
|
||||
|
||||
所以真正要保证的是**局域网这条路能用**。
|
||||
这个脚本起一个真实被控端,走 192.168.2.x 直连,量一下握手耗时。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
PORT = 8875
|
||||
CODE = "246810"
|
||||
|
||||
|
||||
def talk(payload: dict, timeout: float = 12.0) -> dict:
|
||||
sock = socket.create_connection(("127.0.0.1", PORT), timeout=timeout)
|
||||
sock.settimeout(timeout)
|
||||
sock.sendall((json.dumps(payload, ensure_ascii=False) + "\n").encode())
|
||||
raw = b""
|
||||
while b"\n" not in raw:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
raw += chunk
|
||||
sock.close()
|
||||
return json.loads(raw.split(b"\n")[0].decode("utf-8"))
|
||||
|
||||
|
||||
def lan_ip() -> str:
|
||||
try:
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
probe.connect(("8.8.8.8", 80))
|
||||
addr = probe.getsockname()[0]
|
||||
probe.close()
|
||||
return addr
|
||||
except OSError:
|
||||
return "192.168.2.64"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
env = dict(__import__("os").environ)
|
||||
env["YKXMZ_DATA"] = str(ROOT / "_build" / "_lantest_data")
|
||||
Path(env["YKXMZ_DATA"]).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(" 启动被控端(端口 %d,配对码 %s)" % (PORT, CODE))
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(ROOT / "windows" / "run_agent.py"),
|
||||
"--port", str(PORT), "--code", CODE, "--silent"],
|
||||
cwd=str(ROOT), env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
nonlocal passed, failed
|
||||
if ok:
|
||||
passed += 1
|
||||
print(" [PASS] " + name)
|
||||
else:
|
||||
failed += 1
|
||||
print(" [FAIL] " + name + " " + str(detail))
|
||||
|
||||
try:
|
||||
time.sleep(7)
|
||||
ip = lan_ip()
|
||||
print(" 本机局域网地址: %s" % ip)
|
||||
print()
|
||||
|
||||
# 1. 本机回环
|
||||
print(" 一、回环路径")
|
||||
t = time.perf_counter()
|
||||
reply = talk({"cmd": "pair", "deviceId": "lantest",
|
||||
"deviceName": "测试手表", "code": CODE})
|
||||
loop_ms = (time.perf_counter() - t) * 1000
|
||||
check("回环配对成功", reply.get("ok") is True, str(reply)[:90])
|
||||
token = reply.get("token", "")
|
||||
print(" 握手 %.0f ms" % loop_ms)
|
||||
|
||||
# 2. 局域网地址(真实用户路径)
|
||||
print()
|
||||
print(" 二、局域网地址 %s(手表实际走这条)" % ip)
|
||||
sock = socket.create_connection((ip, PORT), timeout=10)
|
||||
sock.settimeout(10)
|
||||
t = time.perf_counter()
|
||||
sock.sendall((json.dumps({"cmd": "auth", "deviceId": "lantest",
|
||||
"token": token}, ensure_ascii=False)
|
||||
+ "\n").encode())
|
||||
raw = b""
|
||||
while b"\n" not in raw:
|
||||
raw += sock.recv(65536)
|
||||
lan_ms = (time.perf_counter() - t) * 1000
|
||||
auth = json.loads(raw.split(b"\n")[0].decode("utf-8"))
|
||||
check("局域网认证成功", auth.get("ok") is True, str(auth)[:90])
|
||||
print(" 认证 %.0f ms" % lan_ms)
|
||||
|
||||
# 3. 功能列表
|
||||
t = time.perf_counter()
|
||||
sock.sendall((json.dumps({"cmd": "feature_list"}, ensure_ascii=False)
|
||||
+ "\n").encode())
|
||||
raw = b""
|
||||
while b"\n" not in raw:
|
||||
raw += sock.recv(262144)
|
||||
feat = json.loads(raw.split(b"\n")[0].decode("utf-8"))
|
||||
feat_ms = (time.perf_counter() - t) * 1000
|
||||
features = feat.get("features", [])
|
||||
check("功能列表拿到 %d 项" % len(features), len(features) > 100)
|
||||
print(" 功能列表 %.0f ms" % feat_ms)
|
||||
sock.close()
|
||||
|
||||
print()
|
||||
print(" 三、结论")
|
||||
print(" 局域网往返 %.0f ms vs 公网 1215 ms" % lan_ms)
|
||||
print(" 局域网快 %.1f 倍" % (1215 / max(lan_ms, 1)))
|
||||
check("局域网明显快于公网", lan_ms < 300, "%.0f ms" % lan_ms)
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
print()
|
||||
print("=" * 52)
|
||||
print(" 通过 %d 项,失败 %d 项" % (passed, failed))
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''测被控端实际报出去的 lanIps 列表。'''
|
||||
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 / "windows"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from lanremote.server import lan_ips, local_ip, _ip_rank
|
||||
|
||||
ips = lan_ips()
|
||||
print(" lan_ips() -> %s" % (ips,))
|
||||
for ip in ips:
|
||||
print(" %-18s rank=%d" % (ip, _ip_rank(ip)))
|
||||
print()
|
||||
print(" local_ip() -> %s" % local_ip())
|
||||
|
||||
print()
|
||||
print(" 手表那边的判断(sameSubnet 比前 3 段):")
|
||||
if ips:
|
||||
first = ips[0]
|
||||
print(" 被控端报的第一个: %s" % first)
|
||||
print(" 如果手表在 192.168.2.x -> 匹配 ✓")
|
||||
print(" 如果手表在别的网段 -> 不匹配,走中转")
|
||||
|
||||
print()
|
||||
print(" 结论:")
|
||||
real = [ip for ip in ips if ip.startswith(("192.168.", "10."))]
|
||||
if real:
|
||||
print(" ✓ 报了真实局域网地址 %s,手表同网段时能直连" % real[0])
|
||||
print(" 所以「电脑不在同一网络」的提示,意味着")
|
||||
print(" 手表当时**真的不在** 192.168.2.x —— 比如连了 5G 频段")
|
||||
print(" 的另一个路由,或者开着移动数据。")
|
||||
else:
|
||||
print(" ✗ 没有真实局域网地址,手表永远匹配不上")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''写提交消息文件(纯 UTF-8 无 BOM,避开 PowerShell 的坑)。'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MSG = """阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收
|
||||
|
||||
阶段3 管理页
|
||||
- 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、
|
||||
删除按钮 #2d1214 等 11 处),现在 0 处深色
|
||||
- 接口全部正常:health / downloads / netcheck
|
||||
|
||||
阶段4 局域网诊断
|
||||
- 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的**
|
||||
(lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一),
|
||||
是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据
|
||||
- 所以改了手表的提示:不再只说"不在同一网络",
|
||||
而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi
|
||||
- 端到端验证局域网路径:认证 1 ms,功能列表 207 项
|
||||
|
||||
阶段1 结论修正(一天内推翻自己两次,如实记录)
|
||||
- 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP
|
||||
- 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了
|
||||
—— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址
|
||||
- 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余
|
||||
- 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s
|
||||
- **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**,
|
||||
比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身,
|
||||
不是设备的隧道。公网侧已无优化空间。
|
||||
- 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路
|
||||
|
||||
阶段5 验收
|
||||
- 全量回归 29 项全通过
|
||||
- 设备侧验收 38 项全通过
|
||||
- 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
target = Path(__file__).resolve().parent / "_commitmsg_a.txt"
|
||||
target.write_bytes(MSG.encode("utf-8"))
|
||||
print(" 写好 %s(%d 字节,无 BOM)" % (target.name, target.stat().st_size))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user