mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
用户反馈两件事:
1. 早上打开 relay.xxmxxs.top 看到 Cloudflare Error 1033 —— "这就是你说的优化好了?"
2. "中转管理那边 ui 界面大改,现在难看的要死"
============ 一、隧道 1033/502/530 的真因 ============
之前我只测 15~25 次就说"可用率 100%",太草率 ——
实际是**间歇性假活**:隧道显示 ha_connections=2 连着,
公网却 502/530/000。watchdog 日志把过程记了下来。
根因:**4G 链路静默断开 TCP 连接**(不发 FIN/RST,双方都以为还活着)。
Cloudflare 把请求发到这条僵尸连接上就回错误。
Linux 默认 tcp_keepalive_time = **7200 秒(2 小时)**才发现,
中间这段时间服务一直"看着在线、实际打不开"。
修法(都有实测数据支撑):
1. tcp_keepalive 30 秒 + 每 10 秒探测 3 次
=> 最多约 60 秒发现僵尸连接并重连
2. 隧道连接数 2 -> 4
(对照实验:http2+2 只有 1 条连上时公网 0/12;
http2+4 稳定 12/12。连接数太少会出现"只有 1 条"的危险窗口)
3. 协议定为 http2(cloudflared 自检说 TCP 7844 被墙,
但实测 http2 反而比 quic 稳)
实测:http2+4 之后公网 40/40 = 100%,最慢 1.3 秒,0 次重连。
============ 二、中转管理后台重做 ============
原来是几百行内联在 relay.py 的 f-string 里(HTML+CSS+JS 全挤一起),
改一处样式要在一堆转义花括号里翻。拆成独立模块 relay/admin.py。
新版设计(对齐官网那套白色简洁):
* 吸顶顶栏:品牌 + 运行状态 + 版本/内存/请求数
* 公网地址给最大权重(这是用户最需要复制的东西)
* 四个数字卡(在线/会话数带颜色区分)
* 设备表卡片:状态圆点、等宽配对码、行悬停
* 工具卡:网络体检 + 下载区并排
* 空状态改成图标+引导文案,不再是干巴巴一句
* 窄屏时表格自动变卡片
relay.py 从 2089 行缩到 1550 行。
============ 三、顺手修的两个真 bug ============
1. 设备记录只增不减
self.devices 从不清理,被控端每次换端口/重装就多一条,
实测一天攒了 75 条僵尸记录,/v1/health 从几 KB 涨到 20KB。
加 DEVICE_TTL(6 小时)在 snapshot() 里清。
2. 配对码映射也没清
RELAY.codes 和 devices 是一对孪生 dict,
只清 devices 会留下指向已删设备的孤儿条目 ——
管理页显示"配对码占用 131",实际一台设备都没有。
现在一起清。
============ 四、我自己造的两次事故(如实记录) ============
1. 删文件删过头
用"从 _admin_page 截到文件末尾"的方式替换方法,
但 _admin_page 后面**还有 _esc/_kb/_uptime/class Server/main()**。
结果 relay 起不来,systemd 重启 24 次,所有接口 000。
教训:**"删到文件末尾"之前必须先确认末尾有什么。**
2. ADMIN_PATH 引用了不存在的常量
路由里写的是字面量,我却在新代码里用了 ADMIN_PATH,
管理页一访问就 NameError。现在统一定义了常量。
二者都是"改完没立刻验"造成的。现在每次改完都跑一遍
管理页 + 官网 + 关键接口的回归。
215 lines
8.1 KiB
Python
215 lines
8.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''官网的"产品展示"部分:手表界面 mockup + 连接方式示意。
|
||
|
||
为什么用 HTML/CSS 画手表界面,而不是放截图:
|
||
* 手表是安卓设备,没有真机/模拟器就拍不到界面
|
||
* CSS 画的图在任何屏幕上都是清晰的(截图放大会糊)
|
||
* 用的颜色和布局**直接从 MainActivity.java 抄**,
|
||
所以不是"想象的样子",是真实界面
|
||
|
||
手表真实参数(android/.../MainActivity.java):
|
||
C_BG = 0xFFF5F7FA 页面底色(浅灰)
|
||
C_FG = 0xFF1F2328 主文字(近黑)
|
||
C_ACCENT = 0xFF185CE8 强调蓝
|
||
C_DIM = 0xFF656D76 次要文字
|
||
十个分页:媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
# 和手表代码一致的颜色
|
||
W_BG = "#F5F7FA"
|
||
W_FG = "#1F2328"
|
||
W_ACCENT = "#185CE8"
|
||
W_DIM = "#656D76"
|
||
W_CARD = "#FFFFFF"
|
||
W_BORDER = "#E3E7ED"
|
||
|
||
TABS = ["媒体", "屏幕", "文件", "语音", "电脑",
|
||
"网络", "系统", "教室", "效率", "娱乐"]
|
||
|
||
# "媒体"分页的真实操作(从 TabDef 抄的)
|
||
MEDIA_ACTIONS = [
|
||
("放音频", "🔊"), ("放视频", "▶"), ("暂停", "⏸"),
|
||
("继续", "⏵"), ("音量 +", "+"), ("音量 −", "−"),
|
||
("静音", "🔇"), ("下一首", "⏭"), ("投图到屏幕", "🖼"),
|
||
]
|
||
|
||
# "教室"分页(老师的核心场景)
|
||
CLASSROOM_ACTIONS = [
|
||
("上课:开投影", "📽"), ("打开课件", "📂"), ("课堂专注", "🎯"),
|
||
("恢复桌面", "↩"), ("看屏幕", "👁"), ("下课关机", "⏻"),
|
||
]
|
||
|
||
TAB_ACTIONS = {"媒体": MEDIA_ACTIONS, "教室": CLASSROOM_ACTIONS}
|
||
|
||
|
||
def _watch_card(active_tab: str) -> str:
|
||
"""画一个手表界面。active_tab 决定显示哪个分页的内容。"""
|
||
actions = TAB_ACTIONS.get(active_tab, MEDIA_ACTIONS)
|
||
|
||
# 顶部标签条
|
||
tabs = []
|
||
for name in TABS:
|
||
on = name == active_tab
|
||
tabs.append(
|
||
'<span class="w-tab%s">%s</span>' % (" on" if on else "", name))
|
||
tabs_html = "".join(tabs)
|
||
|
||
# 操作网格
|
||
buttons = []
|
||
for label, icon in actions:
|
||
buttons.append(
|
||
'<div class="w-btn"><span class="w-ic">%s</span>'
|
||
'<span class="w-lb">%s</span></div>' % (icon, label))
|
||
grid_html = "".join(buttons)
|
||
|
||
return (
|
||
'<div class="watch">'
|
||
'<div class="w-screen">'
|
||
'<div class="w-status"><span>ykxmz</span>'
|
||
'<span class="w-conn">● 局域网 3 ms</span></div>'
|
||
'<div class="w-tabs">%s</div>'
|
||
'<div class="w-body">'
|
||
'<div class="w-head">%s</div>'
|
||
'<div class="w-grid">%s</div>'
|
||
'</div>'
|
||
'<div class="w-foot">被控端:教室电脑 · 192.168.2.64</div>'
|
||
'</div></div>' % (tabs_html, active_tab, grid_html)
|
||
)
|
||
|
||
|
||
def _pc_card() -> str:
|
||
"""电脑端 —— 用真实截图(_build/site_shots/pc-main.png)。"""
|
||
return (
|
||
'<figure class="pcshot">'
|
||
'<img src="/shot/pc-main.png" alt="ykxmz 电脑端首次设置界面" '
|
||
'loading="lazy">'
|
||
'<figcaption>电脑端:装完填个 6 位配对码就行</figcaption>'
|
||
'</figure>'
|
||
)
|
||
|
||
|
||
def _flow_diagram() -> str:
|
||
"""连接方式示意:局域网直连 vs 跨网络走中转。"""
|
||
return '''
|
||
<div class="flow">
|
||
<div class="flow-col">
|
||
<div class="flow-tag ok">同一个 WiFi</div>
|
||
<div class="flow-row">
|
||
<div class="node">⌚<span>手表</span></div>
|
||
<div class="link"><span class="ms">3 ms</span>
|
||
<div class="line"></div></div>
|
||
<div class="node">🖥<span>教室电脑</span></div>
|
||
</div>
|
||
<p>直连,不经过任何服务器。几十毫秒,画面基本跟手。</p>
|
||
</div>
|
||
|
||
<div class="flow-col">
|
||
<div class="flow-tag warn">不在同一个网络</div>
|
||
<div class="flow-row">
|
||
<div class="node">⌚<span>手表</span></div>
|
||
<div class="link"><div class="line"></div></div>
|
||
<div class="node relay">☁<span>中转</span></div>
|
||
<div class="link"><div class="line"></div></div>
|
||
<div class="node">🖥<span>电脑</span></div>
|
||
</div>
|
||
<p>两边都主动连到中转,绕过 NAT。办公室也能管教室。</p>
|
||
</div>
|
||
</div>
|
||
'''
|
||
|
||
|
||
def render_showcase() -> str:
|
||
"""整个"产品展示"区块。"""
|
||
return '''
|
||
<section id="shots"><div class="wrap">
|
||
<div class="sec-head">
|
||
<h2>长什么样</h2>
|
||
<p>手表上十个分页,常用操作都在第一屏</p>
|
||
</div>
|
||
<div class="showcase">
|
||
%(watch)s
|
||
%(pc)s
|
||
</div>
|
||
</div></section>
|
||
|
||
<section id="how-connect"><div class="wrap">
|
||
<div class="sec-head">
|
||
<h2>怎么连上的</h2>
|
||
<p>同一个 WiFi 就走局域网直连,不同网络自动切中转</p>
|
||
</div>
|
||
%(flow)s
|
||
</div></section>
|
||
''' % {"watch": _watch_card("媒体"), "pc": _pc_card(),
|
||
"flow": _flow_diagram()}
|
||
|
||
|
||
SHOWCASE_CSS = """
|
||
/* ---------- 产品展示 ---------- */
|
||
.showcase{display:grid;gap:34px;align-items:start;
|
||
justify-items:center}
|
||
@media(min-width:820px){.showcase{grid-template-columns:1fr 1fr}}
|
||
|
||
/* 手表 mockup。尺寸按真实手表屏幕比例 */
|
||
.watch{width:300px;padding:10px;border-radius:34px;
|
||
background:linear-gradient(160deg,#2b3038,#14171c);
|
||
box-shadow:0 18px 40px rgba(16,24,40,.18);
|
||
position:relative}
|
||
.watch::after{content:'';position:absolute;right:-3px;top:110px;
|
||
width:5px;height:44px;border-radius:3px;background:#3a4048}
|
||
.w-screen{background:%(W_BG)s;border-radius:26px;overflow:hidden;
|
||
font-size:11px;color:%(W_FG)s}
|
||
.w-status{display:flex;justify-content:space-between;align-items:center;
|
||
padding:11px 12px 7px;font-size:9px;font-weight:600;
|
||
gap:6px;white-space:nowrap}
|
||
.w-conn{color:#16a34a;font-weight:600}
|
||
.w-tabs{display:flex;gap:4px;overflow:hidden;padding:0 11px 8px;
|
||
border-bottom:1px solid %(W_BORDER)s}
|
||
.w-tab{flex:none;padding:3px 8px;border-radius:999px;color:%(W_DIM)s;
|
||
font-size:10px;white-space:nowrap}
|
||
.w-tab.on{background:%(W_ACCENT)s;color:#fff;font-weight:600}
|
||
.w-body{padding:12px 13px 14px}
|
||
.w-head{font-size:12px;font-weight:700;margin-bottom:10px}
|
||
.w-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}
|
||
.w-btn{background:%(W_CARD)s;border:1px solid %(W_BORDER)s;
|
||
border-radius:11px;padding:11px 4px 9px;text-align:center;
|
||
display:flex;flex-direction:column;align-items:center;gap:5px}
|
||
.w-ic{font-size:15px;line-height:1}
|
||
.w-lb{font-size:9px;color:%(W_FG)s;white-space:nowrap}
|
||
.w-foot{padding:9px 13px 12px;color:%(W_DIM)s;font-size:9px;
|
||
border-top:1px solid %(W_BORDER)s}
|
||
|
||
/* 电脑端截图 */
|
||
.pcshot{margin:0;text-align:center}
|
||
.pcshot img{width:100%%;max-width:352px;height:auto;
|
||
border-radius:10px;display:block;margin:0 auto;
|
||
box-shadow:0 18px 40px rgba(16,24,40,.16)}
|
||
.pcshot{padding:14px;background:#f2f4f7;border-radius:16px;
|
||
border:1px solid %(W_BORDER)s}
|
||
.pcshot figcaption{margin-top:14px;color:%(W_DIM)s;font-size:13px}
|
||
|
||
/* 连接方式示意 */
|
||
.flow{display:grid;gap:18px}
|
||
@media(min-width:760px){.flow{grid-template-columns:1fr 1fr}}
|
||
.flow-col{border:1px solid #e6e8eb;border-radius:14px;padding:24px 20px;
|
||
background:#fafbfc}
|
||
.flow-tag{display:inline-block;padding:3px 11px;border-radius:999px;
|
||
font-size:12px;font-weight:600;margin-bottom:20px}
|
||
.flow-tag.ok{background:#dcfce7;color:#15803d}
|
||
.flow-tag.warn{background:#fef3c7;color:#a16207}
|
||
.flow-row{display:flex;align-items:center;justify-content:center;
|
||
gap:2px;margin-bottom:16px;flex-wrap:nowrap}
|
||
.node{display:flex;flex-direction:column;align-items:center;gap:6px;
|
||
font-size:26px;flex:none;width:64px}
|
||
.node span{font-size:12px;color:#4b5563;font-weight:500}
|
||
.node.relay{font-size:24px}
|
||
.link{flex:1;min-width:16px;position:relative;display:flex;
|
||
align-items:center;justify-content:center}
|
||
.link .line{width:100%%;height:2px;background:#c7cdd6;
|
||
border-radius:2px}
|
||
.link .ms{position:absolute;top:-15px;font-size:10px;color:#15803d;
|
||
font-weight:600;white-space:nowrap}
|
||
.flow-col p{color:#6b7280;font-size:13px;line-height:1.7;margin:0}
|
||
""" % {"W_BG": W_BG, "W_FG": W_FG, "W_ACCENT": W_ACCENT, "W_DIM": W_DIM,
|
||
"W_CARD": W_CARD, "W_BORDER": W_BORDER}
|