mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6mobile wallpaper 7mobile wallpaper 8mobile wallpaper 9mobile wallpaper 10mobile wallpaper 11mobile wallpaper 12mobile wallpaper 13mobile wallpaper 14mobile wallpaper 15mobile wallpaper 16mobile wallpaper 17mobile wallpaper 18mobile wallpaper 19mobile wallpaper 20mobile wallpaper 21mobile wallpaper 22
2891 字
8 分钟
Sun-Panel 的宿主机监控和登录会话调整
2026-05-29

本文中的域名、IP 地址、路径、Token 等信息均已脱敏处理。

前段时间我使用 docker 部署了 Sun-Panel 导航页(1.3.0 版本)。

部署完立马发现了一个问题:系统状态模块只能看到容器里的数据,看不到宿主机真实数据

没用几天又觉得登录会话太短,隔一阵子就得重新登录,很是麻烦,毕竟我都配置了 mTLS 了就没必要这么..嗯…严谨了。(详见: 《使用客户端证书保护 Sun-Panel》

这两个问题并不算严重,但都很烦。

前者会让系统监控失去它原本的作用,而官方把 docker 部署查看宿主机系统状态的能力放在了后面的闭源付费版本里,而我当时用的是 1.3.0,也就是最后一个开源版本。

后者则是纯粹的使用体验问题。

于是我就把源码拉下来,自己改了改。


先解决宿主机监控的问题#

最开始发现问题的时候,我使用的还是 docker 部署,后面换成了源码编译运行。


为什么放弃 Docker#

继续用 docker 也不是不行,但想了想,我后面本来就准备自己维护源码,如果继续坚持 Docker,那么后面的开发流程就会变成:

修改源码
重新构建镜像
重新部署容器
重新测试

整个过程会变得非常繁琐。,而且宿主机监控本身就是一个天然会涉及容器边界的问题。

既然迟早都要编译源码,那不如一步到位。

于是我开始研究本地构建流程。

最终部署方式也从 Docker 变成了源码编译后的原生运行。


从源码开始分析监控逻辑#

部署方式调整完成以后,宿主机监控的问题才真正开始解决。

最先处理的是磁盘监控。

顺着代码一路看下去,问题其实藏得很浅:

后端有一段 GetDiskMountpoints()

调用 gopsutil 读取系统所有分区
原样返回给前端

中间没有任何过滤。

也就是说,操作系统报告多少个挂载点,前端就会收到多少个。

而 Linux 和 macOS 都会报告大量虚拟文件系统:

tmpfs
devfs
overlay
proc
sysfs
autofs
nullfs
...

这些本身不是磁盘,但会原样出现在磁盘下拉框里,把真实磁盘淹没掉。


增加文件系统过滤#

原因清楚以后,处理方式就比较直接。

我在 GetDiskMountpoints()GetDiskInfo() 里加了同一层过滤,整体逻辑变成:

扫描全部分区
识别文件系统类型
命中黑名单就跳过
只返回真实挂载点

黑名单覆盖了 Linux 和 macOS 两边的常见虚拟 FS,大概三十项。

处理完以后,磁盘列表终于只剩下真正有意义的设备。


前端看起来却卡住了#

后端改完,构建部署。

结果发现磁盘监控根本添加不进去…

CPU 和内存都添加正常,唯独磁盘,选完挂载点,点确定,毫无反应。

这个问题折腾了挺久的,最后是通过浏览器开发者工具定位到的。

打开 Network 面板,导出 HAR,搜索保存请求,扔给ai检索,结果发现点确定的时候,根本就没有发出保存请求,也就是说问题不在后端拒绝,而是在前端就被拦住了。

继续查看 Console,发现一行 {warnings: undefined},点击跳转进 Naive UI 内部代码,是一段:

return w ? (console.log(w), Promise.resolve(!1)) : Promise.resolve(!0)

意思是验证通过返回 true,验证失败返回 false 并打印 warnings。

w{warnings: undefined},对象始终 truthy,所以被判定为”验证失败”。


这是 Naive UI 版本变化带来的存量 bug#

继续往上追,发现问题出在 DiskEditor/index.vueverification() 方法,原版代码是:

const errors = await formRef.value?.validate()
if (!errors) {
return Promise.resolve(true)
}

这段代码在旧版 Naive UI 上能正常工作,因为 validate() 验证通过时返回 undefined!undefinedtrue

但 Naive UI 升到 2.44 以后,validate() 验证通过时返回的是 {warnings: undefined} 对象。

对象始终 truthy,!{...} 永远是 false

于是验证明明通过了,却被判定为失败。

CPU 和内存监控不走这个 verification(),所以没事,只有磁盘监控被卡住。


解决方式:跳过这层验证#

Naive UI 跨版本的返回值不稳定,写兼容判断维护成本太高 (<—— 纯懒的)

而真正需要防的只有”空值”和”非法路径”两种情况:

  • NSelect 已经加了 filterable,只能从后端返回的列表里选,不会乱填
  • 后端 GetDiskInfoByPath 对无效路径有错误兜底

前端这层 form validate 其实是多余的。

于是直接把 verification() 简化成无条件返回 true:

请求验证
直接返回 true
交给 NSelect 和后端去兜底

处理完以后,三种监控都能正常添加了。


顺手把构建流程整理了一遍

既然已经开始维护源码,构建流程自然也要一起整理。

因为后面:

修改源码
编译
测试
部署

会不断重复。

如果每次都重新这样整一遍,那我大概率会疯掉的。


第一次跑 build.sh 就翻车

项目自带的 build.sh 原本是给 Linux CI 写的。

我第一次在 macOS 上跑 ./build.sh -r,一连暴露了四个问题:

pnpm 11 拒绝运行依赖的 postinstall 脚本
esbuild 和 vue-demi 安装失败
pnpm install 退出码 1
go install 装到 ~/go/bin 不在 PATH
go-bindata-assetfs: command not found
release 目录不存在
go build 找不到输出路径
bindata.go 没生成
Go 编译报 package not in std

每一个单拎出来可能没什么,但全部一起来就炸了。


顺手把 build.sh 也改了

既然都要改,干脆把 build.sh 一起修了:

pnpm install 加 --config.dangerously-allow-all-builds
解决 postinstall 审批失败
buildBackEndAssets 加 export PATH=<span data-math-id="math-0"></span>(go env GOPATH)/bin
go-bindata-assetfs 能被找到
_build 函数在 go build 前加 mkdir -p
release 目录自动创建
SUPPORTED_OSARCH 追加 darwin/arm64/clang
支持 macOS arm64 交叉编译
musl 静态编译加 uname -s 守卫
非 Linux 自动跳过

顺手还修了一个 build.sh 第 9 行的原作者遗留老 bug,VERSION 的命令替换语法写错了,${cut ...} 应该是 $(cut ...),导致版本号一直是空。


从 Windows 同步到 Mac 还有一道坎

build.sh 改完以后,本地构建基本能跑通了。

但脚本从 Windows 同步到 Mac 还会丢两样东西:

换行符:Windows 是 CRLF,Mac 上 bash 报 bad interpreter: /bin/bash^M
执行权限:.command 文件没 +x,双击报 permission denied

于是又写了一个 setup.command,首次同步后跑一次,自动修复这两个:

sed -i '' 's/\r$//' *.command
chmod +x *.command

还有几个隐藏的小坑

构建跑通以后,部署阶段又遇到两个问题。

第一个:.env 文件不在 git 里

前端构建脚本 add-frontend-version.js 强依赖 .env,但 .env 被 gitignore 忽略,从 Windows 同步过来时漏了(我居然能忘了这茬)。

解决方式是构建脚本检测到 .env 不存在时自动创建一份默认配置。

第二个:cp -r 合并拷贝不删旧文件

部署前端用 cp -r dist web,是合并拷贝,不会删掉 web/assets/ 里旧的 JS。

旧 hash 的 JS 残留,新 index.html 如果引用的是新 hash 还好,但偶尔会因为某些原因引用旧 hash,导致浏览器加载旧前端,改动”看起来没生效”。

解决方式是部署时先 rm -rf webcp -r dist web,彻底替换。


最后整理成一套脚本

把这些处理都固化下来以后,最终保留下来的是这几个:

setup.command 首次同步后跑一次,修 CRLF 和权限
build.command 一键构建 + 部署
start.command 一键启动

之后更新源码基本只需要:

./build.command

剩下的环境检查、.env 创建、bindata 生成、二进制编译、前端部署,全部交给脚本处理。

后面继续维护就轻松很多。


登录状态为什么总会失效#

宿主机监控解决以后,还有另一个一直存在的问题:登录状态总会莫名其妙失效。

最开始我以为是前端 Token 过期,后来顺着登录流程一路往下追,才发现 Sun-Panel 用的是双层 token 缓存:

前端 localStorage 里的 cToken
后端 CUserToken 缓存映射到 bToken
数据库 users.token 字段里的 bToken
后端 UserToken 缓存映射到完整用户信息

请求时前端只发 cToken,后端靠这两层缓存拿到用户。

cToken 这一层是会话寿命的真正瓶颈。

初始化代码如下:

return global.NewCache[string](
72*time.Hour,
48*time.Hour,
"CUserToken",
)

也就是说:

cToken 有效期:72 小时
清理间隔:48 小时

而且用的是绝对过期时间。


问题不只是这个 72 小时#

刚看到这里的时候。

最容易想到的办法其实是:

72*time.Hour
30*24*time.Hour

直接把时间改长。

但继续往下看以后发现并没有这么简单,因为绝对过期时间是缓存创建时就开始计时的,不会因为用户持续访问而刷新。

简单来说就是:

周一登录
一直正常使用
周四到达 72 小时
掉登录

即使这三天里每天都在使用,结果也不会改变。


增加滑动续期#

真正的问题其实是缺少续期机制,所以登录中间件那一步要加一次缓存刷新。

原来的流程:

请求进来
读取 CUserToken
命中就放行
返回数据

修改以后:

请求进来
读取 CUserToken
命中就刷新过期时间
再放行
返回数据

对应代码其实只有一行:

global.CUserToken.SetDefault(cToken, token)

用的是 SetDefault 而不是带时长参数的 Set

这样续期时长会跟着初始化配置走。

以后想改 TTL 只动一个地方,不会出现”初始化改了 60 天,中间件还在续 30 天”这种常量散落问题。


同时把有效期调整到 30 天#

既然已经改了续期逻辑。

顺手把默认有效期也一起调整。

从:

72 小时

改成:

30 天

清理间隔顺手从 48 小时改成 24 小时。

最终得到的行为变成:

30 天内有任何访问
自动续期
保持登录
连续 30 天没有访问
登录失效
重新登录

对于导航页这种长期驻留应用来说,这才是更符合我实际使用的习惯。


为什么不动 UserToken 的 1 分钟过期#

其实我改的时候差点顺手把 UserToken 也一起延长,因为这个缓存的默认过期只有 1 分钟,看起来比 cToken 还短。

但 LoginInterceptor 里有一段 DB 兜底逻辑:

UserToken miss
查数据库 users.token
查到就回填缓存
继续放行

数据库里的 bToken 是持久存的。,所以这 1 分钟只影响性能,不影响登录寿命。

如果改长,反而可能会引入新问题:

我改头像
DB 已更新
UserToken 缓存里还是旧的
最多 10 分钟界面不刷新

所以这一层保持不动。


改密码后老 token 还是会失效#

滑动续期加上以后,自然会担心一个问题:

如果改了密码,老 cToken 还在缓存里,那是不是还能继续用?

那必须是不能啊,因为改密码的接口会把数据库里的 users.token 字段清空:

我改密码
DB users.token = ""
UserToken 缓存清掉
老 cToken 还在 CUserToken 里
但下次请求会走 DB 兜底
DB 查不到 token
返回 1001 踢回登录页

滑动续期只刷新 cToken 的 TTL,它管不了已经失效的 DB token,所以安全性不受影响。


退出登录也正常生效#

退出登录的接口是挂在 LoginInterceptor 下面的,也就是说:

请求 /logout
LoginInterceptor 先续期 cToken
Logout 函数里 Delete cToken
续期被 Delete 覆盖
cToken 被清掉

时序上续期在前,Delete 在后,最终结果就是 cToken 正确清理,退出登录不受影响。


重启进程还是要重登一次#

这两层缓存都是 go-cache 内存驱动,数据全在进程内存里。

所以重启 sun-panel 的时候:

进程退出
内存清零
所有 cToken 丢失
所有账号都需要重新登录

这个是这次没解决的,但也是我能接受的。

彻底解决要么装 Redis,要么换成不走缓存的 LoginInterceptorDev

但我觉得没啥必要,不想折腾了。


改完需要重新登录一次#

对了,还有一个容易忽略的点。

旧 cToken 是按 72 小时老配置生成的。

改完代码部署以后,哪怕 cToken 还没过期,它也只会按老配置的 TTL 继续走,必须主动退出重登一次,才能拿到按新 30 天 TTL 生成的新 cToken。


最终效果#

这几轮修改完成以后。

Sun-Panel 基本已经变成了我真正想要的样子。

监控部分:

能够看到宿主机真实状态

构建部分:

能够快速重新编译和部署

登录部分:

30 天滑动续期

这些改动看起来都不算大,但实际使用体验好了不少,比直接增加几个新功能带来的体验明显得多。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时

目录