Compare commits

...
24 Commits
Author SHA1 Message Date
butubb 0a8b2707cd post: 4G 模块文章补配图(大疆 Cellular 模块产品图) 2026-10-05 15:06:03 +08:00
butubb d0c79a9f71 post: 移除旧版标题(已合并进《给飞牛 NAS 加装大疆 4G 模块一代当备用网》) 2026-10-05 14:58:09 +08:00
butubb 8b4c0c1fd5 post: 给飞牛 NAS 加装大疆 4G 模块一代当备用网(补充硬件来源 + HiDeck 管理) 2026-10-05 14:58:06 +08:00
butubb 8c8641d336 post: 给飞牛 NAS 加装 4G 模块当备用网(拔掉网线也不掉线) 2026-10-05 14:53:11 +08:00
butubb b190117582 chore: 删除无用的测试markdown文件 2026-07-11 08:04:04 +08:00
butubb ece663aad7 chore: 删除github同步元数据文件
移除了本地的github sync元数据配置文件,清理同步缓存记录
2026-07-11 08:00:54 +08:00
butubb 1d647d2c8b Merge branch 'main' of github.com:8butubb/8butubb.github.io 2026-07-11 07:57:36 +08:00
butubb 8f1e6db348 chore(themes): 更新PaperMod子模块到最新版本
将PaperMod主题子模块从旧提交c4ca7ca更新到154d006
2026-07-11 07:48:49 +08:00
butubb d2fb63e7d8 chore: add new post and add icp backup info
add the 面向死亡备份 markdown post, and add wanbei icp backup link in footer
2026-07-10 12:44:00 +08:00
butubb dd98cd87fa Sync 2026-07-06 13:53:57 +08:00
butubb 5f2b50e3a9 Sync 2026-07-06 13:47:47 +08:00
butubb 410a5f5217 Sync 2026-07-06 13:46:32 +08:00
butubb 02e2312fce sync: 推送 19 个文件变更 2026-07-06 13:30:17 +08:00
butubb 2a7e4f0edc sync: 推送 19 个文件变更 2026-07-06 13:29:33 +08:00
butubb 8b2707218b sync: 推送 23 个文件变更 2026-07-06 13:28:58 +08:00
butubb 17701a362a sync: 推送 24 个文件变更 2026-07-06 13:28:05 +08:00
butubb de2561fc40 sync: 推送 1 个文件变更 2026-07-06 12:56:27 +08:00
butubb 1fac0280d5 sync: 推送 1 个文件变更 2026-07-06 12:55:39 +08:00
butubb 46ae27c127 sync: 推送 1 个文件变更 2026-07-06 12:55:03 +08:00
butubb 57bbc5f201 sync: 推送 1 个文件变更 2026-07-06 12:54:27 +08:00
butubb 6274bdd5a7 sync: 推送 1 个文件变更 2026-07-06 12:45:39 +08:00
butubb d48c972733 1 2026-06-11 15:42:16 +08:00
butubb def46009ec add photo 2026-05-07 15:30:47 +08:00
butubb a1b8cbd738 新增图库 2026-05-07 15:26:46 +08:00
31 changed files with 835 additions and 71 deletions
+303
View File
@@ -0,0 +1,303 @@
/* ========================================
Gallery — PaperMod Design Language
======================================== */
/* ---------- Page Layout ---------- */
/* Widen the main container for gallery pages */
.main:has(.gallery-page-content) {
max-width: calc(var(--nav-width) + var(--gap) * 2);
}
.gallery-page-content {
max-width: 100%;
}
/* ---------- Album Section ---------- */
.gallery-album {
background: var(--entry);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--gap);
margin-bottom: var(--gap);
transition: transform 0.1s;
}
.gallery-album:active {
transform: scale(0.99);
}
.gallery-album-header {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 4px;
}
.gallery-album-title {
font-size: 24px;
font-weight: 600;
color: var(--primary);
line-height: 1.3;
}
.gallery-album-count {
font-size: 13px;
color: var(--secondary);
flex-shrink: 0;
}
.gallery-album-desc {
color: var(--secondary);
font-size: 14px;
line-height: 1.6;
margin-bottom: var(--content-gap);
}
/* ---------- Image Grid ---------- */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: calc(var(--content-gap) * 0.5);
}
.gallery-card {
position: relative;
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
background: var(--code-bg);
border: 1px solid var(--border);
transition: transform 0.1s;
}
[data-theme="dark"] .gallery-card {
background: var(--tertiary);
}
.gallery-card:active {
transform: scale(0.96);
}
.gallery-card-img {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gallery-card-img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.gallery-card:hover .gallery-card-img img {
transform: scale(1.05);
}
.gallery-card::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
transition: background 0.2s ease;
pointer-events: none;
border-radius: inherit;
}
.gallery-card:hover::after {
background: rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .gallery-card:hover::after {
background: rgba(0, 0, 0, 0.2);
}
/* ---------- Empty State ---------- */
.gallery-empty {
text-align: center;
padding: calc(var(--gap) * 3) 0;
color: var(--secondary);
}
.gallery-empty svg {
margin: 0 auto var(--gap);
opacity: 0.4;
}
.gallery-empty p {
font-size: 16px;
}
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.lightbox.active {
opacity: 1;
visibility: visible;
}
.lightbox-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.85);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
[data-theme="dark"] .lightbox-backdrop {
background: rgba(0, 0, 0, 0.92);
}
.lightbox-body {
position: relative;
max-width: 92vw;
max-height: 88vh;
z-index: 1;
}
.lightbox-body img {
max-width: 92vw;
max-height: 88vh;
object-fit: contain;
border-radius: var(--radius);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Lightbox Buttons */
.lightbox-btn {
position: absolute;
z-index: 2;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: background 0.2s, transform 0.1s;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
padding: 0;
}
.lightbox-btn:hover {
background: rgba(255, 255, 255, 0.22);
}
.lightbox-btn:active {
transform: scale(0.92);
}
.lightbox-btn svg {
width: 20px;
height: 20px;
}
.lightbox-close-btn {
top: 20px;
right: 20px;
}
.lightbox-prev-btn {
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-prev-btn:active {
transform: translateY(-50%) scale(0.92);
}
.lightbox-next-btn {
right: 20px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-next-btn:active {
transform: translateY(-50%) scale(0.92);
}
.lightbox-counter {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
color: rgba(255, 255, 255, 0.7);
font-size: 13px;
background: rgba(0, 0, 0, 0.4);
padding: 4px 12px;
border-radius: 20px;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
:root {
--gap: 14px;
}
.gallery-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.gallery-album {
padding: 14px;
}
.gallery-album-title {
font-size: 20px;
}
.lightbox-btn {
width: 38px;
height: 38px;
}
.lightbox-btn svg {
width: 16px;
height: 16px;
}
.lightbox-prev-btn {
left: 10px;
}
.lightbox-next-btn {
right: 10px;
}
.lightbox-close-btn {
top: 10px;
right: 10px;
}
}
@media (max-width: 480px) {
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
.gallery-album-title {
font-size: 18px;
}
}
+102
View File
@@ -0,0 +1,102 @@
document.addEventListener('DOMContentLoaded', function () {
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxClose = document.getElementById('lightbox-close');
const lightboxPrev = document.getElementById('lightbox-prev');
const lightboxNext = document.getElementById('lightbox-next');
const lightboxCounter = document.getElementById('lightbox-counter');
const lightboxBackdrop = document.getElementById('lightbox-backdrop');
const cards = Array.from(document.querySelectorAll('.gallery-card'));
let currentIndex = -1;
function openLightbox(index) {
if (index < 0 || index >= cards.length) return;
currentIndex = index;
const src = cards[index].dataset.src;
if (!src) return;
lightboxImg.src = src;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
updateCounter();
}
function closeLightbox() {
lightbox.classList.remove('active');
document.body.style.overflow = '';
currentIndex = -1;
}
function showPrev() {
if (currentIndex > 0) {
openLightbox(currentIndex - 1);
}
}
function showNext() {
if (currentIndex < cards.length - 1) {
openLightbox(currentIndex + 1);
}
}
function updateCounter() {
if (lightboxCounter) {
lightboxCounter.textContent = (currentIndex + 1) + ' / ' + cards.length;
}
}
// Click on gallery cards
cards.forEach(function (card, index) {
card.addEventListener('click', function () {
openLightbox(index);
});
});
// Lightbox controls
lightboxClose.addEventListener('click', closeLightbox);
lightboxBackdrop.addEventListener('click', closeLightbox);
lightboxPrev.addEventListener('click', function (e) {
e.stopPropagation();
showPrev();
});
lightboxNext.addEventListener('click', function (e) {
e.stopPropagation();
showNext();
});
// Keyboard navigation
document.addEventListener('keydown', function (e) {
if (!lightbox.classList.contains('active')) return;
switch (e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
showPrev();
break;
case 'ArrowRight':
showNext();
break;
}
});
// Touch swipe support
var touchStartX = 0;
var touchEndX = 0;
lightbox.addEventListener('touchstart', function (e) {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
lightbox.addEventListener('touchend', function (e) {
touchEndX = e.changedTouches[0].screenX;
var diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
showNext();
} else {
showPrev();
}
}
}, { passive: true });
});
+3 -3
View File
@@ -1,5 +1,5 @@
---
title: "归档"
layout: "archives"
url: "/archives"
title: 归档
layout: archives
url: /archives
---
+5
View File
@@ -0,0 +1,5 @@
---
title: 跑步记录
date: 2026-03-12T08:00:00Z
---
![123](https://cdn.jsdelivr.net/gh/8butubb/image/img/20260309120246636.png)
+4
View File
@@ -0,0 +1,4 @@
---
title: 画廊
description: 记录生活中的美好瞬间
---
+3 -4
View File
@@ -1,10 +1,9 @@
---
title: Git 完整教程指南
tags: ["git", "版本控制", "教程"]
date: 2025-10-08
summary: "从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。"
tags: [git, 版本控制, 教程]
date: 2025-10-11T08:00:00Z
summary: 从零开始学习 Git 版本控制系统,包含基础概念、常用命令、分支管理、协作流程等完整内容。
---
> 本教程适用于 Git 初学者到进阶用户,涵盖日常开发中 90% 的使用场景。
## 什么是 Git
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: Linux 硬链接
tags: ["linux"]
date: 2024-12-04
summary: "在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。"
tags: [linux]
date: 2024-12-07T08:00:00Z
summary: 在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
---
在 Linux 系统中,文件的存储与管理是通过链接来实现的。硬链接是其中一种重要的链接类型,本文将详细介绍硬链接的概念、创建方法及其应用场景。
+2 -2
View File
@@ -1,7 +1,7 @@
---
title: Linux文件系统概述
tags: ["linux"]
date: 2024-12-04
tags: [linux]
date: 2024-12-07T08:00:00Z
---
Linux的文件系统结构是其核心组成部分之一,它以一种层次化的方式组织数据,使得用户和程序能够高效地访问和管理文件。以下是一些关键概念和特点:
+3 -4
View File
@@ -1,10 +1,9 @@
---
title: docker搭建思源笔记
tags: ["linux","docker"]
date: 2024-11-23
summary: "docker搭建思源笔记..."
tags: [linux, docker]
date: 2024-11-25T00:00:00Z
summary: docker搭建思源笔记...
---
- `PUID`: 自定义用户 ID(可选,如果未提供,默认为 `1000`)
- `PGID`: 自定义组 ID(可选,如果未提供,默认为 `1000`)
- `workspace_dir_host`:宿主机上的工作空间文件夹路径
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: frp内网穿透使用指南
tags: ["linux"]
date: 2024-12-02
summary: "frp(Fast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。..."
tags: [linux]
date: 2024-12-05T08:00:00Z
summary: frp(Fast Reverse Proxy)是一个高性能的反向代理应用,主要用于内网穿透。...
---
## 什么是 frp?
+3 -4
View File
@@ -1,10 +1,9 @@
---
title: gitea 自部署使用体验
tags: ["git","docker"]
date: 2025-11-27
summary: "本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。"
tags: [git, docker]
date: 2025-11-30T08:00:00Z
summary: 本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
---
> 注意:本文基于 Docker Compose 一键启动 Gitea + Gitea Runner 的实战记录。
## 为什么使用 gitea
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: GitHub 白嫖指南
tags: ["网站", "GitHub", "博客"]
date: 2026-03-19
tags: [网站, GitHub, 博客]
date: 2026-03-21T00:00:00Z
---
## GitHub 白嫖指南
GitHub 俗称"男人的交友市场",但它的实用价值远不止如此。今天就来分享一下我在 GitHub 上白嫖的那些东西。
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: markdown语法使用指南
tags: ["markdown"]
date: 2025-03-06
summary: "用来学习markdown语法"
tags: [markdown]
date: 2025-03-09T08:00:00Z
summary: 用来学习markdown语法
---
## 基础语法
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: running_page部署
tags: ["学习","运动"]
date: 2026-03-07
tags: [学习, 运动]
date: 2026-03-09T00:00:00Z
---
最近我的博客上线了一个新的页面,[running_page](https://run.butubb.cn)
![11](https://cdn.jsdelivr.net/gh/8butubb/image/img/20260309120246636.png)
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: zerotier使用指南
tags: ["linux"]
date: 2025-09-07
summary: "zerotier使用指南"
tags: [linux]
date: 2025-09-10T08:00:00Z
summary: zerotier使用指南
---
# 注册
@@ -1,8 +1,8 @@
---
title: 一个NAS用户的UPS求生指南:从断电焦虑到真香体验
tags: ["nas"]
date: 2025-10-22
summary: '作为一个在群租房里"苟活"的绿联NAS用户,这两年我深刻体会到了什么叫"用电如履薄冰"...'
tags: [nas]
date: 2025-10-24T00:00:00Z
summary: "作为一个在群租房里\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"苟活\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"的绿联NAS用户,这两年我深刻体会到了什么叫\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"用电如履薄冰\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"..."
---
## 第一章:那些年,我们一起经历的断电惊魂
@@ -1,9 +1,8 @@
---
title: 减脂健身计划最新进展
tags: ["健康"]
date: 2026-02-24
tags: [健康]
date: 2026-02-27T08:00:00Z
---
## 近期进展汇报
今天是年后上班第一天,过年期间我的体重不重反轻,目前体重79.5kg。虽然减少的幅度下降,整个过年期间只减少了0.5kg,但是没有上涨我已经很满足了。
@@ -1,10 +1,9 @@
---
title: 把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码
tags: ["学习"]
date: 2025-10-01
summary: "前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。"
title: "把Kindle变成信息屏!越狱+自动更换新闻壁纸全攻略 | 附开源代码"
tags: [学习]
date: 2025-10-03T00:00:00Z
summary: 前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
---
前些日子翻出尘封两年的KPW4,本想重拾阅读初心,却发现亚马逊中国早已物是人非。
正当想着要不要给它闲鱼出手时转卖时,B站刷到Kindle全机型都支持越狱了。
+2 -2
View File
@@ -1,7 +1,7 @@
---
title: 新手运动建议
tags: ["运动","健康"]
date: 2026-03-19
tags: [运动, 健康]
date: 2026-03-22T08:00:00Z
---
作为一个坚持运动了3个多月的新手,我总结了一些经验和建议,希望能帮助刚开始运动的初学者少走弯路。
@@ -1,11 +1,10 @@
---
title: "给 Hugo 博客归档页加上 Github 风格热力图"
date: 2026-03-28T16:26:31+08:00
title: 给 Hugo 博客归档页加上 Github 风格热力图
date: 2026-03-31T16:26:31Z
draft: false
tags: ["博客", "Hugo", "前端", "魔改"]
categories: ["博客"]
tags: [博客, Hugo, 前端, 魔改]
categories: [博客]
---
最近看着博客的归档页面,总觉得光秃秃的列表差点意思。平时经常看 Github 主页那块绿色的代码提交热力图(Heatmap),看着自己的“绿格子”一天天变多,那种正反馈确实挺让人上瘾的。
于是就萌生了一个想法:**能不能在我的博客归档页也加一个类似 Github 风格的文章更新热力图?**
@@ -0,0 +1,263 @@
---
title: 给飞牛 NAS 加装大疆 4G 模块一代当备用网:拔掉网线也不掉线
tags: [nas, 4g, 大疆, 网络, 飞牛os]
date: 2026-10-05T00:00:00Z
summary: "单条网线的 NAS 一断线就彻底失联:中继连不上、机器人掉线、远程全废。这篇记录我给飞牛 NAS 插上一块淘宝二手 100 元淘来的大疆 4G 模块一代(QDC507),用开源项目 HiDeck 管它,并一路踩过 OVS 藏断线、僵尸默认路由、网口编号漂移、模块编号变化这些坑,最后实现「拔线自动切 4G、插回自动切回」的全过程。"
---
## 起因:单线 NAS 的焦虑
家里的 NAS 只有一条网线。平时岁月静好,但只要出现下面任意一种情况:
- 网线被谁碰掉(比如我插拔 USB 设备时顺手带下来)
- 光猫/路由器抽风重启
- 宽带施工挖断
NAS 上的东西就**全部对外失联**:远程访问进不来、自建的机器人掉线、同步全部卡住,而人在外面只能干瞪眼。
想上第二条宽带吧,又贵又没必要。所以很自然就想到了:**插一块 4G 模块当备用链路**,平时待命,主线断了自动顶上去。
## 硬件:大疆 4G 模块一代(二手 100 元)+ 一张流量卡
主角是一块**大疆 4G 模块一代**(型号 QDC507)—— 本来是大疆给无人机做 4G 图传增强用的,我在淘宝二手渠道 **100 元**拿下。现在回头看,这价钱也算「高价入手」了 😂(同款行情早就跌到几十块)。
![大疆 4G 模块一代(DJI Cellular 模块)](https://cdn.jsdelivr.net/gh/8butubb/image/img/dji-cellular-4g-module-gen1.jpg)
*大疆 4G 模块一代(DJI Cellular 模块):U 盘大小的黑盒子,插上电脑就是一块普通 4G 网卡*
它本质上是**深度定制的 Quectel LTE 模块**,识别信息非常诚实:
```bash
lsusb | grep -i quectel # Bus 007 Device 003: ID 2c7c:0125 Quectel EC25 LTE modem
mmcli -L # /org/freedesktop/ModemManager1/Modem/0 QUECTEL Mobile Broadband Module
mmcli -m 0 | grep -i firmware # firmware revision: QDC507GLEFM21 ← QDC507 就是大疆一代的型号
ip -br link | grep wwan # wwan0 UP
ip route show default
# default via 192.168.20.1 dev enp4s0-ovs proto static metric 100 ← 有线(主)
# default via 10.200.35.20 dev wwan0 metric 5000 ← 4G(备)
```
USB 的 VID `2c7c` 是 Quectel 的,固件里又带着大疆的型号串 QDC507 —— **「大疆定制固件 + Quectel 硬件」**这个组合在折腾时很有用:网上大半资料是按 Quectel(EG25-G / EC25 那套)写的,照着查基本都能对上。
几个要点:
- 网卡驱动是 `qmi_wwan`,出来一个 `wwan0`;
- 拨号是 **ModemManager 的 initial EPS bearer** 自动做的,**NetworkManager 完全不接管它**(这点后面很关键);
- 运营商给的是内网地址(CGNAT)+ 一段 IPv6;
- 默认路由的 `metric` 决定优先级:**数字小的优先**。所以有线 100、4G 5000 —— 有线在的时候,4G 就是一条安静的备胎。
## 顺手安利:用 HiDeck 管理这块模块
能当网卡用之后,日常还得看它:信号多少、注册没注册、SIM 什么状态、以及最好用的——**收发短信**(查话费/流量余额就靠短信)。命令行 `mmcli` 都能看,但天天敲太累,于是我把这块交给了一个开源项目:**HiDeck**(`yibaiba/hideck`)—— 一个自托管的 Qualcomm 4G / LTE / 5G 模块 Web 控制台。
部署一句话(Docker,host 网络 + 需要 `/dev` 权限):
```bash
curl -fsSL https://raw.githubusercontent.com/yibaiba/hideck/main/deploy.sh | sh
# 浏览器打开 http://<NAS_IP>:7575 默认 admin / admin(首次登录强制改密码)
```
我常用的几项:
- **设备**:USB 模块自动发现(QMI / MBIM / AT),实时看信号、频段、SIM 身份 —— 换 USB 口、模块编号漂移这类问题,在这里一眼就能看出模块还在不在;
- **短信**:收件箱 + 发短信。查话费/流量就是给它发 `101` / `108` 到 `10001`,回复直接显示在页面上;
- **代理**:SOCKS5 / HTTP 绑定到这块模块(`SO_BINDTODEVICE`),需要"只有某个程序走 4G"时很省事;
- **电话 / VoWiFi**:还能当网络电话用(VoLTE / WiFi Calling),这部分我还没细折腾;
- **定时任务 + 通知**:支持 Bark、飞书、Telegram 等,做状态/流量提醒很方便。
我的分工是:**HiDeck 当模块的「仪表盘」**(信号、短信、临时代理),**链路切换交给系统那一套路由看门狗**(下面细说)—— 两者互不干扰。
## 飞牛的第一盆冷水:界面里根本没有 4G
打开飞牛「网络设置」,**找不到这块 4G 网卡**。
一开始以为是配置问题,后来翻了 `/usr/trim/bin/network_service` 二进制、它的日志、以及前端页面资源,发现里面连 `wwan`/`modem` 这类字样都没有 —— 结论很干脆:**飞牛的网络设置目前不支持 WWAN 类型的接口**。
所以 4G 这条链路只能靠命令行管(`mmcli` / `ip` / `nmcli`)。界面指望不上,但也不影响用。
## 大坑:拔掉网线,NAS 反而彻底失联
最反直觉的地方来了。
信心满满地拔掉网线,结果**整台 NAS 直接像死了一样**:远程中继连不上、飞书机器人静默、连域名都解析不出来。
但诡异的是:
```bash
curl -4 -s --interface wwan0 https://ifconfig.me/ip # ✅ 走 4G 单独测,通!
ip route show default # ✅ 4G 那条 metric 5000 明明在路由表里
```
**4G 是好的,路由也在,但流量就是不从它走。** 这就是典型的需要抓现场的问题 —— 等你回我消息的时候,故障现场早就过去了。
### 第一步:用「触发式采样器」抓现场
间歇性故障最忌讳猜。做法是写个采样器,**等状态跳变**(比如物理网口 `carrier` 从 1 变 0),跳变后立刻连采十几轮,把每一轮的路由表、DNS、每个接口单独发请求的出口 IP、中继域名可达性全记下来。
采样结果非常干净:
- 有线接口 `enp4s0` 的 `carrier=0`(物理上确实断了);
- 但桥接口 `enp4s0-ovs` 的 `carrier` **恒为 1**;
- 断网窗口里,ping/HTTPS/DNS 全是超时(`000`),而 `curl --interface wwan0` 单独走 4G 是通的。
### 第二步:根因 —— OVS 把断线藏了起来
我的网口套在 OVS 里。**OVS 会把物理断线藏起来**:
- 物理口 `enp4s0`:`carrier=0`(断了)
- OVS 内部口 `enp4s0-ovs`:`carrier` 永远是 1
于是 **NetworkManager 根本不知道线断了**,那条 `proto static metric 100` 的默认路由**不会撤销**。结果就是:所有流量继续往一个已经不可达的网关灌(黑洞),而 4G 的 `metric 5000` 老实排在后头,**永远轮不到**。
所以这不是 4G 的问题,是**僵尸默认路由**的问题。
## 解法:2 秒轮询的默认路由看门狗
思路很直接:**盯物理口的 carrier**,断了就把 4G 那条默认路由的 metric 从 5000 提到 50(压过有线的 100),线回来再撤掉。
```bash
PHYS=enp4s0 # 物理口(读 carrier 用)
LTE_DEV=wwan0 # 4G 网卡
LTE_PRIO_METRIC=50 # 临时优先权
carrier() { cat /sys/class/net/$PHYS/carrier 2>/dev/null || echo "?"; }
lte_gw() { ip route show default | awk -v d="$LTE_DEV" '$1=="default" && $5==d {print $3; exit}'; }
# 注意:判断 metric 必须用数字比较,别用字符串匹配(见下面的坑)
route_metric_is() { # $1=网关 $2=网卡 $3=metric
ip route show default | awk -v g="$1" -v d="$2" -v want="$3" '
$1=="default" && $3==g && $5==d { for (i=1;i<=NF;i++) if ($i=="metric" && $(i+1)+0==want+0) found=1 }
END { exit(found?0:1) }'
}
check_once() {
c=$(carrier); lg=$(lte_gw)
case "$c" in
0) # 有线断了 → 给 4G 提权
if [ -n "$lg" ] && ! route_metric_is "$lg" "$LTE_DEV" "$LTE_PRIO_METRIC"; then
ip route replace default via "$lg" dev "$LTE_DEV" metric "$LTE_PRIO_METRIC"
fi ;;
1) # 有线恢复 → 撤掉临时路由
if [ -n "$lg" ] && route_metric_is "$lg" "$LTE_DEV" "$LTE_PRIO_METRIC"; then
ip route del default via "$lg" dev "$LTE_DEV" metric "$LTE_PRIO_METRIC"
fi ;;
esac
}
```
### 这里有两个必须踩过才知道的坑
**坑 1:不要动 NetworkManager 管的那条路由。**
我最早的做法是去改**有线**那条默认路由的 metric(100 → 10000),结果 **30 秒内就被 NM 改回 100**,看门狗白干 ✗。
正确姿势是改**没人管的那条**:4G 的默认路由是 **ModemManager** 建的,NM 不碰它,所以把它从 `metric 5000` 提到 `metric 50`,稳稳压过有线的 100 ✓。
> 通用经验:**改配置前先问「这条配置属于哪个管理器?我改它会不会被覆盖?」** 会覆盖,就换一个等价但归你的杠杆。
**坑 2:判断 metric 别用 `grep` 做前缀匹配。**
我第二版用了 `grep "metric 50"` 来判断"临时优先路由还在不在",看着挺对,实际灾难:
```
default via 10.200.35.20 dev wwan0 metric 5000
^^^^ 里面就含 "metric 50"
```
于是有线恢复之后,脚本以为临时路由还在,**每 2 秒重复删一次、刷满日志** ✗。改用 `awk` 把 metric 取出来做**数字比较**之后,世界清净了。
## 部署:非 root 也能长期维护的「转发脚本 + 自更新」
这台 NAS 上我跑服务的账号没有 root,每次改脚本都要找人 `sudo` 重装,很烦。所以又套了一层:
1. `/usr/local/sbin/wan_carrier_fix.sh` 只放一个 **3 行的 stub**,干一件事:exec 到工作目录里的真身;
2. 真身脚本放在普通用户可写的工作目录里,主循环每 2 秒顺手比一下**自己的 mtime**,发现被改了就地 `exec "$SELF"` 重新加载。
```bash
# /usr/local/sbin/wan_carrier_fix.sh —— 只装一次,以后再也不碰
#!/bin/bash
exec /path/to/real/wan_carrier_fix.sh "$@"
```
```ini
# /etc/systemd/system/wan-carrier-fix.service
[Unit]
Description=WAN carrier failover watchdog
After=network-online.target
[Service]
ExecStart=/usr/local/sbin/wan_carrier_fix.sh
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
效果:**以后改脚本只要改工作目录里那份,2 秒后自动生效**,systemd 只负责兜底崩溃重启。省下的都是找 sudo 的时间。
(小细节:自更新重载必须用**绝对路径**,因为 systemd 给的工作目录可能是 `/`。)
## 切到 4G 之后要处理的两件事
**① 出口 IP 变了,中继要重新握手。**
4G 出口 IP 和宽带完全不是一个网段。家里的 NAS 中继(fnid)是长连接,切过去之后它那边不认旧会话,**必须重启一次中继服务**(`systemctl restart trim_connect`),重启完立刻可用 ✓。
**② 裸 4G 只有出、没有进。**
4G 是运营商 CGNAT,**入站不可能**。所以对外访问要么走中继、要么走 Tailscale —— 局域网直连在断线时必然不通,这不是故障。
顺带一个同源坑:**给隧道/反代配置回源地址时别写局域网 IP**。我原来写的是 `http://192.168.20.18:8080` 这种,一旦网卡上的地址没了,回源直接断(外部看到 530)。改成 `http://127.0.0.1:8080` 之后,**跟走哪条线路、哪个地址完全无关**了 ✓。
## 别让 4G 偷偷烧流量
装完之后我看了眼计数器,吓了一跳:**开机 36 分钟,4G 上行已经 3 GB** 😅
原因:机器上 **IPv6 的默认路由只有 4G 这一条**,所以所有 IPv6 出站流量都在走 4G。而跑着 qBittorrent 的话,做种上传会疯狂吃 IPv6。
两个处理:
- **治本(软件里就能做)**:qBittorrent → 设置 → 高级 → 「网络接口」选有线口、「可选 IP 地址」选有线地址 → 它只用有线收发,线断了它自己哑掉,**绝不会吃 4G** ✓
- **兜底**:加一个静默流量看门狗,**只在"有线本来就正常"时才可能报警**(当日 4G > 1 GB,或单轮增量 > 200 MB),断线期间 4G 是唯一出口,属正常使用,一个字都不说。
## 换个 USB 口的意外收获:别写死网卡名
后来我把 4G 模块换个 USB 口插,顺手发现两个"绝对不能写死"的东西:
1. **ModemManager 里的模块编号会变**:`Modem/0` 变成了 `Modem/2`;
2. **接口名也可能变**(这次还是 `wwan0`,但没理由指望它永远不变)。
所以所有脚本都改成了**自动识别**:
```bash
# 找 4G 网卡:优先看驱动是不是 qmi_wwan,退路才找 wwan*
for d in /sys/class/net/*; do
[ -e "$d/device/driver" ] || continue
[ "$(basename "$(readlink -f "$d/device/driver")")" = "qmi_wwan" ] && basename "$d"
done
```
另外还遇到一次 ModemManager 状态错乱:`mmcli` 报 `state: failed / failed reason: sim-missing`,可数据其实是通的(模块自己续着会话)。`systemctl restart ModemManager` 让它重新识别 SIM 即可 —— **但要注意,这时 MM 已经不托管任何数据会话了,一旦掉线它不会自动重连**,所以别放着不管。
## 效果
现在这套跑下来,日常是这样的:
| 场景 | 表现 |
| --- | --- |
| 拔掉网线 | 1~2 秒内 4G 提权顶上,NAS 服务、远程访问、机器人都不断 |
| 插回网线 | 自动撤掉临时的 4G 路由,出口回到有线,4G 回到待命 |
| 只有 4G 时 | 远程访问走中继(需重启一次中继服务),局域网直连不通属正常 |
| 有线正常时 | 4G 只当备胎,流量几乎为零(qB 已绑定网卡) |
成本就是一块二手大疆 4G 模块(100 元)+ 一张流量卡,比第二条宽带便宜太多了。
## 小结
- **单线 NAS 的备用链路,值得折腾**;大疆一代 4G 模块(本质是 Quectel LTE 模块)在 Linux 上几乎是插上就能用,日常管理交给 HiDeck 很省心;
- **断线切换的坑常常不在备用链路本身,而在主链路的"僵尸状态"**(本例:OVS 藏了物理断线 → 默认路由不撤 → 备用链路永远轮不到);
- **别跟上层管理器抢它管的配置**,换个"没人管的等价杠杆"(改 4G 那条 metric,而不是有线那条);
- **判断数值别用字符串匹配**,`metric 50` 会被 `metric 5000` 命中;
- **脚本别写死网卡名/模块编号**,用 `qmi_wwan` 驱动去认;
- 计量的链路上,**把大流量程序绑到主网卡**,再配一个"只在异常时才说话"的看门狗。
折腾的过程很烦,但结果是"真香":现在拔网线,我甚至感觉不到。
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: 过年期间的运动记录
tags: ["健康","运动"]
date: 2026-02-25
tags: [健康, 运动]
date: 2026-02-28T08:00:00Z
---
整个过年期间我的运动计划没有荒废,自认为执行的还行。
基本每天都有在运动,吃上面也很克制,不会暴饮暴食。
+2 -3
View File
@@ -1,9 +1,8 @@
---
title: 重新用回macos
tags: ["日常","mac"]
date: 2025-12-01
tags: [日常, mac]
date: 2025-12-04T08:00:00Z
summary: ""
---
最近几天,我重新用回了去年使用的macbookpro,屏幕有一条黑线,使用的是外接显示器
深感还是mac系统简洁好用,没有windows那么多的莫名其妙的bug
+3 -3
View File
@@ -1,8 +1,8 @@
---
title: 长期健康生活计划
tags: ["健康"]
date: 2026-01-17
summary: "人的一生最重要的就是健康"
tags: [健康]
date: 2026-01-20T08:00:00Z
summary: 人的一生最重要的就是健康
---
- 最近开始尝试更规律的生活,调整饮食与运动习惯。一方面是真的想对身体好一点,另一方面,也希望生活有个稳定的节奏,让自己心态更积极。
+7
View File
@@ -0,0 +1,7 @@
---
title: 面向死亡备份
tags: ["健康","备份","死亡","感想"]
date: 2026-07-10
---
面向死亡备份,这是个有意思的话题。
多年以后我不再了,我已不在人间,我的这些数据要怎么处理呢?
+4 -4
View File
@@ -1,6 +1,6 @@
---
title: "🔍 搜索"
layout: "search"
summary: "search"
placeholder: "搜索"
title: 🔍 搜索
layout: search
summary: search
placeholder: 搜索
---
+7 -3
View File
@@ -7,11 +7,10 @@ defaultContentLanguage = 'zh-cn'
[outputs]
home = ["HTML", "RSS", "JSON"]
# ===== 新增区域:告诉 Hugo 允许渲染安全的 HTML =====
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true # 允许在 Markdown 中渲染原始 HTML
unsafe = true
[params]
[params.homeInfoParams]
@@ -47,7 +46,12 @@ defaultContentLanguage = 'zh-cn'
url = "/search"
weight = 4
[[menu.main]]
name = "画廊"
url = "/gallery"
weight = 5
[[menu.main]]
name = "Run"
url = "https://run.butubb.cn"
weight = 5
weight = 6
+75
View File
@@ -0,0 +1,75 @@
{{- define "main" -}}
{{- if .Title }}
<header class="page-header">
<h1>{{ .Title }}</h1>
{{- with .Description }}
<div class="post-description">{{ . | markdownify }}</div>
{{- end }}
</header>
{{- end }}
<div class="gallery-page-content">
{{- $albums := where .Site.RegularPages "Type" "gallery" }}
{{- range $album := $albums }}
{{- $images := findRE `!\[.*?\]\((.*?)\)` $album.RawContent }}
{{- if $images }}
<section class="gallery-album">
<div class="gallery-album-header">
<h2 class="gallery-album-title">{{ $album.Title }}</h2>
<span class="gallery-album-count">{{ len $images }} 张</span>
</div>
{{- with $album.Params.description }}
<p class="gallery-album-desc">{{ . }}</p>
{{- end }}
<div class="gallery-grid">
{{- range $image := $images }}
{{- $url := replaceRE `!\[.*?\]\((.*?)\)` "$1" $image }}
<div class="gallery-card" data-src="{{ $url }}">
<div class="gallery-card-img">
<img src="{{ $url }}" alt="{{ $album.Title }}" loading="lazy">
</div>
</div>
{{- end }}
</div>
</section>
{{- end }}
{{- end }}
{{- if not $albums }}
<div class="gallery-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<p>暂无图片</p>
</div>
{{- end }}
</div>
<div class="lightbox" id="lightbox">
<div class="lightbox-backdrop" id="lightbox-backdrop"></div>
<div class="lightbox-body">
<img id="lightbox-img" src="" alt="">
</div>
<button class="lightbox-btn lightbox-close-btn" id="lightbox-close" title="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<button class="lightbox-btn lightbox-prev-btn" id="lightbox-prev" title="上一张">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15,18 9,12 15,6"/>
</svg>
</button>
<button class="lightbox-btn lightbox-next-btn" id="lightbox-next" title="下一张">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9,18 15,12 9,6"/>
</svg>
</button>
<div class="lightbox-counter" id="lightbox-counter"></div>
</div>
{{- end -}}
+9
View File
@@ -0,0 +1,9 @@
{{- if eq .Type "gallery" }}
{{- $galleryJS := resources.Get "js/gallery.js" | minify }}
{{- if not site.Params.assets.disableFingerprinting }}
{{- $galleryJS = $galleryJS | fingerprint }}
<script defer crossorigin="anonymous" src="{{ $galleryJS.RelPermalink }}" integrity="{{ $galleryJS.Data.Integrity }}"></script>
{{- else }}
<script defer crossorigin="anonymous" src="{{ $galleryJS.RelPermalink }}"></script>
{{- end }}
{{- end }}
+2
View File
@@ -22,6 +22,8 @@
Deploy on github pages
<br>
<a href="https://icp.gov.moe/?keyword=20250363" target="_blank" rel="noopener noreferrer">萌ICP备20250363号</a>
<br>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">皖ICP备2026019386号</a>
</span>
</footer>
{{- end }}