一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的 Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、 访问控制、DDNS、文件共享、Docker 管理等。 核心设计原则:不重新实现任何协议 PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。 Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。 面板删掉后,系统仍是一台配置完好的 Debian 路由器。 安全默认值 · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务 · 所有改网络的动作都需要用户在面板上亲手点「应用」 · 避开 5900 端口,不触碰 VNC / X 会话 · 配置写入前先做语法预检,失败则回滚 架构 · backend/ Python 3 标准库,零第三方依赖(9 个模块) · web/ 原生 JS/CSS,无构建步骤 · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess · 鉴权走 X-Token 请求头 打包 · packaging/build-deb.sh → drouter_1.0.0_all.deb(含 6 个 systemd 单元) · scripts/make-offline-deps.sh → 236 个依赖 .deb + 本地仓库索引 · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装 · packaging/build-docker.sh → 容器镜像(体验用) 测试 40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配), 全部挂在 devtools/sync.sh 的预检链里。 许可:MIT
131 lines
6.1 KiB
Python
131 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
生成 docs/screenshots/ 下的占位截图(SVG → 直接当 PNG 用不合适,这里输出 SVG)。
|
||
|
||
设计取舍:
|
||
README 里引用的是 .png。但仓库里没有真实截图,硬引用会得到 6 个死链。
|
||
两种做法:
|
||
A) 生成真 PNG(需要 Pillow 或 cairosvg,构建机上不一定有)
|
||
B) 生成 SVG 占位,README 里改引用 .svg
|
||
选 B:SVG 是纯文本、零依赖、可随仓库走、而且缩放不糊。
|
||
用户日后截了真图,按同名替换 + 改扩展名即可(README 里已写明)。
|
||
"""
|
||
import os
|
||
|
||
PRI = "#1f6feb"
|
||
BG = "#eef2f9"
|
||
PANEL = "#ffffff"
|
||
LINE = "#e2e7f0"
|
||
TXT = "#161f30"
|
||
TXT2 = "#556480"
|
||
TXT3 = "#8894a8"
|
||
|
||
W, H = 1440, 900
|
||
|
||
SHOTS = [
|
||
("01-dash", "系统概览", "CPU / 内存 / 磁盘 / 负载 / 温度 + 实时网速与历史流量"),
|
||
("02-iface", "网卡与桥接", "以 MAC 为主键管理网卡,标注备注 / 角色 / 链路 / 速率 / MTU"),
|
||
("03-fw4", "防火墙 IPv4", "nftables 规则可视化编辑,支持链 / 表 / 规则级操作"),
|
||
("04-qos", "智能限速 QoS", "基于 IFB 的入向整形 + 分类限速,规则实时下发"),
|
||
("05-flowlog", "连接与流日志", "conntrack 连接跟踪 + 实时流日志滚动查看"),
|
||
("06-webshell", "Web 终端", "浏览器内直接操作 shell,支持多会话"),
|
||
]
|
||
|
||
def logo_mark(x, y, s=1.0):
|
||
"""复刻 web/logo.svg 的路由环,作为占位图的水印标识。"""
|
||
return f'''<g transform="translate({x},{y}) scale({s})">
|
||
<circle cx="24" cy="24" r="21" fill="none" stroke="{PRI}" stroke-width="2.4"
|
||
stroke-linecap="round" stroke-dasharray="96 36" transform="rotate(-42 24 24)" opacity=".55"/>
|
||
<path d="M11 31.5 20.5 22 27 27.5 37 15.5" fill="none" stroke="{PRI}" stroke-width="3"
|
||
stroke-linecap="round" stroke-linejoin="round"/>
|
||
<circle cx="11" cy="31.5" r="3.1" fill="{PRI}"/>
|
||
<circle cx="20.5" cy="22" r="2.4" fill="{PRI}"/>
|
||
<circle cx="27" cy="27.5" r="2.4" fill="{PRI}"/>
|
||
<path d="M33.2 12.6h4.6v4.6" fill="none" stroke="{PRI}" stroke-width="3"
|
||
stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M12 38.5h24" fill="none" stroke="{PRI}" stroke-width="2.4"
|
||
stroke-linecap="round" opacity=".45"/>
|
||
</g>'''
|
||
|
||
def skeleton(w, h, x, y, rows=4, gap=14):
|
||
"""画一块灰色的内容骨架,暗示「这里将来是真的界面」。"""
|
||
out = []
|
||
out.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="12" '
|
||
f'fill="{PANEL}" stroke="{LINE}"/>')
|
||
yy = y + 22
|
||
for i in range(rows):
|
||
# 行宽交替,看起来像文字块
|
||
ww = int(w * (0.72 if i % 2 == 0 else 0.52))
|
||
out.append(f'<rect x="{x+20}" y="{yy}" width="{ww}" height="10" rx="5" '
|
||
f'fill="{LINE}"/>')
|
||
yy += 10 + gap
|
||
return "\n ".join(out)
|
||
|
||
def svg(key, title, desc):
|
||
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}"
|
||
width="{W}" height="{H}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',system-ui,sans-serif">
|
||
<rect width="{W}" height="{H}" fill="{BG}"/>
|
||
|
||
<!-- 侧边栏 -->
|
||
<rect x="0" y="0" width="236" height="{H}" fill="{PANEL}"/>
|
||
<line x1="236" y1="0" x2="236" y2="{H}" stroke="{LINE}"/>
|
||
{logo_mark(18, 14, 0.9)}
|
||
<text x="58" y="34" font-size="17" font-weight="700" fill="{TXT}">Drouter</text>
|
||
<text x="58" y="50" font-size="11" fill="{TXT3}">Debian 13 路由管理</text>
|
||
|
||
<!-- 侧栏分组骨架 -->
|
||
<g opacity=".85">
|
||
<rect x="14" y="86" width="208" height="30" rx="8" fill="{PRI}" opacity=".10"/>
|
||
<rect x="26" y="96" width="96" height="10" rx="5" fill="{PRI}" opacity=".55"/>
|
||
<rect x="26" y="132" width="128" height="10" rx="5" fill="{LINE}"/>
|
||
<rect x="26" y="164" width="104" height="10" rx="5" fill="{LINE}"/>
|
||
<rect x="26" y="196" width="140" height="10" rx="5" fill="{LINE}"/>
|
||
<rect x="26" y="228" width="88" height="10" rx="5" fill="{LINE}"/>
|
||
<rect x="26" y="260" width="120" height="10" rx="5" fill="{LINE}"/>
|
||
<rect x="26" y="292" width="100" height="10" rx="5" fill="{LINE}"/>
|
||
</g>
|
||
|
||
<!-- 顶栏 -->
|
||
<rect x="236" y="0" width="{W-236}" height="62" fill="{PANEL}"/>
|
||
<line x1="236" y1="62" x2="{W}" y2="62" stroke="{LINE}"/>
|
||
<text x="268" y="39" font-size="19" font-weight="700" fill="{TXT}">{title}</text>
|
||
<rect x="{W-330}" y="19" width="120" height="24" rx="12" fill="{LINE}"/>
|
||
<circle cx="{W-56}" cy="31" r="15" fill="{PRI}" opacity=".13"/>
|
||
<text x="{W-61}" y="36" font-size="13" font-weight="700" fill="{PRI}">A</text>
|
||
|
||
<!-- 内容骨架 -->
|
||
{skeleton(540, 214, 268, 92, rows=4)}
|
||
{skeleton(540, 214, 832, 92, rows=4)}
|
||
{skeleton(1104, 300, 268, 330, rows=6)}
|
||
{skeleton(468, 300, 600, 330, rows=6)}
|
||
{skeleton(468, 300, 1096, 330, rows=6)}
|
||
{skeleton(1104, 168, 268, 660, rows=3)}
|
||
|
||
<!-- 水印:占位提示 -->
|
||
<g opacity=".92">
|
||
<rect x="{W/2-330}" y="{H/2-58}" width="660" height="116" rx="16"
|
||
fill="{PANEL}" stroke="{PRI}" stroke-opacity=".35" stroke-width="1.5"/>
|
||
<text x="{W/2}" y="{H/2-14}" font-size="21" font-weight="700" fill="{TXT}"
|
||
text-anchor="middle">【占位图】{title}</text>
|
||
<text x="{W/2}" y="{H/2+16}" font-size="14" fill="{TXT2}"
|
||
text-anchor="middle">{desc}</text>
|
||
<text x="{W/2}" y="{H/2+42}" font-size="12" fill="{TXT3}"
|
||
text-anchor="middle">真实界面请以安装后为准 · 替换 docs/screenshots/{key}.svg 即可</text>
|
||
</g>
|
||
</svg>
|
||
'''
|
||
|
||
out_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||
"docs", "screenshots")
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
for key, title, desc in SHOTS:
|
||
p = os.path.join(out_dir, key + ".svg")
|
||
# newline="\n" 是必须的:Windows 上 Python 默认把 \n 翻成 \r\n,
|
||
# 生成的 SVG 在 git 里会被 .gitattributes 回写成 LF,导致每次重建都
|
||
# 产生"改了又没改"的假 diff。显式指定 \n 后,生成结果与入库结果一致。
|
||
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
||
f.write(svg(key, title, desc))
|
||
print("wrote", p)
|
||
print("done:", len(SHOTS), "placeholders")
|