fix:system info dialog and ai assistant

This commit is contained in:
samwaf
2026-08-06 10:47:56 +08:00
parent 32a9254db8
commit 14f6d39f92
11 changed files with 596 additions and 20 deletions
+107 -14
View File
@@ -1,6 +1,7 @@
package api
import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/common/response"
@@ -293,6 +294,75 @@ func (w *WafGPTApi) SaveGptConfigApi(c *gin.Context) {
response.OkWithMessage("保存成功", c)
}
// ============ 对话场景与系统提示词 ============
// AI 助手是多入口共用的:日志详情点"AI分析"、OWASP 规则解读、右下角自由提问。
// 以前不管从哪来都硬套"信息安全专家 + 风险等级/风险类型/风险说明"这套格式,
// 结果随便问一句也被套成风险报告。这里按场景分开,前端发 scene,后端选提示词。
const (
GptSceneSecurityLog = "security_log" // 日志详情:分析这条请求的安全风险
GptSceneOwaspRule = "owasp_rule" // OWASP CRS 规则解读
GptSceneGeneral = "general" // 通用问答(默认)
)
const gptSystemPromptSecurityLog = `你是一位信息安全专家,正在分析 SamWaf(Web 应用防火墙)记录到的一条 HTTP 请求。请判断这条请求是否存在攻击特征,输出如下格式:
风险等级: 0-100
风险类型:某种注入,跨站等
风险说明:对风险的阐释`
// gptDocRefs 官方资料索引。
// 注意:对话走的是标准 chat/completions,没有联网/检索工具,模型打不开任何网页。
// 所以这里不是"让它去搜",而是给它一份**准确的地址清单**,让它把用户导向官方文档和 issues,
// 同时明确禁止它编造链接或假装读过页面——否则模型很容易凭空造出 /guide/XXX.html。
const gptDocRefs = `
# 官方资料(你没有联网能力,只能引用下面列出的地址;禁止编造其它链接,也不要声称自己读过页面内容)
- 文档站首页:https://doc.samwaf.com/
- 功能文档地址格式:https://doc.samwaf.com/guide/<页面名>.html(英文界面把 /guide/ 换成 /en/guide/)
- 可用的页面名(只能从这里挑,拿不准就只给文档站首页):
Host 网站配置 | Rule 防护规则 | CC CC防护 | Owasp OWASP规则集 | AIDetection AI智能检测 |
IPBlack IP黑名单 | IPWhite IP白名单 | IPGroup IP组 | ThreatIP 威胁情报IP订阅 | FirewallIPBlock 防火墙IP封禁 |
UrlBlack URL限制访问 | UrlWhite URL白名单 | Sensitive 敏感词 | Ldp URL隐私防护 | Spider 爬虫识别 |
SSL SSL证书管理 | CDNIP CDN回源IP | Tunnel 隧道防护 | CacheRule 缓存规则 | Application 应用管理 |
AttackLog 风险日志 | VisitLog 访问日志 | Analysis 数据分析 | BlockingPage 自定义拦截页 |
NotifyChannel 通知渠道 | NotifySubscription 通知订阅 | NotifyLog 通知日志 |
HttpAuthBase 网站访问认证 | AccessConfig 认证配置 | AccessAccount 访问账号 | AccessSession 在线会话 | AccessAudit 认证审计 |
Account 账号管理 | Otp 双因素认证 | SystemConfig 系统配置 | VpConfig 参数设置 | PrivateInfo 密钥管理 |
OpenPlatform 开放平台 | SqlQuery SQL查询 | Task 任务管理 | BatchTask 批量任务 | DataRetention 数据保留策略 |
IPLocation IP数据库 | FileManage 文件管理 | RuntimeInfo 运行信息 | SysLog 系统日志 | OneKeyMod 一键修改
- 常见问题:https://doc.samwaf.com/faq/
- 安装与升级:https://doc.samwaf.com/quickstart/ 、https://doc.samwaf.com/quickstart/Update.html
- 已知问题与反馈:https://github.com/samwafgo/SamWaf/issues
引导用户自己检索时,给出可直接点击的搜索地址:https://github.com/samwafgo/SamWaf/issues?q=is%3Aissue+关键词(多个关键词用 + 连接)
引用规则:
1. 回答涉及某个具体功能时,在结尾附上对应文档页链接。
2. 用户描述的现象像 bug、或你不确定当前版本是否支持,就直说"建议到 issues 搜一下,没有就提一个",并给出上面的 issues 搜索地址。
3. 只给地址,不要转述你没有的页面内容,也不要编造文档里的章节名或配置项名称。`
const gptSystemPromptOwaspRule = `你是 OWASP ModSecurity 核心规则集(CRS)专家,正在帮用户读懂 SamWaf 里的一条 CRS 规则。请用简洁中文说明:
1. 这条规则想拦什么攻击;
2. 匹配条件的含义(变量、操作符、转换函数分别起什么作用);
3. 常见误报场景;
4. 如果确认是误报,建议怎么处理(加白名单、调整阈值/等级、或对该规则单独放行)。
不要输出与规则无关的内容。` + gptDocRefs
const gptSystemPromptGeneral = `你是 SamWaf(一款开源轻量级 Web 应用防火墙)的智能助手,熟悉 Web 安全、网站防护、反向代理、SSL 证书、WAF 规则与告警等话题。
请直接回答用户的问题:用与用户提问相同的语言(默认中文),简洁作答,必要时分点说明;不知道就说不知道,不要编造 SamWaf 不存在的功能。
不要给回答强行套用固定模板或风险评分格式,除非用户明确要求。` + gptDocRefs
// gptSystemPromptByScene 按场景取系统提示词,未知场景一律按通用问答处理
func gptSystemPromptByScene(scene string) string {
switch strings.TrimSpace(strings.ToLower(scene)) {
case GptSceneSecurityLog:
return gptSystemPromptSecurityLog
case GptSceneOwaspRule:
return gptSystemPromptOwaspRule
default:
return gptSystemPromptGeneral
}
}
// 新增用于解析流式响应的结构体
type StreamResponse struct {
ID string `json:"id"`
@@ -322,6 +392,20 @@ type TokenUsage struct {
TotalTokens int `json:"total_tokens"`
}
// buildDeltaPayload 组装一条加密后的 SSE 消息体
func buildDeltaPayload(content string, role string) (string, error) {
encryptStr, _ := wafsec.AesEncrypt([]byte(content), global.GWAF_COMMUNICATION_KEY)
msg := Delta{
Content: encryptStr,
Role: &role,
}
payload, err := json.Marshal(msg)
if err != nil {
return "", err
}
return string(payload), nil
}
// SendDeltaMessage 发送信息
func SendDeltaMessage(messageChan chan<- string, content string, role ...string) {
// 设置默认角色为 assistant
@@ -329,23 +413,25 @@ func SendDeltaMessage(messageChan chan<- string, content string, role ...string)
if len(role) > 0 {
r = role[0]
}
encryptStr, _ := wafsec.AesEncrypt([]byte(content), global.GWAF_COMMUNICATION_KEY)
// 创建消息结构
msg := Delta{
Content: encryptStr,
Role: &r,
}
// 序列化并发送
if payload, err := json.Marshal(msg); err == nil {
messageChan <- string(payload)
if payload, err := buildDeltaPayload(content, r); err == nil {
messageChan <- payload
}
}
func (w *WafGPTApi) ChatApi(c *gin.Context) {
var req request.WafGptSendReq
err := c.ShouldBindJSON(&req)
if err == nil {
if err != nil {
// 以前这里静默返回,前端表现是"点了发送但气泡一直空着",很难排查。
// 注意 StreamMiddleware 已经把 Content-Type 定成 text/event-stream 了,
// 这时候再 c.JSON 前端也当流读,所以错误也顺着 SSE 回,气泡里能直接看到。
zlog.Error("GPT对话请求解析失败:" + err.Error())
if payload, buildErr := buildDeltaPayload("对话请求解析失败:"+err.Error(), "assistant"); buildErr == nil {
c.SSEvent("message", payload)
}
return
}
{
// 创建一个取消信号通道,用于触发异常退出
stopChan := make(chan bool)
messageChan := make(chan string)
@@ -355,11 +441,18 @@ func (w *WafGPTApi) ChatApi(c *gin.Context) {
defer close(stopChan)
defer close(messageChan)
// 构造基础消息数组
// 未配置密钥时不去请求空地址,直接回一条可读提示,前端也会在打开助手时先检测并引导配置
if !IsGPTConfigured() {
SendDeltaMessage(messageChan, "尚未配置AI密钥,请点击【AI 参数设置】填写接口地址/模型/密钥后再使用。", "assistant")
stopChan <- true
return
}
// 构造基础消息数组:系统提示词按场景取(日志分析/规则解读/通用问答)
messages := []model.GptMessage{
{
Content: "你是一位信息安全专家,输出如下格式:\n\n风险等级: 0-100\n风险类型:某种注入,跨站等\n风险说明:对风险的阐释",
Role: "user",
Content: gptSystemPromptByScene(req.Scene),
Role: "system",
},
}
// 将History内容转换为消息并追加
+42
View File
@@ -109,6 +109,48 @@ func (w *WafSysInfoApi) SysVersionApi(c *gin.Context) {
}, "获取成功", c)
}
// SysRuntimeInfoApi 获取系统运行环境信息
// @Summary 获取系统运行环境信息
// @Description 返回操作系统类型/发行版及版本、内核、容器(docker/k8s)、虚拟化、运行时长与软件版本,便于反馈问题时定位环境
// @Tags 系统信息
// @Produce json
// @Success 200 {object} response.Response{data=model.RuntimeSystemInfo} "获取成功"
// @Security ApiKeyAuth
// @Router /sysinfo/runtimeinfo [get]
func (w *WafSysInfoApi) SysRuntimeInfoApi(c *gin.Context) {
//探测过程全部容错:任何一项拿不到就留空,保证接口一定有返回
detail := utils.GetOSDetail()
info := model.RuntimeSystemInfo{
Version: global.GWAF_RELEASE_VERSION,
VersionName: global.GWAF_RELEASE_VERSION_NAME,
VersionRelease: global.GWAF_RELEASE,
OS: detail.GOOS,
Arch: detail.GOARCH,
GoVersion: detail.GoVersion,
OSName: detail.OSName,
Platform: detail.Platform,
PlatformVersion: detail.PlatformVersion,
KernelVersion: detail.KernelVersion,
KernelArch: detail.KernelArch,
IsWindows: detail.IsWindows,
IsWin7Build: global.GWAF_RUNTIME_WIN7_VERSION == "true",
IsWin7Kernel: detail.IsWin7Kernel,
Container: detail.Container,
InKubernetes: detail.InKubernetes,
IsWSL: detail.IsWSL,
Virtualization: detail.Virtualization,
RunAsService: global.GWAF_RUNTIME_SERVER_TYPE,
SystemUptimeSeconds: int64(utils.GetSystemUptimeSeconds()),
ProcessUptimeSeconds: int64(time.Since(global.GWAF_RUNTIME_PROCESS_START_TIME).Seconds()),
}
response.OkWithDetailed(info, "获取成功", c)
}
func (w *WafSysInfoApi) CheckVersionApi(c *gin.Context) {
if global.GWAF_RUNTIME_IS_UPDATETING == true {
response.FailWithMessage("正在升级中...请在消息等待结果", c)
+54
View File
@@ -2,6 +2,7 @@ package api
import (
"SamWaf/global"
"SamWaf/model"
"SamWaf/wafsec"
"encoding/json"
"github.com/gin-gonic/gin"
@@ -23,6 +24,59 @@ type ComplexData struct {
VersionRelease string `json:"version_release"`
}
// 测试用例 SysRuntimeInfoApi:系统信息探测必须永远有返回,不能因为取不到某项而失败
func TestSysRuntimeInfoApi(t *testing.T) {
r := gin.Default()
global.GWAF_RELEASE_VERSION = "v1.0.0"
global.GWAF_RELEASE_VERSION_NAME = "20241028"
r.GET("/api/v1/sysinfo/runtimeinfo", new(WafSysInfoApi).SysRuntimeInfoApi)
req, err := http.NewRequest(http.MethodGet, "/api/v1/sysinfo/runtimeinfo", nil)
if err != nil {
t.Fatalf("无法创建请求:%v", err)
}
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
t.Logf("响应体内容: %s", rec.Body.String())
if rec.Code != http.StatusOK {
t.Errorf("期望的状态码:%d,实际状态码:%d", http.StatusOK, rec.Code)
}
var response WafCheckVersionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatalf("响应解析失败:%v", err)
}
if response.Code != 0 {
t.Errorf("期望的响应码:0,实际:%v", response.Code)
}
// 响应体默认经通讯密钥加密,先解密再解析
payload := response.Data
var encrypted string
if json.Unmarshal(response.Data, &encrypted) == nil {
decrypted, err := wafsec.AesDecrypt(encrypted, global.GWAF_COMMUNICATION_KEY)
if err != nil {
t.Fatalf("响应解密失败:%v", err)
}
payload = decrypted
}
var info model.RuntimeSystemInfo
if err := json.Unmarshal(payload, &info); err != nil {
t.Fatalf("系统信息解析失败:%v", err)
}
if info.OS == "" || info.Arch == "" || info.GoVersion == "" {
t.Errorf("编译相关信息不应为空:%+v", info)
}
if info.OSName == "" {
t.Error("操作系统名称至少应有兜底值")
}
if info.Version != "v1.0.0" || info.VersionName != "20241028" {
t.Errorf("软件版本信息不正确:%+v", info)
}
if info.ProcessUptimeSeconds < 0 {
t.Errorf("程序运行时长不应为负:%v", info.ProcessUptimeSeconds)
}
}
// 测试用例 CheckVersionApi
func TestCheckVersionApi(t *testing.T) {
// 创建一个基于 Gin 的引擎
+2
View File
@@ -63,6 +63,8 @@ var (
GWAF_RUNTIME_CURRENT_TUNNELPORT string = "" //当前程序所占用隧道端口
GWAF_RUNTIME_CURRENT_EXPORT_DB_LOG_FILE_PATH string = "" //生成的日志导出文件路径
GWAF_RUNTIME_PROCESS_START_TIME time.Time = time.Now() //当前进程启动时间(包加载即赋值)
GWAF_RUNTIME_SSL_EXPIRE_CHECK bool = false //SSL过期检测是否正在运行
GWAF_RUNTIME_SSL_SYNC_HOST bool = false //主机同步信息到过期检测是否正在运行
/**
+3
View File
@@ -2,6 +2,9 @@ package request
type WafGptSendReq struct {
History [][]string `json:"history"` // 定义结构体
// Scene 对话场景,决定用哪套系统提示词:
// security_log 日志安全分析 / owasp_rule OWASP规则解读 / general 通用问答(默认)
Scene string `json:"scene"`
}
// WafGptRuleReq AI 生成自定义规则请求
+35
View File
@@ -8,3 +8,38 @@ type VersionInfo struct {
VersionNew string `json:"version_new"`
VersionDesc string `json:"version_desc"`
}
// RuntimeSystemInfo 系统运行环境信息(供前端版本弹窗展示,便于用户反馈问题时定位环境)
// 说明:所有字段都是"取不到就为空/零值",前端按需隐藏,不会因为某项探测失败而整体失败
type RuntimeSystemInfo struct {
//软件信息
Version string `json:"version"` //软件版本Code 如 v1.0.0
VersionName string `json:"version_name"` //软件版本 如 20241028
VersionRelease string `json:"version_release"` //是否正式版 "true"/"false"
//编译信息
OS string `json:"os"` //编译目标系统 windows/linux/darwin
Arch string `json:"arch"` //编译目标架构 amd64/arm64
GoVersion string `json:"go_version"` //编译器版本
//操作系统信息
OSName string `json:"os_name"` //操作系统名称 如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
Platform string `json:"platform"` //平台 如 ubuntu
PlatformVersion string `json:"platform_version"` //平台版本 如 24.04
KernelVersion string `json:"kernel_version"` //内核版本
KernelArch string `json:"kernel_arch"` //内核架构(真实机器架构)
//特殊环境标记(前端按条件展示)
IsWindows bool `json:"is_windows"` //是否 Windows 系统
IsWin7Build bool `json:"is_win7_build"` //当前二进制是否 Win7 专版
IsWin7Kernel bool `json:"is_win7_kernel"` //当前系统是否 Win7/Win8/Server2008R2 内核
Container string `json:"container"` //容器类型:空=非容器;docker/podman/containerd/lxc
InKubernetes bool `json:"in_kubernetes"` //是否运行在 Kubernetes 中
IsWSL bool `json:"is_wsl"` //是否运行在 WSL 中
Virtualization string `json:"virtualization"` //虚拟化类型 如 kvm/vmware/hyperv,空=未识别
RunAsService bool `json:"run_as_service"` //是否以系统服务方式启动
//运行时长
SystemUptimeSeconds int64 `json:"system_uptime_seconds"` //操作系统已运行时长(秒),0=未取到
ProcessUptimeSeconds int64 `json:"process_uptime_seconds"` //SamWaf 进程已运行时长(秒)
}
+1
View File
@@ -16,6 +16,7 @@ func (receiver *WebSysInfoRouter) InitSysInfoRouter(group *gin.RouterGroup) {
router := group.Group("")
router.GET("/api/v1/sysinfo/version", api.SysVersionApi)
router.GET("/api/v1/sysinfo/checkversion", api.CheckVersionApi)
router.GET("/api/v1/sysinfo/runtimeinfo", api.SysRuntimeInfoApi)
// N7:自升级/回滚属高危系统操作,仅系统管理员(或超管)可执行
router.GET("/api/v1/sysinfo/update", middleware.RequireRole(enums.ROLE_SYSTEM_ADMIN), api.UpdateApi)
router.GET("/api/v1/sysinfo/announcement", api.GetAnnouncementApi)
+46 -6
View File
@@ -13,6 +13,7 @@ import (
"github.com/shirou/gopsutil/v4/host"
"runtime"
"strconv"
"strings"
"time"
)
@@ -606,15 +607,34 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
Value: time.Unix(0,
global.GWAF_MEASURE_PROCESS_DEQUEENGINE.ReadData()*int64(time.Millisecond)).Format("2006-01-02 15:04:05")})
data = append(data, response2.WafNameValue{Name: "Goroutine数量", Value: fmt.Sprintf("%v", runtime.NumGoroutine())})
osDetail := utils.GetOSDetail()
data = append(data, response2.WafNameValue{Name: "系统类型", Value: fmt.Sprintf("%v", runtime.GOOS)})
data = append(data, response2.WafNameValue{Name: "系统架构", Value: fmt.Sprintf("%v", runtime.GOARCH)})
data = append(data, response2.WafNameValue{Name: "编译器版本", Value: fmt.Sprintf("%v", runtime.Version())})
data = append(data, response2.WafNameValue{Name: "Win7内核", Value: func() string {
if global.GWAF_RUNTIME_WIN7_VERSION == "true" {
return "是"
// 具体操作系统发行版及版本,如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
if osDetail.OSName != "" {
data = append(data, response2.WafNameValue{Name: "操作系统", Value: osDetail.OSName})
}
if osDetail.KernelVersion != "" {
kernel := osDetail.KernelVersion
if osDetail.KernelArch != "" {
kernel = kernel + " (" + osDetail.KernelArch + ")"
}
return "否"
}()})
data = append(data, response2.WafNameValue{Name: "内核版本", Value: kernel})
}
// 运行环境:容器/K8s/WSL/虚拟化,只有识别到才展示
if env := describeRuntimeEnv(osDetail); env != "" {
data = append(data, response2.WafNameValue{Name: "运行环境", Value: env})
}
data = append(data, response2.WafNameValue{Name: "编译器版本", Value: fmt.Sprintf("%v", runtime.Version())})
// Win7内核只在 Windows 下才有意义,其它系统不展示
if osDetail.IsWindows {
data = append(data, response2.WafNameValue{Name: "Win7内核", Value: func() string {
if global.GWAF_RUNTIME_WIN7_VERSION == "true" {
return "是"
}
return "否"
}()})
}
// 获取开机时间
boottime, _ := host.BootTime()
ntime := time.Now().Unix()
@@ -631,6 +651,8 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
data = append(data, response2.WafNameValue{
Name: "系统已运行时长", Value: fmt.Sprintf("%v 天 %v 时 %v 分 %v 秒", days, hours, minutes, seconds)})
data = append(data, response2.WafNameValue{
Name: "程序已运行时长", Value: utils.FormatDurationCN(int64(time.Since(global.GWAF_RUNTIME_PROCESS_START_TIME).Seconds()))})
data = append(data, response2.WafNameValue{Name: "软件版本", Value: fmt.Sprintf("%v", global.GWAF_RELEASE_VERSION_NAME)})
data = append(data, response2.WafNameValue{Name: "软件版本Code", Value: fmt.Sprintf("%v", global.GWAF_RELEASE_VERSION)})
@@ -643,3 +665,21 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
return data
}
// describeRuntimeEnv 把容器/K8s/WSL/虚拟化信息拼成一句可读描述,都没识别到就返回空字符串
func describeRuntimeEnv(detail utils.OSDetail) string {
var parts []string
if detail.Container != "" {
parts = append(parts, "容器("+detail.Container+")")
}
if detail.InKubernetes {
parts = append(parts, "Kubernetes")
}
if detail.IsWSL {
parts = append(parts, "WSL")
}
if detail.Virtualization != "" {
parts = append(parts, "虚拟化("+detail.Virtualization+")")
}
return strings.Join(parts, " / ")
}
+1
View File
@@ -239,6 +239,7 @@ var registeredRoutes = []route{
// ── 系统信息 ───────────────────────────────────────────────────
{"GET", "/api/v1/sysinfo/version", "获取版本信息"},
{"GET", "/api/v1/sysinfo/runtimeinfo", "获取系统运行环境信息"},
// ── 系统日志 ───────────────────────────────────────────────────
{"GET", "/api/v1/sys_log/list", "获取系统日志列表"},
+252
View File
@@ -0,0 +1,252 @@
package utils
import (
"SamWaf/common/zlog"
"bufio"
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"github.com/shirou/gopsutil/v4/host"
)
// OSDetail 运行环境详情:用于版本弹窗/运行参数展示,方便用户反馈问题时定位环境
type OSDetail struct {
GOOS string `json:"goos"` //编译目标系统 windows/linux/darwin
GOARCH string `json:"goarch"` //编译目标架构 amd64/arm64
GoVersion string `json:"go_version"` //编译器版本
OSName string `json:"os_name"` //友好的操作系统名称 如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
Platform string `json:"platform"` //平台 如 ubuntu / centos / Microsoft Windows 10 Pro
PlatformFamily string `json:"platform_family"` //平台家族 如 debian / rhel
PlatformVersion string `json:"platform_version"` //平台版本 如 24.04
KernelVersion string `json:"kernel_version"` //内核版本
KernelArch string `json:"kernel_arch"` //内核架构(真实机器架构,32位程序跑在64位系统时与 GOARCH 不同)
IsWindows bool `json:"is_windows"` //是否 Windows 系统
IsWin7Kernel bool `json:"is_win7_kernel"` //当前系统是否是 Win7/Win8/Server2008R2 内核(6.1/6.2)
Container string `json:"container"` //容器类型:空=非容器;docker/podman/containerd/lxc/oci
InKubernetes bool `json:"in_kubernetes"` //是否运行在 Kubernetes 中
IsWSL bool `json:"is_wsl"` //是否运行在 WSL 中
Virtualization string `json:"virtualization"` //虚拟化类型(仅 guest 角色时给出) 如 kvm/vmware/hyperv
}
var (
osDetailOnce sync.Once
osDetailCache OSDetail
errHostInfoPanic = errors.New("读取主机信息发生异常")
)
// GetOSDetail 获取运行环境详情(静态信息,进程内只探测一次)
// 探测过程全部容错:任何一项取不到就留空,不会返回错误、也不会 panic
func GetOSDetail() OSDetail {
osDetailOnce.Do(func() {
// 基础信息不依赖任何系统调用,先兜底填上,保证即使后续探测异常也有值可展示
osDetailCache = OSDetail{
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
GoVersion: runtime.Version(),
IsWindows: runtime.GOOS == "windows",
OSName: runtime.GOOS,
}
defer func() {
// gopsutil 在部分定制/裁剪系统上可能 panic,这里兜住,保留已填充的基础信息
if err := recover(); err != nil {
zlog.Warn("获取系统运行环境信息异常", err)
}
}()
osDetailCache = buildOSDetail()
})
return osDetailCache
}
func buildOSDetail() OSDetail {
detail := OSDetail{
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
GoVersion: runtime.Version(),
IsWindows: runtime.GOOS == "windows",
IsWin7Kernel: safeBool(IsSupportedWindows7Version),
}
if info, err := safeHostInfo(); err == nil && info != nil {
detail.Platform = info.Platform
detail.PlatformFamily = info.PlatformFamily
detail.PlatformVersion = info.PlatformVersion
detail.KernelVersion = info.KernelVersion
detail.KernelArch = info.KernelArch
// 只有作为 guest 运行时虚拟化类型才有意义,宿主机(host 角色)不展示
if strings.EqualFold(info.VirtualizationRole, "guest") {
detail.Virtualization = info.VirtualizationSystem
}
}
detail.OSName = buildOSName(detail)
detail.Container = detectContainerRuntime()
detail.InKubernetes = os.Getenv("KUBERNETES_SERVICE_HOST") != ""
detail.IsWSL = detectWSL()
return detail
}
// GetSystemUptimeSeconds 获取系统已运行时长(秒),取不到返回 0,不会 panic
func GetSystemUptimeSeconds() (seconds uint64) {
defer func() {
if r := recover(); r != nil {
zlog.Warn("读取系统运行时长异常", r)
seconds = 0
}
}()
up, err := host.Uptime()
if err != nil {
zlog.Warn("读取系统运行时长失败:" + err.Error())
return 0
}
return up
}
// FormatDurationCN 把秒数格式化为 "x 天 x 时 x 分 x 秒"
func FormatDurationCN(totalSeconds int64) string {
if totalSeconds < 0 {
totalSeconds = 0
}
seconds := totalSeconds % 60
minutes := (totalSeconds / 60) % 60
hours := (totalSeconds / 3600) % 24
days := totalSeconds / 86400
return fmt.Sprintf("%v 天 %v 时 %v 分 %v 秒", days, hours, minutes, seconds)
}
// safeHostInfo 调用 gopsutil 获取主机信息,任何 panic 都转成错误返回
func safeHostInfo() (info *host.InfoStat, err error) {
defer func() {
if r := recover(); r != nil {
zlog.Warn("读取主机信息异常", r)
info = nil
err = errHostInfoPanic
}
}()
return host.Info()
}
// safeBool 执行可能触发系统调用的判断函数,异常时返回 false
func safeBool(fn func() bool) (result bool) {
defer func() {
if r := recover(); r != nil {
zlog.Warn("系统信息判断异常", r)
result = false
}
}()
return fn()
}
// buildOSName 拼一个尽量可读的操作系统名称
func buildOSName(detail OSDetail) string {
// Linux 优先取 /etc/os-release 的 PRETTY_NAME,如 "Ubuntu 24.04.1 LTS"、"Alpine Linux v3.20"
if runtime.GOOS == "linux" {
if pretty := readOSReleasePrettyName(); pretty != "" {
return pretty
}
}
name := strings.TrimSpace(detail.Platform)
version := strings.TrimSpace(detail.PlatformVersion)
if name == "" {
name = runtime.GOOS
}
if version != "" && !strings.Contains(name, version) {
name = name + " " + version
}
return name
}
// readOSReleasePrettyName 读取 /etc/os-release(或 /usr/lib/os-release) 中的 PRETTY_NAME
func readOSReleasePrettyName() string {
for _, path := range []string{"/etc/os-release", "/usr/lib/os-release"} {
f, err := os.Open(path)
if err != nil {
continue
}
scanner := bufio.NewScanner(f)
pretty := ""
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "PRETTY_NAME=") {
continue
}
pretty = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), `"'`)
break
}
f.Close()
if pretty != "" {
return pretty
}
}
return ""
}
// detectContainerRuntime 粗略判断当前进程运行在什么容器里,非容器返回空字符串
func detectContainerRuntime() string {
if runtime.GOOS != "linux" {
return ""
}
// docker 官方镜像会写入该标记文件
if _, err := os.Stat("/.dockerenv"); err == nil {
return "docker"
}
// podman/buildah 的标记文件
if _, err := os.Stat("/run/.containerenv"); err == nil {
return "podman"
}
// systemd-nspawn / podman / lxc 会注入 container 环境变量
if env := strings.ToLower(strings.TrimSpace(os.Getenv("container"))); env != "" {
return env
}
// 退回到 cgroup 特征判断(cgroup v1 常见;v2 下 /proc/1/cgroup 可能只有 "0::/")
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
content := strings.ToLower(string(data))
switch {
case strings.Contains(content, "docker"):
return "docker"
case strings.Contains(content, "kubepods"):
return "containerd"
case strings.Contains(content, "containerd"):
return "containerd"
case strings.Contains(content, "lxc"):
return "lxc"
}
}
// cgroup v2 场景:挂载信息里通常能看到 overlay 根文件系统与容器运行时目录
if data, err := os.ReadFile("/proc/self/mountinfo"); err == nil {
content := strings.ToLower(string(data))
switch {
case strings.Contains(content, "/docker/containers/"):
return "docker"
case strings.Contains(content, "/containers/storage/"):
return "podman"
case strings.Contains(content, "/kubelet/pods/"):
return "containerd"
}
}
return ""
}
// detectWSL 判断是否运行在 WSL(Windows Subsystem for Linux)
func detectWSL() bool {
if runtime.GOOS != "linux" {
return false
}
if os.Getenv("WSL_DISTRO_NAME") != "" || os.Getenv("WSL_INTEROP") != "" {
return true
}
data, err := os.ReadFile("/proc/version")
if err != nil {
return false
}
content := strings.ToLower(string(data))
return strings.Contains(content, "microsoft") || strings.Contains(content, "wsl")
}
+53
View File
@@ -0,0 +1,53 @@
package utils
import (
"runtime"
"testing"
)
// 探测失败也必须给出兜底值,不能 panic、不能整体为空
func TestGetOSDetail(t *testing.T) {
detail := GetOSDetail()
if detail.GOOS != runtime.GOOS || detail.GOARCH != runtime.GOARCH {
t.Errorf("编译信息不正确: %+v", detail)
}
if detail.GoVersion == "" {
t.Error("编译器版本不应为空")
}
if detail.OSName == "" {
t.Error("操作系统名称至少要有兜底值")
}
if detail.IsWindows != (runtime.GOOS == "windows") {
t.Errorf("IsWindows 判断不正确: %v", detail.IsWindows)
}
// 非 Windows 不可能是 Win7 内核
if runtime.GOOS != "windows" && detail.IsWin7Kernel {
t.Error("非 Windows 系统不应判定为 Win7 内核")
}
// 非 Linux 不做容器/WSL 判定
if runtime.GOOS != "linux" && (detail.Container != "" || detail.IsWSL) {
t.Errorf("非 Linux 不应识别出容器/WSL: container=%v wsl=%v", detail.Container, detail.IsWSL)
}
t.Logf("运行环境: %+v", detail)
t.Logf("系统已运行: %v", FormatDurationCN(int64(GetSystemUptimeSeconds())))
}
func TestFormatDurationCN(t *testing.T) {
cases := []struct {
seconds int64
want string
}{
{0, "0 天 0 时 0 分 0 秒"},
{-10, "0 天 0 时 0 分 0 秒"}, //负数按 0 处理,不能出现负值展示
{59, "0 天 0 时 0 分 59 秒"},
{3661, "0 天 1 时 1 分 1 秒"},
{262145, "3 天 0 时 49 分 5 秒"},
}
for _, c := range cases {
if got := FormatDurationCN(c.seconds); got != c.want {
t.Errorf("FormatDurationCN(%v)=%v, 期望 %v", c.seconds, got, c.want)
}
}
}