mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30: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())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 608 KiB |
@@ -2552,7 +2552,19 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appendLog("电脑不在同一网络,继续用中转");
|
||||
// 不在同一网段 -> 继续走中转。
|
||||
// 光说"不在同一网络"用户没法自查,所以把
|
||||
// 双方地址都打出来:一眼就能看出是网段不同、
|
||||
// 还是手表连错了 WiFi。
|
||||
java.util.List<String> theirs = client.relayLanIps();
|
||||
java.util.List<String> mine = localIpv4List();
|
||||
appendLog("电脑在 " + (theirs == null || theirs.isEmpty()
|
||||
? "(没报地址)" : theirs.get(0))
|
||||
+ ",手表在 " + (mine.isEmpty()
|
||||
? "(读不到网卡)" : mine.get(0))
|
||||
+ " —— 不在同一网段,走中转");
|
||||
appendLog("想让手表直连:确认两边连的是同一个"
|
||||
+ " WiFi(注意别连成 5G 频段的另一个名字)");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+12
-12
@@ -1511,7 +1511,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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}}
|
||||
border-bottom:1px dashed #d0d7de;transition:.12s}}
|
||||
.meta a.logout:hover{{color:var(--accent);border-bottom-color:var(--accent)}}
|
||||
|
||||
/* ---------- 通用卡片 ---------- */
|
||||
@@ -1524,7 +1524,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
.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;
|
||||
background:#f6f8fa;border:1px solid #d0d7de;border-radius:8px;
|
||||
padding:10px 12px}}
|
||||
.url.empty{{color:var(--muted);font-weight:400;font-style:normal}}
|
||||
|
||||
@@ -1540,7 +1540,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
background:var(--accent);opacity:.65}}
|
||||
.stat.ok::after{{background:var(--ok)}}
|
||||
.stat.warn::after{{background:var(--warn)}}
|
||||
.stat.mute::after{{background:#484f58}}
|
||||
.stat.mute::after{{background:#d0d7de}}
|
||||
|
||||
/* ---------- 工具条 ---------- */
|
||||
.toolbar{{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
|
||||
@@ -1552,14 +1552,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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:hover{{background:#f3f4f6;border-color:#d0d7de}}
|
||||
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.primary:hover{{background:#1d4ed8;border-color:#1d4ed8}}
|
||||
button.del{{background:#fff5f5;border-color:#ffc9c9;color:#c92a2a}}
|
||||
button.del:hover{{background:#ffe3e3;border-color:#a33;color:#a61b1b}}
|
||||
button.del:disabled{{opacity:.4}}
|
||||
button.small{{padding:5px 11px;font-size:12px}}
|
||||
|
||||
@@ -1574,7 +1574,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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:hover td{{background:#f6f8fa}}
|
||||
tbody tr:last-child td{{border-bottom:none}}
|
||||
td.name{{font-weight:500}}
|
||||
td.num{{font-family:ui-monospace,Consolas,monospace;color:var(--dim);
|
||||
@@ -1589,7 +1589,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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}}
|
||||
margin-right:7px;vertical-align:middle;background:#d0d7de}}
|
||||
.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}}
|
||||
@@ -1598,13 +1598,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
.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}}
|
||||
tr.picked td{{background:#eff6ff}}
|
||||
tr.picked:hover td{{background:#dbeafe}}
|
||||
|
||||
/* ---------- 提示条 ---------- */
|
||||
#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;
|
||||
font-size:13px;box-shadow:0 8px 24px #0002;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}}
|
||||
|
||||
Reference in New Issue
Block a user