feat: 飞牛NAS成本审核更新 - 新增SKU、更新价格表、清理归档及订单记录,同步auto-check改动
This commit is contained in:
@@ -6,13 +6,13 @@
|
||||
# - CIDR: "10.0.3.0/24"
|
||||
# - IP 范围: "10.0.3.1-10.0.3.100"
|
||||
scan:
|
||||
cidr: "10.107.95.2-10.107.95.2"
|
||||
cidr: "10.0.3.148-10.0.3.148"
|
||||
timeout: 3s
|
||||
concurrency: 10
|
||||
|
||||
# --- SSH 认证 ---
|
||||
ssh:
|
||||
user: "admin-pc"
|
||||
user: "admin"
|
||||
password: "admin123"
|
||||
key: ""
|
||||
port: 22
|
||||
@@ -29,7 +29,7 @@ stress:
|
||||
temp_limit: 90 # 温度上限(°C),超过判失败,0=不判定
|
||||
# --- Docker 镜像(压测统一在容器中执行,工具与脚本预装在镜像里)---
|
||||
# 固定分发模式:目标机无镜像时,上传本地导出的 tar 并 docker load(不依赖 registry)
|
||||
docker_image: "stress-test:latest" # 压测镜像名,镜像维护见 docker/stress-test/
|
||||
docker_image: "stress-test:20260824-144244" # 压测镜像名,镜像维护见 docker/stress-test/
|
||||
docker_tar: "docker/stress-test/stress-test.tar" # 本地镜像文件(docker save -o 导出)
|
||||
docker_args: "--network=host --privileged" # Docker 运行参数,--privileged 可访问完整资源
|
||||
|
||||
@@ -47,3 +47,12 @@ report:
|
||||
# usb: 空=自动(Win \\.\usb001 / Linux /dev/usb/lp0), 或指定设备路径
|
||||
# file: 输出路径
|
||||
width: 42 # 80mm 热敏纸行宽
|
||||
html:
|
||||
base_url: "" # HTML 报告访问基地址(如 http://192.168.1.10:8080),扫码链接会拼接到此地址
|
||||
# 留空则只保存到本地(打印时仅提示本地路径)
|
||||
path: "" # HTML 保存目录,留空 = 与 txt 报告同目录
|
||||
filename: "report-{mac}-{ip}-{ts}.html" # 文件名模板
|
||||
qr_url: "" # 二维码 URL 模板,支持 {filename} {mac} {ip} {ts} 占位
|
||||
# 例: "http://192.168.1.10:8080/reports/{filename}"
|
||||
# 留空时自动用 base_url + 实际文件名拼接
|
||||
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
# 压力测试工具镜像
|
||||
#
|
||||
# 用途:为 auto-check 提供预装压测工具的容器环境
|
||||
# 优势:工具预装在镜像中,无需在线安装,环境一致性好,适合国内网络环境
|
||||
# 用途:为 auto-check 提供预装压测工具与脚本的容器环境
|
||||
# 优势:工具预装在镜像中,无需在目标机在线安装,环境一致性好
|
||||
#
|
||||
# 工作流程:
|
||||
# 工作流程(固定分发模式,不依赖 registry):
|
||||
# 1. auto-check 检测目标机器的 Docker 可用性
|
||||
# 2. 从 Registry 拉取镜像(或本地已存在则跳过)
|
||||
# 3. 启动容器执行测试(脚本已打包在镜像中)
|
||||
# 2. 目标机无镜像时,上传工程目录中导出的 tar(docker save)并 docker load
|
||||
# 3. 启动容器执行测试(脚本已打包在镜像中,挂载 /sys 采集温度)
|
||||
# 4. 收集测试结果并解析
|
||||
#
|
||||
# 构建命令:
|
||||
# 构建与导出(推荐用脚本,自动时间戳 tag):
|
||||
# cd docker/stress-test
|
||||
# docker build -t your-registry/stress-test:latest .
|
||||
# docker push your-registry/stress-test:latest
|
||||
# ./build.sh # 自动用当前时间戳打 tag
|
||||
# ./build.sh 20260824-153045 # 或手动指定
|
||||
# 脚本会同时:构建镜像(时间戳 tag) + 导出 tar + 同步 auto-check.yaml 配置
|
||||
#
|
||||
# 手动构建(如绕过脚本):
|
||||
# cd docker/stress-test
|
||||
# docker build -t stress-test:<时间戳> .
|
||||
# docker save -o stress-test.tar stress-test:<时间戳>
|
||||
# # 同步修改 ../../auto-check.yaml 的 docker_image
|
||||
#
|
||||
# 使用方式(在 auto-check.yaml 中配置):
|
||||
# stress:
|
||||
# use_docker: true
|
||||
# docker_image: "your-registry/stress-test:latest"
|
||||
# docker_image: "stress-test:<时间戳>"
|
||||
# docker_tar: "docker/stress-test/stress-test.tar"
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
# UTF-8 locale:保证 sensors 输出带 ° 符号的温度格式(脚本正则依赖)
|
||||
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
|
||||
|
||||
# 清理缓存,减少镜像体积
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
stress-ng \
|
||||
|
||||
@@ -29,20 +29,52 @@
|
||||
固定分发模式:不依赖 registry,镜像以 tar 文件形式放在工程目录,
|
||||
目标机无镜像时由 auto-check 自动上传并 `docker load`。
|
||||
|
||||
### 推荐:使用构建脚本(自动时间戳 tag)
|
||||
|
||||
`build.sh` 会以 `YYYYMMDD-HHMMSS` 格式自动生成时间戳 tag(如 `stress-test:20260824-153045`),
|
||||
便于在 `docker images` 中一眼区分新旧版本。脚本会同时:
|
||||
|
||||
1. 用时间戳 tag 构建镜像(**不再保留 `latest`**)
|
||||
2. 导出两份 tar:带时间戳的归档 + 同步覆盖 `stress-test.tar`(auto-check 默认引用)
|
||||
3. 自动更新 `auto-check.yaml` 的 `docker_image` 字段,确保配置与新 tag 同步
|
||||
|
||||
```bash
|
||||
cd docker/stress-test
|
||||
docker build -t stress-test:latest .
|
||||
# 导出到工程目录(供 auto-check 上传分发,已加入 .gitignore)
|
||||
docker save -o stress-test.tar stress-test:latest
|
||||
./build.sh # 自动取当前时间戳
|
||||
./build.sh 20260824-153045 # 手动指定时间戳(重打/复现)
|
||||
```
|
||||
|
||||
产物:
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| 镜像 `stress-test:<时间戳>` | 唯一 tag,`docker images` 可看到每次构建 |
|
||||
| Tar `stress-test-<时间戳>.tar` | 带时间戳归档,保留历史版本 |
|
||||
| Tar `stress-test.tar` | auto-check 默认引用,脚本自动覆盖为最新 |
|
||||
| `auto-check.yaml` | `docker_image` 自动同步为新 tag |
|
||||
|
||||
> 旧时间戳镜像可手动 `docker rmi stress-test:<旧时间戳>` 清理。
|
||||
|
||||
### 手动构建(不推荐)
|
||||
|
||||
如需绕过脚本手动构建,必须自行同步 `auto-check.yaml`:
|
||||
|
||||
```bash
|
||||
cd docker/stress-test
|
||||
docker build -t stress-test:20260824-153045 . # 必须带时间戳
|
||||
docker save -o stress-test.tar stress-test:20260824-153045
|
||||
# 然后编辑 ../../auto-check.yaml,把 docker_image 改成新 tag
|
||||
```
|
||||
|
||||
> 注意:手动构建如忘记带时间戳,重复构建后会丢失版本信息,调试时易混淆。
|
||||
|
||||
## 使用
|
||||
|
||||
在 `auto-check.yaml` 中配置:
|
||||
在 `auto-check.yaml` 中配置(`docker_image` 由 `build.sh` 自动维护):
|
||||
|
||||
```yaml
|
||||
stress:
|
||||
docker_image: "stress-test:latest"
|
||||
docker_image: "stress-test:20260824-153045" # 每次构建自动更新为新时间戳
|
||||
docker_tar: "docker/stress-test/stress-test.tar"
|
||||
docker_args: "--network=host --privileged"
|
||||
```
|
||||
@@ -68,7 +100,16 @@ stress:
|
||||
2. 温度监控后台采样(`temp_start.sh`)
|
||||
3. 各项压测:先单项(cpu/memory/disk/memnative)、综合(full)最后
|
||||
4. 温度监控收尾(`===MONITOR:temp===`)
|
||||
5. 内核日志(`===MONITOR:dmesg===`)+ 结束标记
|
||||
5. 内核日志(`===MONITOR:dmesg===`,`-T` 带时间戳)+ 结束标记
|
||||
|
||||
## 容器运行时挂载(由 auto-check 自动添加)
|
||||
|
||||
| 挂载 | 作用 |
|
||||
|------|------|
|
||||
| `-v /sys:/sys:ro` | 容器内是独立 sysfs,挂载后才能读到宿主机硬件传感器(温度采集依赖此项) |
|
||||
| `-v /etc/localtime:/etc/localtime:ro` | 容器时区与宿主机一致,dmesg 时间戳与采样时间才准确 |
|
||||
|
||||
> 注意:镜像内已设置 `LC_ALL=C.UTF-8`。若 locale 为纯 C,`sensors` 会输出 "+51.0 C"(无 ° 符号),导致温度正则失配、采集不到数据。
|
||||
|
||||
## 镜像内容
|
||||
|
||||
|
||||
78
auto-check/docker/stress-test/build.sh
Normal file
78
auto-check/docker/stress-test/build.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# build.sh - 构建压测镜像并以时间戳打 tag
|
||||
#
|
||||
# 目的:每次构建都生成唯一 tag(如 stress-test:20260824-153045),
|
||||
# 便于在 docker images 中区分新旧版本,避免 latest 重复构建后无法追溯。
|
||||
#
|
||||
# 用法:
|
||||
# ./build.sh # 自动使用当前时间戳 (YYYYMMDD-HHMMSS)
|
||||
# ./build.sh 20260824-153045 # 手动指定时间戳(用于复现/重打)
|
||||
#
|
||||
# 产物:
|
||||
# - 镜像: stress-test:<timestamp> (仅时间戳 tag,不再保留 latest)
|
||||
# - Tar : stress-test-<timestamp>.tar (带时间戳归档)
|
||||
# + stress-test.tar (auto-check 默认引用,同步覆盖)
|
||||
# - auto-check.yaml 的 docker_image 字段会被自动同步为新时间戳 tag
|
||||
#
|
||||
# 注意:
|
||||
# - 脚本会自动 cd 到自身所在目录,可从任意位置调用
|
||||
# - 需要本地有 Docker 可用
|
||||
# - 每次构建会覆盖上一份 stress-test.tar;时间戳 tar 保留可作历史归档
|
||||
# - 旧时间戳镜像可执行 docker rmi stress-test:<old-ts> 清理
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 切到脚本所在目录(Dockerfile 在这里)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 时间戳:支持外部传入,否则取当前时间
|
||||
TS="${1:-$(date +%Y%m%d-%H%M%S)}"
|
||||
|
||||
IMAGE_NAME="stress-test"
|
||||
TAG_TS="${IMAGE_NAME}:${TS}"
|
||||
TAR_TS="${IMAGE_NAME}-${TS}.tar"
|
||||
TAR_LATEST="${IMAGE_NAME}.tar"
|
||||
|
||||
# auto-check.yaml 路径(相对本脚本 ../..)
|
||||
YAML_FILE="$SCRIPT_DIR/../../auto-check.yaml"
|
||||
|
||||
echo "==> 时间戳: ${TS}"
|
||||
echo "==> 构建镜像: ${TAG_TS}"
|
||||
docker build -t "${TAG_TS}" .
|
||||
|
||||
echo "==> 导出 Tar: ${TAR_TS}"
|
||||
docker save -o "${TAR_TS}" "${TAG_TS}"
|
||||
|
||||
echo "==> 同步 Latest Tar: ${TAR_LATEST}"
|
||||
cp -f "${TAR_TS}" "${TAR_LATEST}"
|
||||
|
||||
# 同步 auto-check.yaml 中的 docker_image 为新时间戳 tag
|
||||
if [ -f "$YAML_FILE" ]; then
|
||||
if grep -q '^[[:space:]]*docker_image:' "$YAML_FILE"; then
|
||||
if ! grep -q "${TAG_TS}" "$YAML_FILE"; then
|
||||
# -i.bak 同时生成备份(兼容 GNU sed / BSD sed),改完再删
|
||||
sed -i.bak -E "s|(docker_image:[[:space:]]*\")[^\"]+(\")|\1${TAG_TS}\2|" "$YAML_FILE"
|
||||
rm -f "${YAML_FILE}.bak"
|
||||
echo "==> 已更新 ${YAML_FILE##*/}: docker_image -> ${TAG_TS}"
|
||||
else
|
||||
echo "==> ${YAML_FILE##*/} 中 docker_image 已是 ${TAG_TS},跳过"
|
||||
fi
|
||||
else
|
||||
echo "==> 未在 ${YAML_FILE##*/} 中找到 docker_image 字段,请手动更新"
|
||||
fi
|
||||
else
|
||||
echo "==> 未找到 ${YAML_FILE},请手动确保 docker_image 与新 tag 一致"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 构建完成"
|
||||
echo "=========================================="
|
||||
echo " 镜像 Tag: ${TAG_TS}"
|
||||
echo " Tar 文件: ${TAR_TS}"
|
||||
echo " ${TAR_LATEST} (auto-check 默认引用)"
|
||||
echo ""
|
||||
echo "==> 本机 ${IMAGE_NAME} 镜像列表:"
|
||||
docker images "${IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
echo '===MONITOR:dmesg==='
|
||||
DMESG_ERR=$(dmesg --level=err,crit,alert,emerg 2>/dev/null | tail -30 || true)
|
||||
# -T 输出人类可读时间戳(如 [Mon Aug 24 10:23:45 2026]),
|
||||
# 依赖挂载宿主机 /etc/localtime 保证时区正确;老内核不支持 -T 时回退
|
||||
DMESG_ERR=$(dmesg -T --level=err,crit,alert,emerg 2>/dev/null | tail -30)
|
||||
if [ -z "$DMESG_ERR" ]; then
|
||||
DMESG_ERR=$(dmesg --level=err,crit,alert,emerg 2>/dev/null | tail -30 || true)
|
||||
fi
|
||||
if [ -n "$DMESG_ERR" ]; then
|
||||
echo 'status:fail'
|
||||
echo "output:${DMESG_ERR}"
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
# 系统信息 → 温度监控启动 → 各项压测(先单项、后综合)→ 温度监控收尾 → 内核日志
|
||||
set -e
|
||||
|
||||
# 统一 UTF-8 locale:否则 sensors 输出 "+51.0 C"(无 ° 符号),
|
||||
# 脚本中 (?=°C) 的温度正则会全部失配,导致温度采集不到
|
||||
export LC_ALL=C.UTF-8
|
||||
export LANG=C.UTF-8
|
||||
|
||||
# 默认值(docker -e 传入时覆盖)
|
||||
export AUTOCHECK_TYPES="${AUTOCHECK_TYPES:-cpu,memory}"
|
||||
export AUTOCHECK_DURATION="${AUTOCHECK_DURATION:-30}"
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/pkg/sftp v1.13.11 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
||||
@@ -61,6 +61,17 @@ type StressConfig struct {
|
||||
type ReportConfig struct {
|
||||
Path string `yaml:"path"` // 报告保存路径
|
||||
Print PrintConfig `yaml:"print"` // 打印配置
|
||||
HTML HTMLConfig `yaml:"html"` // HTML 报告配置
|
||||
}
|
||||
|
||||
// HTMLConfig HTML 报告配置
|
||||
type HTMLConfig struct {
|
||||
BaseURL string `yaml:"base_url"` // HTML 报告访问基地址(如 http://192.168.1.10:8080),空则只保存本地
|
||||
Path string `yaml:"path"` // HTML 报告保存目录,空 = 与 txt 报告同目录
|
||||
Filename string `yaml:"filename"` // 文件名模板,支持 {mac} {ip} {ts},默认 report-{mac}-{ip}-{ts}.html
|
||||
// QRURL 热敏打印中二维码指向的 URL 模板,支持 {filename} {mac} {ip} {ts} 占位
|
||||
// 留空时用 BaseURL + 实际文件名拼接
|
||||
QRURL string `yaml:"qr_url"`
|
||||
}
|
||||
|
||||
// PrintConfig 打印配置
|
||||
|
||||
@@ -2,22 +2,32 @@ package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
|
||||
"auto-check/pkg/config"
|
||||
"auto-check/pkg/model"
|
||||
"auto-check/pkg/stress"
|
||||
)
|
||||
|
||||
// BuildDeviceReport 由压测结果构建设备报告(MAC 唯一标识)
|
||||
func BuildDeviceReport(dev *model.Device, rpt *stress.Report) *Report {
|
||||
// qrURL 非空时会附加到报告,供 80mm 模板在末尾显示"扫码查看详细报告"提示与 URL 文本兜底
|
||||
func BuildDeviceReport(dev *model.Device, rpt *stress.Report, qrURL string) *Report {
|
||||
// 从 sysInfo 提取 CPU 型号用于按硬件库匹配基准
|
||||
cpuModel := ""
|
||||
if rpt.SysInfo != nil {
|
||||
cpuModel = rpt.SysInfo["cpu"]
|
||||
}
|
||||
b := NewBuilder(ConfigSummary{})
|
||||
b.StartPhase("压力测试")
|
||||
b.AddHost(HostResult{MAC: dev.MAC, IP: dev.IP})
|
||||
for _, res := range rpt.Results {
|
||||
b.AddStressResult(dev.MAC, StressResult{
|
||||
sr := StressResult{
|
||||
Type: string(res.Type),
|
||||
Status: res.Status,
|
||||
Duration: res.Duration.String(),
|
||||
@@ -26,10 +36,21 @@ func BuildDeviceReport(dev *model.Device, rpt *stress.Report) *Report {
|
||||
Metrics: res.OrderedMetrics(),
|
||||
Error: res.Error,
|
||||
Output: res.Output,
|
||||
})
|
||||
}
|
||||
// 基准评级(让用户对结果有感性认知;按硬件库匹配专属阈值,未匹配全部"达到正常")
|
||||
if items := stress.EvaluateResult(res, cpuModel); len(items) > 0 {
|
||||
sr.Benchmarks = items
|
||||
if lvl := stress.OverallLevel(items); lvl != "" {
|
||||
sr.OverallLevel = string(lvl)
|
||||
sr.OverallText = stress.LevelMeta(lvl).Text
|
||||
}
|
||||
}
|
||||
b.AddStressResult(dev.MAC, sr)
|
||||
}
|
||||
b.EndPhase(rpt.Summary())
|
||||
return b.Build()
|
||||
r := b.Build()
|
||||
r.QRURL = qrURL
|
||||
return r
|
||||
}
|
||||
|
||||
// SaveDeviceReport 保存设备报告(文件名含 MAC/IP/时间戳)
|
||||
@@ -40,17 +61,119 @@ func SaveDeviceReport(dev *model.Device, rpt *stress.Report, dir string) error {
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
name := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
path := filepath.Join(dir, fmt.Sprintf("report-%s-%s-%s.txt", name, dev.IP, ts))
|
||||
return BuildDeviceReport(dev, rpt).SaveFile(path, "text")
|
||||
return BuildDeviceReport(dev, rpt, "").SaveFile(path, "text")
|
||||
}
|
||||
|
||||
// PrintDeviceReport 渲染 80mm 模板并送打印机
|
||||
func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintConfig) error {
|
||||
// SaveDeviceQR 保存设备报告对应的二维码 PNG(与 txt/html 同目录)
|
||||
// url 为空时不生成;返回 PNG 完整路径,失败时返回错误
|
||||
func SaveDeviceQR(dev *model.Device, url string, dir string) (string, error) {
|
||||
if url == "" {
|
||||
return "", nil
|
||||
}
|
||||
if dir == "" {
|
||||
dir = "reports"
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
filename := fmt.Sprintf("report-%s-%s-%s.qr.png", mac, dev.IP, ts)
|
||||
path := filepath.Join(dir, filename)
|
||||
|
||||
// 512 字节足够;高纠错级别 H 提升扫码容错
|
||||
png, err := qrcode.New(url, qrcode.High)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 512x512 PNG;二维码扫描距离更友好
|
||||
if err := png.WriteFile(512, path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// SaveDeviceHTMLReport 保存设备 HTML 报告(文件名按 config.HTML.Filename 模板生成)
|
||||
// 返回: (filename, fullPath, error)
|
||||
func SaveDeviceHTMLReport(dev *model.Device, rpt *stress.Report, dir string, htmlCfg config.HTMLConfig) (string, string, error) {
|
||||
dir = htmlDir(dir, htmlCfg)
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
filename := renderHTMLFilename(htmlCfg.Filename, mac, dev.IP, ts)
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("report-%s-%s-%s.html", mac, dev.IP, ts)
|
||||
}
|
||||
|
||||
path, err := stress.WriteReportHTMLTo(dev.IP, rpt, dir, filename)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return filename, path, nil
|
||||
}
|
||||
|
||||
// BuildDeviceReportURL 构建设备 HTML 报告的访问 URL(用于二维码)
|
||||
// 优先级:qr_url 模板 > base_url + filename > 空字符串
|
||||
// 占位符:{filename} {mac} {ip} {ts}
|
||||
func BuildDeviceReportURL(dev *model.Device, filename string, htmlCfg config.HTMLConfig) string {
|
||||
mac := strings.ReplaceAll(dev.MAC, ":", "")
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
if tpl := strings.TrimSpace(htmlCfg.QRURL); tpl != "" {
|
||||
return fillPlaceholders(tpl, map[string]string{
|
||||
"filename": filename,
|
||||
"mac": mac,
|
||||
"ip": dev.IP,
|
||||
"ts": ts,
|
||||
})
|
||||
}
|
||||
base := strings.TrimRight(strings.TrimSpace(htmlCfg.BaseURL), "/")
|
||||
if base == "" || filename == "" {
|
||||
return ""
|
||||
}
|
||||
return base + "/" + filename
|
||||
}
|
||||
|
||||
// saveDeviceHTMLAndQR 保存 HTML 报告 + 二维码 PNG(一体化),返回 (filename, qrPath, error)
|
||||
// - HTML 失败:仅返回错误,不生成 QR
|
||||
// - QR 失败:只打日志,不影响 HTML(用户至少能用 URL 文本兜底)
|
||||
func saveDeviceHTMLAndQR(dev *model.Device, rpt *stress.Report, dir string, htmlCfg config.HTMLConfig) (string, string, error) {
|
||||
filename, _, err := SaveDeviceHTMLReport(dev, rpt, dir, htmlCfg)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
url := BuildDeviceReportURL(dev, filename, htmlCfg)
|
||||
if url == "" && filename != "" {
|
||||
// 兜底:base_url 未配置时,至少打印本地路径(便于手工打开)
|
||||
url = filename
|
||||
}
|
||||
qrPath, qerr := SaveDeviceQR(dev, url, dir)
|
||||
if qerr != nil {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG 生成失败: %v\n", dev.IP, qerr)
|
||||
}
|
||||
return filename, qrPath, nil
|
||||
}
|
||||
|
||||
// PrintDeviceReport 渲染 80mm 模板并送打印机,启用 HTML 时自动追加二维码
|
||||
func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintConfig, htmlCfg config.HTMLConfig) error {
|
||||
width := cfg.Width
|
||||
if width <= 0 {
|
||||
width = 42
|
||||
}
|
||||
|
||||
text, err := BuildDeviceReport(dev, rpt).Render80mmWithConfig(TemplateConfig{Width: width})
|
||||
// 先保存 HTML + 二维码 PNG,并计算二维码 URL
|
||||
filename, qrPath, herr := saveDeviceHTMLAndQR(dev, rpt, "", htmlCfg)
|
||||
if herr != nil {
|
||||
fmt.Printf(" [报告] %s HTML 保存失败: %v\n", dev.IP, herr)
|
||||
}
|
||||
url := BuildDeviceReportURL(dev, filename, htmlCfg)
|
||||
if url == "" && filename != "" {
|
||||
// 兜底:base_url 未配置时,至少打印本地路径(便于手工打开)
|
||||
url = filename
|
||||
}
|
||||
if qrPath != "" {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG: %s\n", dev.IP, qrPath)
|
||||
}
|
||||
|
||||
text, err := BuildDeviceReport(dev, rpt, url).Render80mmWithConfig(TemplateConfig{Width: width})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,20 +184,76 @@ func PrintDeviceReport(dev *model.Device, rpt *stress.Report, cfg config.PrintCo
|
||||
}
|
||||
defer printer.Close()
|
||||
|
||||
return printer.Print(text)
|
||||
if err := printer.Print(text); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 追加二维码(HTML 报告访问链接);后端不支持或无 URL 时 PrintQR 内部静默跳过
|
||||
if printer != nil && url != "" {
|
||||
if qerr := printer.PrintQR(url); qerr != nil {
|
||||
fmt.Printf(" [打印] %s 二维码失败: %v\n", dev.IP, qerr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAndPrintDeviceReport 保存报告并(启用时)打印,含错误输出(一站式业务入口)
|
||||
func SaveAndPrintDeviceReport(dev *model.Device, rpt *stress.Report, dir string, printCfg config.PrintConfig) {
|
||||
func SaveAndPrintDeviceReport(dev *model.Device, rpt *stress.Report, dir string, printCfg config.PrintConfig, htmlCfg config.HTMLConfig) {
|
||||
if err := SaveDeviceReport(dev, rpt, dir); err != nil {
|
||||
fmt.Printf(" [报告] %s 保存失败: %v\n", dev.IP, err)
|
||||
}
|
||||
|
||||
// 总是保存 HTML + 二维码 PNG(便于不打印时也能扫码查看详细报告)
|
||||
_, qrPath, herr := saveDeviceHTMLAndQR(dev, rpt, dir, htmlCfg)
|
||||
if herr != nil {
|
||||
fmt.Printf(" [报告] %s HTML 保存失败: %v\n", dev.IP, herr)
|
||||
} else if qrPath != "" {
|
||||
fmt.Printf(" [报告] %s 二维码 PNG: %s\n", dev.IP, qrPath)
|
||||
}
|
||||
|
||||
if printCfg.Enabled {
|
||||
if err := PrintDeviceReport(dev, rpt, printCfg); err != nil {
|
||||
if err := PrintDeviceReport(dev, rpt, printCfg, htmlCfg); err != nil {
|
||||
fmt.Printf(" [打印] %s 失败: %v\n", dev.IP, err)
|
||||
} else {
|
||||
fmt.Printf(" [打印] %s 已送出 (%s)\n", dev.IP, printCfg.Backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 辅助
|
||||
// ============================
|
||||
|
||||
// htmlDir 解析 HTML 报告目录
|
||||
func htmlDir(defaultDir string, cfg config.HTMLConfig) string {
|
||||
if p := strings.TrimSpace(cfg.Path); p != "" {
|
||||
return p
|
||||
}
|
||||
if defaultDir == "" {
|
||||
return "reports"
|
||||
}
|
||||
return defaultDir
|
||||
}
|
||||
|
||||
// renderHTMLFilename 渲染文件名模板,缺省时返回默认
|
||||
func renderHTMLFilename(tpl, mac, ip, ts string) string {
|
||||
tpl = strings.TrimSpace(tpl)
|
||||
if tpl == "" {
|
||||
return ""
|
||||
}
|
||||
return fillPlaceholders(tpl, map[string]string{
|
||||
"filename": "", // 文件名模板内不应再嵌 filename
|
||||
"mac": mac,
|
||||
"ip": ip,
|
||||
"ts": ts,
|
||||
})
|
||||
}
|
||||
|
||||
// fillPlaceholders 简单占位符替换 {key}
|
||||
func fillPlaceholders(s string, kv map[string]string) string {
|
||||
for k, v := range kv {
|
||||
s = strings.ReplaceAll(s, "{"+k+"}", url.PathEscape(v))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ var Printed = make(map[string]string)
|
||||
// Printer 报告打印机
|
||||
type Printer interface {
|
||||
Print(text string) error
|
||||
// PrintQR 在打印流末尾追加二维码(ESC/POS GS ( k 指令)
|
||||
// 后端不支持时返回 nil(不打印二维码)
|
||||
PrintQR(url string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -66,6 +69,63 @@ func writeEscpos(w io.Writer, text string) error {
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
// ============================
|
||||
// ESC/POS 二维码(GS ( k 指令)
|
||||
// ============================
|
||||
//
|
||||
// 适配 EPSON / Star / 芯烨(Xprinter) / 佳博 等主流 ESC/POS 热敏打印机。
|
||||
// 指令格式:GS ( k pL pH cn fn [n...]
|
||||
// cn = 49 (0x31) QR Code
|
||||
// fn = 65 (A) 选择模型 / 67 (C) 模块大小 / 69 (E) 纠错 / 80 (P) 数据 / 81 (Q) 打印
|
||||
//
|
||||
// 参考:EPSON TM-T20 QR Code 规范
|
||||
// 数据长度限制(Model 2 + Level M 纠错):URL 场景 < 512 字节完全够用。
|
||||
|
||||
// escposQR 写入 ESC/POS 二维码指令流(不切纸,由调用方决定结尾)
|
||||
// size: 1-16(模块像素数),默认 6
|
||||
// ecc : 48/49/50/51 (L/M/Q/H)
|
||||
func escposQR(w io.Writer, data string, size byte, ecc byte) error {
|
||||
if size < 1 {
|
||||
size = 1
|
||||
}
|
||||
if size > 16 {
|
||||
size = 16
|
||||
}
|
||||
if ecc < 48 || ecc > 51 {
|
||||
ecc = 49 // 默认 M
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
bw := bufio.NewWriter(w)
|
||||
|
||||
// 1) 选择模型:Model 2(cn=49 fn=65 n=50)
|
||||
bw.WriteString("\x1d(k\x04\x00\x31\x41\x32")
|
||||
|
||||
// 2) 模块大小(cn=49 fn=67 n=size)
|
||||
bw.WriteString("\x1d(k\x03\x00\x31\x43")
|
||||
bw.WriteByte(size)
|
||||
|
||||
// 3) 纠错级别(cn=49 fn=69 n=ecc)
|
||||
bw.WriteString("\x1d(k\x03\x00\x31\x45")
|
||||
bw.WriteByte(ecc)
|
||||
|
||||
// 4) 存储数据:GS ( k pL pH 49 80 48 <data>
|
||||
// pL pH = (data_len + 3) 低/高字节
|
||||
pL := byte((len(data) + 3) & 0xff)
|
||||
pH := byte(((len(data) + 3) >> 8) & 0xff)
|
||||
bw.WriteString("\x1d(k")
|
||||
bw.WriteByte(pL)
|
||||
bw.WriteByte(pH)
|
||||
bw.WriteString("\x31\x50\x30")
|
||||
bw.WriteString(data)
|
||||
|
||||
// 5) 打印(cn=49 fn=81 n=48 不叠加文字)
|
||||
bw.WriteString("\x1d(k\x03\x00\x31\x51\x30")
|
||||
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 后端实现
|
||||
// ============================
|
||||
@@ -120,6 +180,9 @@ func (p *SystemPrinter) Print(text string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// PrintQR 系统打印后端无二维码指令能力,no-op
|
||||
func (p *SystemPrinter) PrintQR(url string) error { return nil }
|
||||
|
||||
// Close 系统打印无需关闭
|
||||
func (p *SystemPrinter) Close() error { return nil }
|
||||
|
||||
@@ -147,6 +210,20 @@ func (p *ESCPOSPrinter) Print(text string) error {
|
||||
return writeEscpos(p.conn, text)
|
||||
}
|
||||
|
||||
// PrintQR 发送 ESC/POS 二维码指令到网络打印机
|
||||
// 走纸居中追加一行提示,模块大小 6,纠错级别 M
|
||||
func (p *ESCPOSPrinter) PrintQR(url string) error {
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
// 居中(ESC a 1)后写一行"扫码查看详细报告",然后发二维码
|
||||
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
|
||||
if _, err := p.conn.Write([]byte(center)); err != nil {
|
||||
return err
|
||||
}
|
||||
return escposQR(p.conn, url, 6, 49)
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (p *ESCPOSPrinter) Close() error {
|
||||
if p.conn != nil {
|
||||
@@ -193,6 +270,18 @@ func (p *USBPrinter) Print(text string) error {
|
||||
return writeEscpos(p.dev, text)
|
||||
}
|
||||
|
||||
// PrintQR 发送 ESC/POS 二维码指令到 USB 打印机
|
||||
func (p *USBPrinter) PrintQR(url string) error {
|
||||
if p.dev == nil {
|
||||
return nil
|
||||
}
|
||||
center := "\x1ba\x01———— 扫码查看详细报告 ————\n\x1ba\x00"
|
||||
if _, err := p.dev.Write([]byte(center)); err != nil {
|
||||
return err
|
||||
}
|
||||
return escposQR(p.dev, url, 6, 49)
|
||||
}
|
||||
|
||||
// Close 关闭设备
|
||||
func (p *USBPrinter) Close() error {
|
||||
if p.dev != nil {
|
||||
@@ -226,6 +315,12 @@ func (p *FilePrinter) Print(text string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// PrintQR 文件后端:将"扫码查看详细报告"提示行 + URL 写入文件(便于在文件中复制链接)
|
||||
func (p *FilePrinter) PrintQR(url string) error {
|
||||
_, err := fmt.Fprintf(p.f, "\n———— 扫码查看详细报告 ————\nURL: %s\n\n", url)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close 关闭文件
|
||||
func (p *FilePrinter) Close() error {
|
||||
return p.f.Close()
|
||||
|
||||
131
auto-check/pkg/report/print_test.go
Normal file
131
auto-check/pkg/report/print_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEscposQR_EmptyData 空数据应直接返回 nil,不写入任何指令
|
||||
func TestEscposQR_EmptyData(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := escposQR(&buf, "", 6, 49); err != nil {
|
||||
t.Fatalf("空数据应返回 nil,得到 err=%v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("空数据不应输出指令,实际写入 %d 字节", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_BasicCommands 验证基本 GS ( k 指令流
|
||||
func TestEscposQR_BasicCommands(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
data := "http://192.168.1.10:8080/reports/report-abc-10.0.0.1-20260824.html"
|
||||
if err := escposQR(&buf, data, 6, 49); err != nil {
|
||||
t.Fatalf("生成指令失败: %v", err)
|
||||
}
|
||||
|
||||
got := buf.Bytes()
|
||||
// 必须包含 GS ( k 起始
|
||||
if !bytes.Contains(got, []byte{0x1d, 0x28, 0x6b}) {
|
||||
t.Error("缺少 GS ( k 起始指令")
|
||||
}
|
||||
// 必须包含模型选择 cn=49 fn=65 n=50
|
||||
if !bytes.Contains(got, []byte{0x31, 0x41, 0x32}) {
|
||||
t.Error("缺少 QR 模型选择指令 (31 41 32)")
|
||||
}
|
||||
// 必须包含模块大小 n=6
|
||||
if !bytes.Contains(got, []byte{0x31, 0x43, 0x06}) {
|
||||
t.Error("缺少模块大小指令 (31 43 06)")
|
||||
}
|
||||
// 必须包含纠错级别 M (49)
|
||||
if !bytes.Contains(got, []byte{0x31, 0x45, 49}) {
|
||||
t.Error("缺少纠错级别指令 (31 45 49)")
|
||||
}
|
||||
// 必须包含数据存储 49 80 48
|
||||
if !bytes.Contains(got, []byte{0x31, 0x50, 0x30}) {
|
||||
t.Error("缺少数据存储指令 (31 50 30)")
|
||||
}
|
||||
// 必须包含打印指令 49 81 48
|
||||
if !bytes.Contains(got, []byte{0x31, 0x51, 0x30}) {
|
||||
t.Error("缺少打印指令 (31 51 30)")
|
||||
}
|
||||
// 数据本体
|
||||
if !bytes.Contains(got, []byte(data)) {
|
||||
t.Error("缺少 URL 数据本体")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_ClampSizeAndECC 验证 size/ecc 参数越界时自动夹紧
|
||||
func TestEscposQR_ClampSizeAndECC(t *testing.T) {
|
||||
var buf1, buf2, buf3 bytes.Buffer
|
||||
// size 越界 0 → 夹紧为 1
|
||||
_ = escposQR(&buf1, "abc", 0, 49)
|
||||
// size 越界 99 → 夹紧为 16
|
||||
_ = escposQR(&buf2, "abc", 99, 49)
|
||||
// ecc 非法 99 → 夹紧为 49 (M)
|
||||
_ = escposQR(&buf3, "abc", 6, 99)
|
||||
|
||||
if !bytes.Contains(buf1.Bytes(), []byte{0x31, 0x43, 0x01}) {
|
||||
t.Error("size=0 应夹紧为 1,未生效")
|
||||
}
|
||||
if !bytes.Contains(buf2.Bytes(), []byte{0x31, 0x43, 0x10}) {
|
||||
t.Error("size=99 应夹紧为 16,未生效")
|
||||
}
|
||||
if !bytes.Contains(buf3.Bytes(), []byte{0x31, 0x45, 49}) {
|
||||
t.Error("ecc=99 应夹紧为 49(M),未生效")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEscposQR_DataLengthEncoding pL/pH 编码正确(大数据场景)
|
||||
// 指令布局: 1d 28 6b pL pH 31 50 30 <data>
|
||||
func TestEscposQR_DataLengthEncoding(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// 200 字节数据 → pL = 203 (0xCB), pH = 0
|
||||
data := strings.Repeat("a", 200)
|
||||
_ = escposQR(&buf, data, 6, 49)
|
||||
got := buf.Bytes()
|
||||
|
||||
wantPL := byte((200 + 3) & 0xff) // 203 = 0xCB
|
||||
wantPH := byte(((200 + 3) >> 8) & 0xff)
|
||||
|
||||
// 数据存储标记 "31 50 30" 在 got 中的位置
|
||||
dataMarker := []byte{0x31, 0x50, 0x30}
|
||||
idx := bytes.Index(got, dataMarker)
|
||||
if idx < 0 {
|
||||
t.Fatal("未找到数据存储指令 31 50 30")
|
||||
}
|
||||
// 期望布局:1d(GS) 28 ( 6b k pL pH 31 50 30 ...
|
||||
// got[idx-5] = 0x1d, got[idx-4] = 0x28, got[idx-3] = 0x6b
|
||||
// got[idx-2] = pL, got[idx-1] = pH
|
||||
if got[idx-5] != 0x1d || got[idx-4] != 0x28 || got[idx-3] != 0x6b {
|
||||
t.Fatalf("pL/pH 前不是 GS ( k,实际前 5 字节 = % x", got[idx-5:idx])
|
||||
}
|
||||
if got[idx-2] != wantPL || got[idx-1] != wantPH {
|
||||
t.Errorf("pL/pH 编码错误: got=0x%02x 0x%02x, want=0x%02x 0x%02x",
|
||||
got[idx-2], got[idx-1], wantPL, wantPH)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemPrinter_PrintQR 系统打印后端 no-op
|
||||
func TestSystemPrinter_PrintQR(t *testing.T) {
|
||||
p := &SystemPrinter{}
|
||||
if err := p.PrintQR("http://example.com"); err != nil {
|
||||
t.Errorf("SystemPrinter.PrintQR 应返回 nil,得到 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilePrinter_PrintQR 文件后端写入提示行 + URL
|
||||
func TestFilePrinter_PrintQR(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
p, err := NewFilePrinter(tmp + "/out.log")
|
||||
if err != nil {
|
||||
t.Fatalf("创建 FilePrinter 失败: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
url := "http://192.168.1.10/reports/r.html"
|
||||
if err := p.PrintQR(url); err != nil {
|
||||
t.Fatalf("PrintQR 失败: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,17 @@ type HostResult struct {
|
||||
|
||||
// StressResult 单项压力测试结果
|
||||
type StressResult struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Status string `json:"status" yaml:"status"` // pass / fail / skip
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Score float64 `json:"score,omitempty" yaml:"score,omitempty"`
|
||||
ScoreUnit string `json:"score_unit,omitempty" yaml:"score_unit,omitempty"`
|
||||
Metrics []stress.MetricItem `json:"metrics,omitempty" yaml:"metrics,omitempty"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Status string `json:"status" yaml:"status"` // pass / fail / skip
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Score float64 `json:"score,omitempty" yaml:"score,omitempty"`
|
||||
ScoreUnit string `json:"score_unit,omitempty" yaml:"score_unit,omitempty"`
|
||||
Metrics []stress.MetricItem `json:"metrics,omitempty" yaml:"metrics,omitempty"`
|
||||
Benchmarks []stress.BenchItem `json:"benchmarks,omitempty" yaml:"benchmarks,omitempty"` // 性能/温度基准评级
|
||||
OverallLevel string `json:"overall_level,omitempty" yaml:"overall_level,omitempty"`
|
||||
OverallText string `json:"overall_text,omitempty" yaml:"overall_text,omitempty"`
|
||||
Output string `json:"output,omitempty" yaml:"output,omitempty"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Report 检测报告
|
||||
@@ -49,6 +52,8 @@ type Report struct {
|
||||
Duration string `json:"duration" yaml:"duration"`
|
||||
Config ConfigSummary `json:"config" yaml:"config"`
|
||||
Phases []PhaseResult `json:"phases" yaml:"phases"`
|
||||
// QRURL HTML 报告访问链接(用于热敏打印中的二维码与模板回退)
|
||||
QRURL string `json:"qr_url,omitempty" yaml:"qr_url,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSummary 配置摘要(脱敏)
|
||||
|
||||
@@ -18,9 +18,14 @@
|
||||
{{ end }}{{ end }}{{ if eq .Phase "SSH 登录" }}{{ range .Hosts }}{{ if .SSHLogin }}✓{{ else }}✗{{ end }} {{ padRight .MAC 19 }} {{ padRight .IP 15 }} {{ .SSHErr }}
|
||||
{{ end }}{{ end }}{{ if eq .Phase "压力测试" }}{{ range .Hosts }}{{ padRight .MAC 19 }} {{ padRight .IP 15 }}
|
||||
{{ range .Stress }}{{ if eq .Status "pass" }}✓{{ else }}✗{{ end }} {{ padRight .Type 12 }} {{ .Status }} {{ .Duration }}
|
||||
{{ if .Score }} 跑分: {{ printf "%.1f" .Score }} {{ .ScoreUnit }}{{ end }}{{ range .Metrics }} {{ .Label }}: {{ printf "%.1f" .Value }}{{ .Unit }}
|
||||
{{ if .Score }} 跑分: {{ printf "%.1f" .Score }} {{ .ScoreUnit }}{{ end }}{{ if .OverallText }} 评级: {{ .OverallText }}{{ end }}{{ range .Benchmarks }} {{ padRight .Label 8 }} {{ .Value }} {{ .LevelText }}
|
||||
{{ end }}{{ range .Metrics }} {{ .Label }}: {{ printf "%.1f" .Value }}{{ .Unit }}
|
||||
{{ end }}{{ if .Error }} ERR: {{ trunc .Error 34 }}{{ end }}{{ end }}{{ end }}{{ end }}{{ end }}
|
||||
{{ "" }}
|
||||
{{ hr '═' }}
|
||||
报告结束
|
||||
{{ hr '═' }}
|
||||
{{ if .QRURL }}{{ "" }}
|
||||
———— 扫码查看详细报告 ————
|
||||
{{ .QRURL }}
|
||||
{{ end }}
|
||||
|
||||
611
auto-check/pkg/stress/benchmark.go
Normal file
611
auto-check/pkg/stress/benchmark.go
Normal file
@@ -0,0 +1,611 @@
|
||||
package stress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================
|
||||
// 性能/温度基准(让用户对结果有感性认知)
|
||||
// ============================
|
||||
//
|
||||
// 评级说明(与"超过正常"程度绑定,4 档):
|
||||
// - 远低于正常 red #dc2626
|
||||
// - 达到正常 green #16a34a
|
||||
// - 超过正常 blue #3b82f6
|
||||
// - 远超过正常 purple #7c3aed
|
||||
//
|
||||
// 评分方向:
|
||||
// - higher_better: 数值越大越好(CPU/内存跑分、IOPS、吞吐)
|
||||
// - lower_better : 数值越小越好(温度)
|
||||
//
|
||||
// 阈值(Cutoffs):3 个分界点将数值划分为 4 档。
|
||||
// - higher_better: <c0 远低于, c0~c1 正常, c1~c2 超过, ≥c2 远超过
|
||||
// - lower_better : >c0 远低于(危险), c1~c0 正常, c2~c1 超过, ≤c2 远超过(优秀)
|
||||
//
|
||||
// 阈值仅作参考,应根据实际硬件代际持续校准;下文取自常见消费级/主流企业级硬件中位水平。
|
||||
|
||||
// Level 评级
|
||||
type Level string
|
||||
|
||||
const (
|
||||
LevelPoor Level = "poor" // 远低于正常
|
||||
LevelNormal Level = "normal" // 达到正常
|
||||
LevelGood Level = "good" // 超过正常
|
||||
LevelExcellent Level = "excellent" // 远超过正常
|
||||
)
|
||||
|
||||
// LevelDisplay 评级显示元数据
|
||||
type LevelDisplay struct {
|
||||
Text string // 远低于正常 / 达到正常 / 超过正常 / 远超过正常
|
||||
Color string // 颜色(与报告 CSS 协调)
|
||||
Order int // 0/1/2/3 用于排序与综合评级
|
||||
}
|
||||
|
||||
var levelDisplay = map[Level]LevelDisplay{
|
||||
LevelPoor: {Text: "远低于正常", Color: "#dc2626", Order: 0},
|
||||
LevelNormal: {Text: "达到正常", Color: "#16a34a", Order: 1},
|
||||
LevelGood: {Text: "超过正常", Color: "#3b82f6", Order: 2},
|
||||
LevelExcellent: {Text: "远超过正常", Color: "#7c3aed", Order: 3},
|
||||
}
|
||||
|
||||
// LevelMeta 取评级显示信息,未知时返回 normal
|
||||
func LevelMeta(l Level) LevelDisplay {
|
||||
if d, ok := levelDisplay[l]; ok {
|
||||
return d
|
||||
}
|
||||
return LevelDisplay{Text: string(l), Color: "#64748b", Order: 1}
|
||||
}
|
||||
|
||||
// Direction 评分方向
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
DirHigherBetter Direction = "higher" // 越高越好
|
||||
DirLowerBetter Direction = "lower" // 越低越好
|
||||
)
|
||||
|
||||
// MetricBench 单个指标的基准配置
|
||||
type MetricBench struct {
|
||||
Key string // 指标键:score / max_temp / iops_total / bw_total
|
||||
Label string // 显示名
|
||||
Unit string // 单位
|
||||
Direction Direction // 评分方向
|
||||
Cutoffs [3]float64
|
||||
Note string // 备注(用于展示)
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 各指标基准(消费/主流企业级硬件参考)
|
||||
// ============================
|
||||
|
||||
// CPU 跑分:stress-ng bogo ops/s,4 线程参考(线程越多分越高)
|
||||
var benchCPU = MetricBench{
|
||||
Key: "score", Label: "CPU 跑分", Unit: "ops/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{8000, 20000, 40000},
|
||||
Note: "stress-ng bogo ops/s(4 线程参考;线程数越多分越高)",
|
||||
}
|
||||
|
||||
// 内存跑分
|
||||
var benchMemory = MetricBench{
|
||||
Key: "score", Label: "内存跑分", Unit: "ops/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{5000, 15000, 30000},
|
||||
Note: "stress-ng bogo ops/s",
|
||||
}
|
||||
|
||||
// 综合工况:CPU+内存 综合
|
||||
var benchFull = MetricBench{
|
||||
Key: "score", Label: "综合跑分", Unit: "ops/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{6000, 18000, 35000},
|
||||
Note: "CPU+内存综合 ops/s",
|
||||
}
|
||||
|
||||
// 内存精压:MB/s(DDR4 一般 20-50 GB/s,受通道数和频率影响)
|
||||
var benchMemNative = MetricBench{
|
||||
Key: "score", Label: "内存带宽", Unit: "MB/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{3000, 8000, 15000},
|
||||
Note: "内存读写带宽",
|
||||
}
|
||||
|
||||
// 磁盘 4K 随机读写 IOPS
|
||||
var benchDiskIOPS = MetricBench{
|
||||
Key: "iops_total", Label: "磁盘 IOPS", Unit: "ops/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{1000, 10000, 50000},
|
||||
Note: "4K 随机读写:<1k≈HDD, 1k-10k≈低端 SSD, 10k-50k≈SATA/NVMe 入门, >50k≈NVMe 中高端",
|
||||
}
|
||||
|
||||
// 磁盘读写总带宽
|
||||
var benchDiskBW = MetricBench{
|
||||
Key: "bw_total", Label: "磁盘吞吐", Unit: "MB/s",
|
||||
Direction: DirHigherBetter,
|
||||
Cutoffs: [3]float64{100, 500, 2000},
|
||||
Note: "读写总带宽:<100≈HDD, 100-500≈低端 SSD, 500-2000≈SATA/NVMe 入门, >2000≈NVMe Gen3+",
|
||||
}
|
||||
|
||||
// 最高温度(°C,越低越好)
|
||||
var benchTemp = MetricBench{
|
||||
Key: "max_temp", Label: "最高温度", Unit: "°C",
|
||||
Direction: DirLowerBetter,
|
||||
Cutoffs: [3]float64{90, 75, 60}, // 阈值由高到低:poor/normal, normal/good, good/excellent
|
||||
Note: "<60°C 优秀, 60-75°C 正常, 75-90°C 偏高, >90°C 危险",
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 硬件基准库(按 CPU 型号匹配)
|
||||
// ============================
|
||||
//
|
||||
// 设计目的:不同代/不同档位的硬件不能用同一套阈值比较。
|
||||
// 例如 i3 与 i7 跑分差距数倍;J4125 与 N100 也不能直接比较。
|
||||
//
|
||||
// 使用方式:
|
||||
// - MatchHardware(cpuModel) 依据 CPU 型号子串匹配(大小写不敏感)
|
||||
// - 匹配成功 → 使用该硬件的专属阈值评估
|
||||
// - 未匹配 → 所有指标强制返回 LevelNormal("达到正常"),不判定好坏
|
||||
// - 库中字段为 nil → 退到默认基准
|
||||
//
|
||||
// 维护说明:库按"匹配顺序"查找,越靠前越优先。新增型号请在最匹配的
|
||||
// 位置追加(i5-12xxx 必须在 i5 之前),否则会被更宽泛的规则先匹配。
|
||||
|
||||
// HardwareBench 一类硬件的基准
|
||||
type HardwareBench struct {
|
||||
// Match CPU 型号匹配子串(大小写不敏感),如 "i7-12"、"N100"、"Ryzen 5"
|
||||
Match string
|
||||
// Note 硬件说明,会显示在报告备注里
|
||||
Note string
|
||||
// CPU/Memory/Full/MemBand 各类跑分基准;nil 表示使用默认基准
|
||||
CPU *MetricBench
|
||||
Memory *MetricBench
|
||||
Full *MetricBench
|
||||
MemBand *MetricBench
|
||||
}
|
||||
|
||||
// hardwareLibrary 硬件基准库(按匹配顺序查找,越靠前越优先)
|
||||
// 数值参考:stress-ng bogo ops/s @ 默认线程数;内存带宽为 stream/memcpy 估算
|
||||
// 维护提醒:每代 CPU 阈值约为上一代的 1.15~1.30 倍,跨代需重新校准
|
||||
var hardwareLibrary = []HardwareBench{
|
||||
// ============ Intel 飞牛常用低功耗 ============
|
||||
{
|
||||
Match: "N100", Note: "Intel N100 (Alder Lake-N, 4C/4T, 6W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 22000, 38000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 12000, 22000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 18000, 30000}, Note: "CPU+内存综合"},
|
||||
MemBand: &MetricBench{Key: "score", Label: "内存带宽", Unit: "MB/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 8000, 15000}, Note: "单通道 DDR5 带宽上限"},
|
||||
},
|
||||
{
|
||||
Match: "N305", Note: "Intel N305 (Alder Lake-N, 8C/8T, 15W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 40000, 65000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 18000, 35000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{15000, 30000, 50000}, Note: "CPU+内存综合"},
|
||||
MemBand: &MetricBench{Key: "score", Label: "内存带宽", Unit: "MB/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 10000, 18000}, Note: "DDR5 双通道带宽"},
|
||||
},
|
||||
{
|
||||
Match: "N95", Note: "Intel N95 (Alder Lake-N, 4C/4T, 15W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 20000, 35000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 11000, 20000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 16000, 28000}, Note: "CPU+内存综合"},
|
||||
MemBand: &MetricBench{Key: "score", Label: "内存带宽", Unit: "MB/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 7000, 13000}, Note: "单通道 DDR4/DDR5 带宽"},
|
||||
},
|
||||
{
|
||||
Match: "N5105", Note: "Intel Celeron N5105 (Jasper Lake, 4C/4T, 10W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{7000, 14000, 25000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 9000, 16000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 12000, 22000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "N5095", Note: "Intel Celeron N5095 (Jasper Lake, 4C/4T, 15W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{6000, 12000, 22000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 8000, 15000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 10000, 19000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "J4125", Note: "Intel Celeron J4125 (Gemini Lake, 4C/4T, 10W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 10000, 18000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 6000, 12000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 8000, 15000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "J4105", Note: "Intel Celeron J4105 (Gemini Lake, 4C/4T, 10W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4500, 9000, 16000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 6000, 11000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 7500, 14000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "J3455", Note: "Intel Celeron J3455 (Apollo Lake, 4C/4T, 10W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3500, 7500, 13000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{2500, 5000, 9500}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 6500, 12000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "J1900", Note: "Intel Celeron J1900 (Bay Trail, 4C/4T, 10W)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{2500, 5500, 10000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{2000, 4000, 7000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{2000, 4500, 8500}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Celeron", Note: "Intel Celeron (其他型号)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 12000, 22000}, Note: "Celeron 系列通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 7000, 14000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 10000, 18000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Atom", Note: "Intel Atom",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{2000, 5000, 9000}, Note: "Atom 系列通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{1500, 3500, 6500}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{1800, 4500, 8000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ Intel Core i9(按代匹配) ============
|
||||
{
|
||||
Match: "i9-13", Note: "13代 i9 (Raptor Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{50000, 100000, 160000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 50000, 90000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{30000, 70000, 120000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i9-12", Note: "12代 i9 (Alder Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{40000, 85000, 140000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 45000, 80000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{28000, 60000, 100000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i9-11", Note: "11代 i9 (Rocket Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{35000, 75000, 125000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{15000, 38000, 70000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{25000, 55000, 95000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i9-10", Note: "10代 i9 (Comet Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{30000, 65000, 110000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 35000, 65000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 48000, 85000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i9-", Note: "Intel Core i9 (其他代)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{25000, 55000, 95000}, Note: "i9 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{12000, 30000, 60000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 45000, 80000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ Intel Core i7(按代匹配) ============
|
||||
{
|
||||
Match: "i7-13", Note: "13代 i7 (Raptor Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{40000, 85000, 140000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 45000, 80000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{28000, 60000, 100000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-12", Note: "12代 i7 (Alder Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{32000, 70000, 115000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{16000, 40000, 70000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{24000, 52000, 90000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-11", Note: "11代 i7 (Rocket Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{28000, 60000, 100000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 35000, 60000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 48000, 80000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-10", Note: "10代 i7 (Comet Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{25000, 55000, 90000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{12000, 30000, 55000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 42000, 72000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-8", Note: "8/9代 i7 (Coffee Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 45000, 75000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 25000, 45000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{16000, 35000, 60000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-7", Note: "7代 i7 (Kaby Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{16000, 35000, 60000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 20000, 38000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{12000, 28000, 48000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i7-", Note: "Intel Core i7 (其他代)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 40000, 70000}, Note: "i7 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{9000, 22000, 42000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 32000, 55000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ Intel Core i5(按代匹配) ============
|
||||
{
|
||||
Match: "i5-13", Note: "13代 i5 (Raptor Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{30000, 65000, 105000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 35000, 60000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 48000, 80000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i5-12", Note: "12代 i5 (Alder Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{25000, 55000, 90000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{12000, 30000, 55000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 40000, 70000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i5-11", Note: "11代 i5 (Rocket Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 48000, 80000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{11000, 27000, 48000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{16000, 35000, 60000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i5-10", Note: "10代 i5 (Comet Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 42000, 70000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 25000, 45000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 32000, 55000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i5-8", Note: "8/9代 i5 (Coffee Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{15000, 32000, 55000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 18000, 35000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 24000, 42000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i5-", Note: "Intel Core i5 (其他代)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 30000, 50000}, Note: "i5 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{7000, 17000, 32000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 22000, 40000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ Intel Core i3(按代匹配) ============
|
||||
{
|
||||
Match: "i3-13", Note: "13代 i3 (Raptor Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 42000, 70000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 24000, 45000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{15000, 32000, 55000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i3-12", Note: "12代 i3 (Alder Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 38000, 62000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{9000, 22000, 40000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{13000, 28000, 50000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i3-10", Note: "10代 i3 (Comet Lake)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 30000, 50000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{7000, 17000, 32000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 22000, 40000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "i3-", Note: "Intel Core i3 (其他代)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 22000, 38000}, Note: "i3 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5500, 13000, 25000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 18000, 32000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ Intel Xeon ============
|
||||
{
|
||||
Match: "Xeon", Note: "Intel Xeon",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{30000, 70000, 120000}, Note: "Xeon 通用参考(按核心数浮动大)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{15000, 35000, 70000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 50000, 90000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
|
||||
// ============ AMD Ryzen(按系列) ============
|
||||
{
|
||||
Match: "Ryzen 9", Note: "AMD Ryzen 9",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{40000, 85000, 145000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{18000, 45000, 85000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{30000, 65000, 110000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Ryzen 7", Note: "AMD Ryzen 7",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{28000, 60000, 100000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{13000, 32000, 60000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{20000, 45000, 78000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Ryzen 5", Note: "AMD Ryzen 5",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{22000, 48000, 80000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{11000, 26000, 48000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{16000, 36000, 62000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Ryzen 3", Note: "AMD Ryzen 3",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{14000, 30000, 50000}, Note: "stress-ng bogo ops/s(4 线程)"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{7000, 17000, 32000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{10000, 22000, 40000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "Athlon", Note: "AMD Athlon",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{5000, 11000, 20000}, Note: "Athlon 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{3000, 7000, 13000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 9000, 17000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
{
|
||||
Match: "AMD", Note: "AMD 处理器 (其他型号)",
|
||||
CPU: &MetricBench{Key: "score", Label: "CPU 跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{8000, 18000, 32000}, Note: "AMD 通用参考"},
|
||||
Memory: &MetricBench{Key: "score", Label: "内存跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{4000, 10000, 18000}, Note: "stress-ng bogo ops/s"},
|
||||
Full: &MetricBench{Key: "score", Label: "综合跑分", Unit: "ops/s", Direction: DirHigherBetter, Cutoffs: [3]float64{6000, 14000, 25000}, Note: "CPU+内存综合"},
|
||||
},
|
||||
}
|
||||
|
||||
// MatchHardware 根据 CPU 型号匹配硬件基准(大小写不敏感,子串匹配)
|
||||
// 返回 nil 表示未在库中(调用方应将所有指标视为"达到正常",不做好坏判定)
|
||||
func MatchHardware(cpuModel string) *HardwareBench {
|
||||
if cpuModel == "" {
|
||||
return nil
|
||||
}
|
||||
lower := strings.ToLower(cpuModel)
|
||||
for i := range hardwareLibrary {
|
||||
if strings.Contains(lower, strings.ToLower(hardwareLibrary[i].Match)) {
|
||||
return &hardwareLibrary[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 评估函数
|
||||
// ============================
|
||||
|
||||
// EvalBench 根据基准评估单个数值
|
||||
func EvalBench(b MetricBench, value float64) Level {
|
||||
c := b.Cutoffs
|
||||
switch b.Direction {
|
||||
case DirHigherBetter:
|
||||
switch {
|
||||
case value < c[0]:
|
||||
return LevelPoor
|
||||
case value < c[1]:
|
||||
return LevelNormal
|
||||
case value < c[2]:
|
||||
return LevelGood
|
||||
default:
|
||||
return LevelExcellent
|
||||
}
|
||||
case DirLowerBetter:
|
||||
switch {
|
||||
case value > c[0]:
|
||||
return LevelPoor
|
||||
case value > c[1]:
|
||||
return LevelNormal
|
||||
case value > c[2]:
|
||||
return LevelGood
|
||||
default:
|
||||
return LevelExcellent
|
||||
}
|
||||
}
|
||||
return LevelNormal
|
||||
}
|
||||
|
||||
// ReferenceText 生成"参考范围"展示文本
|
||||
func (b MetricBench) ReferenceText() string {
|
||||
c := b.Cutoffs
|
||||
if b.Direction == DirHigherBetter {
|
||||
return fmt.Sprintf(
|
||||
"参考: <%.0f%s 远低于 · %.0f-%.0f%s 正常 · %.0f-%.0f%s 良好 · ≥%.0f%s 卓越",
|
||||
c[0], b.Unit, c[0], c[1], b.Unit, c[1], c[2], b.Unit, c[2], b.Unit,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"参考: >%.0f%s 危险 · %.0f-%.0f%s 偏高 · %.0f-%.0f%s 正常 · ≤%.0f%s 优秀",
|
||||
c[0], b.Unit, c[1], c[0], b.Unit, c[2], c[1], b.Unit, c[2], b.Unit,
|
||||
)
|
||||
}
|
||||
|
||||
// BenchItem 一次基准评估的展示单元(序列化到报告)
|
||||
type BenchItem struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"` // 格式化后的值,如 "72.0 °C"
|
||||
ValueNum float64 `json:"value_num"` // 原始数值
|
||||
Unit string `json:"unit"`
|
||||
Level Level `json:"level"`
|
||||
LevelText string `json:"level_text"`
|
||||
LevelColor string `json:"level_color"`
|
||||
Reference string `json:"reference"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
// OverallLevel 取多个评级中"最差"(最差用于提示风险)
|
||||
func OverallLevel(items []BenchItem) Level {
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
worst := LevelExcellent
|
||||
for _, it := range items {
|
||||
if LevelMeta(it.Level).Order < LevelMeta(worst).Order {
|
||||
worst = it.Level
|
||||
}
|
||||
}
|
||||
return worst
|
||||
}
|
||||
|
||||
// EvaluateResult 对一个 Result 评估所有可用指标的基准
|
||||
// 返回的列表按"重要性"排序:跑分 → 温度 → IOPS → 吞吐
|
||||
// cpuModel: 设备 CPU 型号(来自 sysInfo["cpu"]);空串或不在库中时,所有指标项
|
||||
// 强制返回 LevelNormal("达到正常"),不做好坏判定,避免误报
|
||||
func EvaluateResult(r Result, cpuModel string) []BenchItem {
|
||||
hb := MatchHardware(cpuModel)
|
||||
inLibrary := hb != nil
|
||||
|
||||
var items []BenchItem
|
||||
|
||||
makeItem := func(b MetricBench, v float64) BenchItem {
|
||||
// 未在库中 → 强制 normal;已匹配 → 按专属阈值评估
|
||||
lvl := LevelNormal
|
||||
if inLibrary {
|
||||
lvl = EvalBench(b, v)
|
||||
}
|
||||
d := LevelMeta(lvl)
|
||||
return BenchItem{
|
||||
Key: b.Key,
|
||||
Label: b.Label,
|
||||
Value: fmt.Sprintf("%.1f %s", v, b.Unit),
|
||||
ValueNum: v,
|
||||
Unit: b.Unit,
|
||||
Level: lvl,
|
||||
LevelText: d.Text,
|
||||
LevelColor: d.Color,
|
||||
Reference: b.ReferenceText(),
|
||||
Note: b.Note,
|
||||
}
|
||||
}
|
||||
|
||||
// pickBench 选基准:库中有专属 → 专属;库中无该字段或未在库 → 默认
|
||||
pickBench := func(field *MetricBench, defaultBench MetricBench) MetricBench {
|
||||
if hb != nil && field != nil {
|
||||
return *field
|
||||
}
|
||||
return defaultBench
|
||||
}
|
||||
|
||||
// 1) 跑分(按测试类型选基准)
|
||||
if r.Score > 0 {
|
||||
var b MetricBench
|
||||
switch r.Type {
|
||||
case TestCPU:
|
||||
if hb != nil {
|
||||
b = pickBench(hb.CPU, benchCPU)
|
||||
} else {
|
||||
b = benchCPU
|
||||
}
|
||||
case TestMemory:
|
||||
if hb != nil {
|
||||
b = pickBench(hb.Memory, benchMemory)
|
||||
} else {
|
||||
b = benchMemory
|
||||
}
|
||||
case TestFull:
|
||||
if hb != nil {
|
||||
b = pickBench(hb.Full, benchFull)
|
||||
} else {
|
||||
b = benchFull
|
||||
}
|
||||
case TestMemNative:
|
||||
if hb != nil {
|
||||
b = pickBench(hb.MemBand, benchMemNative)
|
||||
} else {
|
||||
b = benchMemNative
|
||||
}
|
||||
}
|
||||
if b.Key != "" {
|
||||
items = append(items, makeItem(b, r.Score))
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 温度(max_temp 存在即评估)
|
||||
// 温度阈值为通用参考(60/75/90°C),未在库中同样会按通用阈值评估并强制 normal
|
||||
if t, ok := r.Metrics["max_temp"]; ok && t > 0 {
|
||||
items = append(items, makeItem(benchTemp, t))
|
||||
}
|
||||
|
||||
// 3) 磁盘 IOPS(通用参考,与 CPU 无关)
|
||||
if t, ok := r.Metrics["iops_total"]; ok && t > 0 {
|
||||
items = append(items, makeItem(benchDiskIOPS, t))
|
||||
}
|
||||
|
||||
// 4) 磁盘吞吐(通用参考,与 CPU 无关)
|
||||
if t, ok := r.Metrics["bw_total"]; ok && t > 0 {
|
||||
items = append(items, makeItem(benchDiskBW, t))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
246
auto-check/pkg/stress/benchmark_test.go
Normal file
246
auto-check/pkg/stress/benchmark_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package stress
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEvalBench 验证基准评估逻辑
|
||||
func TestEvalBench(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bench MetricBench
|
||||
val float64
|
||||
want Level
|
||||
}{
|
||||
// CPU 跑分(越高越好):4 档
|
||||
{"CPU-远低于", benchCPU, 5000, LevelPoor},
|
||||
{"CPU-正常", benchCPU, 15000, LevelNormal},
|
||||
{"CPU-超过", benchCPU, 30000, LevelGood},
|
||||
{"CPU-远超过", benchCPU, 50000, LevelExcellent},
|
||||
{"CPU-边界-正常下限", benchCPU, 8000, LevelNormal},
|
||||
{"CPU-边界-超过下限", benchCPU, 20000, LevelGood},
|
||||
{"CPU-边界-远超过下限", benchCPU, 40000, LevelExcellent},
|
||||
|
||||
// 温度(越低越好)
|
||||
{"Temp-远超过(危险)", benchTemp, 95, LevelPoor},
|
||||
{"Temp-偏高", benchTemp, 80, LevelNormal},
|
||||
{"Temp-正常", benchTemp, 70, LevelGood},
|
||||
{"Temp-优秀", benchTemp, 50, LevelExcellent},
|
||||
{"Temp-边界-正常", benchTemp, 90, LevelNormal},
|
||||
{"Temp-边界-良好", benchTemp, 75, LevelGood},
|
||||
{"Temp-边界-优秀", benchTemp, 60, LevelExcellent},
|
||||
|
||||
// 磁盘 IOPS
|
||||
{"IOPS-远低于(HDD)", benchDiskIOPS, 500, LevelPoor},
|
||||
{"IOPS-正常(低端SSD)", benchDiskIOPS, 5000, LevelNormal},
|
||||
{"IOPS-超过(SATA)", benchDiskIOPS, 30000, LevelGood},
|
||||
{"IOPS-远超过(NVMe)", benchDiskIOPS, 80000, LevelExcellent},
|
||||
|
||||
// 磁盘吞吐
|
||||
{"BW-远低于(HDD)", benchDiskBW, 50, LevelPoor},
|
||||
{"BW-正常", benchDiskBW, 300, LevelNormal},
|
||||
{"BW-超过", benchDiskBW, 1000, LevelGood},
|
||||
{"BW-远超过", benchDiskBW, 3000, LevelExcellent},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := EvalBench(tc.bench, tc.val)
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: val=%.0f got=%s want=%s", tc.name, tc.val, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvaluateResult 验证 Result 评估
|
||||
func TestEvaluateResult(t *testing.T) {
|
||||
// 模拟一次 CPU 压测结果:跑分 25000(超过正常),最高温度 70°C(良好)
|
||||
r := Result{
|
||||
Type: TestCPU,
|
||||
Score: 25000,
|
||||
Metrics: map[string]float64{
|
||||
"max_temp": 70,
|
||||
},
|
||||
}
|
||||
// 使用 N100 库:CPU 跑分阈值 10000/22000/38000 → 25000 介于 22k-38k,应为 LevelGood
|
||||
items := EvaluateResult(r, "Intel N100")
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("期望 2 个评估项(跑分+温度),实际 %d", len(items))
|
||||
}
|
||||
// 第一项是 CPU 跑分
|
||||
if items[0].Level != LevelGood {
|
||||
t.Errorf("CPU 跑分评级错误: got=%s want=%s", items[0].Level, LevelGood)
|
||||
}
|
||||
// 第二项是温度(70°C 在 60-75 之间,应为 LevelGood)
|
||||
if items[1].Level != LevelGood {
|
||||
t.Errorf("温度评级错误: got=%s want=%s", items[1].Level, LevelGood)
|
||||
}
|
||||
// 综合评级应取最差
|
||||
if OverallLevel(items) != LevelGood {
|
||||
t.Errorf("综合评级错误: got=%s", OverallLevel(items))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvaluateResultDisk 验证磁盘测试评估
|
||||
func TestEvaluateResultDisk(t *testing.T) {
|
||||
// 模拟一次磁盘压测:吞吐 1500MB/s(良好),IOPS 80000(远超过)
|
||||
r := Result{
|
||||
Type: TestDiskIO,
|
||||
Score: 1500, // score 是 bw_total
|
||||
Metrics: map[string]float64{
|
||||
"iops_total": 80000,
|
||||
"bw_total": 1500,
|
||||
},
|
||||
}
|
||||
// 磁盘阈值与 CPU 型号无关,传任意库内型号都能正常评估
|
||||
items := EvaluateResult(r, "Intel N100")
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("期望 2 个评估项,实际 %d", len(items))
|
||||
}
|
||||
// 找 IOPS 项
|
||||
var iops, bw BenchItem
|
||||
for _, it := range items {
|
||||
switch it.Key {
|
||||
case "iops_total":
|
||||
iops = it
|
||||
case "bw_total":
|
||||
bw = it
|
||||
}
|
||||
}
|
||||
if iops.Level != LevelExcellent {
|
||||
t.Errorf("IOPS 80000 应评为远超过正常: got=%s", iops.Level)
|
||||
}
|
||||
if bw.Level != LevelGood {
|
||||
t.Errorf("吞吐 1500 应评为超过正常: got=%s", bw.Level)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchHardware 验证硬件库匹配
|
||||
func TestMatchHardware(t *testing.T) {
|
||||
tests := []struct {
|
||||
cpu string
|
||||
wantNil bool
|
||||
wantNote string // 仅检查 Note 包含的子串
|
||||
}{
|
||||
// 飞牛常用低功耗
|
||||
{"Intel(R) Celeron(R) N100 CPU @ 0.80GHz", false, "N100"},
|
||||
{"Intel N305", false, "N305"},
|
||||
{"12th Gen Intel(R) Core(TM) i3-12100", false, "12代 i3"},
|
||||
{"13th Gen Intel(R) Core(TM) i5-13500", false, "13代 i5"},
|
||||
{"11th Gen Intel(R) Core(TM) i7-11700K", false, "11代 i7"},
|
||||
{"Intel(R) Core(TM) i9-12900K CPU", false, "12代 i9"},
|
||||
{"Intel(R) Celeron(R) J4125 CPU @ 2.00GHz", false, "J4125"},
|
||||
{"Intel(R) Celeron(R) N5095 @ 2.00GHz", false, "N5095"},
|
||||
|
||||
// AMD
|
||||
{"AMD Ryzen 5 5600 6-Core Processor", false, "Ryzen 5"},
|
||||
{"AMD Ryzen 7 5800X 8-Core", false, "Ryzen 7"},
|
||||
{"AMD Ryzen 9 5900X", false, "Ryzen 9"},
|
||||
|
||||
// 大小写不敏感
|
||||
{"INTEL N100", false, "N100"},
|
||||
{"intel celeron n100", false, "N100"},
|
||||
|
||||
// 不在库中
|
||||
{"", true, ""},
|
||||
{"Some Unknown CPU XYZ", true, ""},
|
||||
{"Apple M1", true, ""},
|
||||
{"Loongson 3A5000", true, ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := MatchHardware(tc.cpu)
|
||||
if tc.wantNil {
|
||||
if got != nil {
|
||||
t.Errorf("cpu=%q 应返回 nil,得到 %v (Note=%s)", tc.cpu, got, got.Note)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == nil {
|
||||
t.Errorf("cpu=%q 应匹配到硬件,得到 nil", tc.cpu)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(got.Note, tc.wantNote) {
|
||||
t.Errorf("cpu=%q 匹配到 %q,但 Note 应包含 %q", tc.cpu, got.Note, tc.wantNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchHardware_Priority 验证匹配顺序优先级(i5-12xxx 必须优先于 i5 通用)
|
||||
func TestMatchHardware_Priority(t *testing.T) {
|
||||
hb := MatchHardware("Intel Core i5-12400")
|
||||
if hb == nil {
|
||||
t.Fatal("i5-12400 应匹配到硬件")
|
||||
}
|
||||
if !strings.Contains(hb.Note, "12代 i5") {
|
||||
t.Errorf("i5-12400 应优先匹配 12代 i5,实际 Note=%s", hb.Note)
|
||||
}
|
||||
|
||||
// 不带代号的 i5 应匹配通用条目
|
||||
hb = MatchHardware("Intel Core i5-4400") // 老 4 代
|
||||
if hb == nil {
|
||||
t.Fatal("i5-4400 应匹配到硬件")
|
||||
}
|
||||
if !strings.Contains(hb.Note, "i5") {
|
||||
t.Errorf("i5-4400 应匹配 i5 系列,实际 Note=%s", hb.Note)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvaluateResult_HardwareAware 验证同一分数在不同硬件下的评级不同
|
||||
func TestEvaluateResult_HardwareAware(t *testing.T) {
|
||||
r := Result{Type: TestCPU, Score: 25000}
|
||||
mr := map[string]float64{"max_temp": 70}
|
||||
|
||||
// N100:阈值 10000/22000/38000 → 25000 介于 22k-38k → Good
|
||||
rN100 := r
|
||||
rN100.Metrics = mr
|
||||
itemsN100 := EvaluateResult(rN100, "Intel N100")
|
||||
if itemsN100[0].Level != LevelGood {
|
||||
t.Errorf("N100 跑分 25k 应为 Good,实际 %s", itemsN100[0].Level)
|
||||
}
|
||||
|
||||
// i3-12100:阈值 18000/38000/62000 → 25000 介于 18k-38k → Normal
|
||||
itemsI3 := EvaluateResult(r, "Intel Core i3-12100")
|
||||
if itemsI3[0].Level != LevelNormal {
|
||||
t.Errorf("i3-12100 跑分 25k 应为 Normal,实际 %s", itemsI3[0].Level)
|
||||
}
|
||||
|
||||
// i7-12700:阈值 32000/70000/115000 → 25000 < 32k → Poor
|
||||
itemsI7 := EvaluateResult(r, "Intel Core i7-12700")
|
||||
if itemsI7[0].Level != LevelPoor {
|
||||
t.Errorf("i7-12700 跑分 25k 应为 Poor(明显低于 i7 水平),实际 %s", itemsI7[0].Level)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvaluateResult_UnknownHardware 验证未在库中的硬件全部返回 normal
|
||||
func TestEvaluateResult_UnknownHardware(t *testing.T) {
|
||||
r := Result{
|
||||
Type: TestCPU,
|
||||
Score: 25000,
|
||||
Metrics: map[string]float64{
|
||||
"max_temp": 70,
|
||||
"iops_total": 80000,
|
||||
"bw_total": 1500,
|
||||
},
|
||||
}
|
||||
|
||||
// 1) 空字符串
|
||||
items := EvaluateResult(r, "")
|
||||
if len(items) != 4 {
|
||||
t.Fatalf("空 cpuModel 应返回 4 项,实际 %d", len(items))
|
||||
}
|
||||
for _, it := range items {
|
||||
if it.Level != LevelNormal {
|
||||
t.Errorf("空 cpuModel 时 %s 应为 Normal,实际 %s", it.Label, it.Level)
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 未知型号
|
||||
items = EvaluateResult(r, "SomeUnknownCPU XYZ")
|
||||
if len(items) != 4 {
|
||||
t.Fatalf("未知 cpuModel 应返回 4 项,实际 %d", len(items))
|
||||
}
|
||||
for _, it := range items {
|
||||
if it.Level != LevelNormal {
|
||||
t.Errorf("未知 cpuModel 时 %s 应为 Normal,实际 %s", it.Label, it.Level)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -51,16 +52,20 @@ type chartPayload struct {
|
||||
|
||||
// testCard 单个测试卡片数据
|
||||
type testCard struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"`
|
||||
Duration string `json:"duration"`
|
||||
Score float64 `json:"score"`
|
||||
ScoreUnit string `json:"score_unit"`
|
||||
ScoreHint string `json:"score_hint"`
|
||||
Metrics []kvPair `json:"metrics"`
|
||||
Output string `json:"output"`
|
||||
Chart *chartPayload `json:"chart,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"`
|
||||
Duration string `json:"duration"`
|
||||
Score float64 `json:"score"`
|
||||
ScoreUnit string `json:"score_unit"`
|
||||
ScoreHint string `json:"score_hint"`
|
||||
Metrics []kvPair `json:"metrics"`
|
||||
Benchmarks []BenchItem `json:"benchmarks,omitempty"` // 性能/温度基准评级
|
||||
OverallLevel Level `json:"overall_level,omitempty"` // 综合评级(各项最差)
|
||||
OverallText string `json:"overall_text,omitempty"`
|
||||
OverallColor string `json:"overall_color,omitempty"`
|
||||
Output string `json:"output"`
|
||||
Chart *chartPayload `json:"chart,omitempty"`
|
||||
}
|
||||
|
||||
// reportHTMLData 报告整体数据
|
||||
@@ -76,7 +81,14 @@ type reportHTMLData struct {
|
||||
}
|
||||
|
||||
// WriteReportHTML 生成综合 HTML 报告(跑分卡片 + 多曲线图),返回文件路径
|
||||
// 默认文件名:report-<ip>.html
|
||||
func WriteReportHTML(ip string, report *Report, reportDir string) (string, error) {
|
||||
filename := fmt.Sprintf("report-%s.html", SanitizeFilename(ip))
|
||||
return WriteReportHTMLTo(ip, report, reportDir, filename)
|
||||
}
|
||||
|
||||
// WriteReportHTMLTo 生成综合 HTML 报告到指定文件名,返回完整文件路径
|
||||
func WriteReportHTMLTo(ip string, report *Report, reportDir, filename string) (string, error) {
|
||||
data := buildReportData(ip, report)
|
||||
dataJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@@ -88,11 +100,11 @@ func WriteReportHTML(ip string, report *Report, reportDir string) (string, error
|
||||
if err := os.MkdirAll(reportDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename := fmt.Sprintf("%s/report-%s.html", reportDir, SanitizeFilename(ip))
|
||||
if err := os.WriteFile(filename, []byte(html), 0644); err != nil {
|
||||
path := filepath.Join(reportDir, filename)
|
||||
if err := os.WriteFile(path, []byte(html), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filename, nil
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func buildReportData(ip string, report *Report) reportHTMLData {
|
||||
@@ -105,13 +117,18 @@ func buildReportData(ip string, report *Report) reportHTMLData {
|
||||
Summary: report.Summary(),
|
||||
SysInfo: report.SysInfo,
|
||||
}
|
||||
// 硬件型号:从 sysInfo["cpu"] 读取,用于按硬件库匹配基准
|
||||
cpuModel := ""
|
||||
if report.SysInfo != nil {
|
||||
cpuModel = report.SysInfo["cpu"]
|
||||
}
|
||||
for _, res := range report.Results {
|
||||
data.Cards = append(data.Cards, buildCard(res))
|
||||
data.Cards = append(data.Cards, buildCard(cpuModel, res))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func buildCard(res Result) testCard {
|
||||
func buildCard(cpuModel string, res Result) testCard {
|
||||
card := testCard{
|
||||
Name: string(res.Type),
|
||||
Label: testLabel(res.Type),
|
||||
@@ -128,6 +145,16 @@ func buildCard(res Result) testCard {
|
||||
Value: fmt.Sprintf("%.1f%s", res.Metrics[name], MetricUnit(name)),
|
||||
})
|
||||
}
|
||||
// 性能/温度基准评级(仅在有数据时填充;按硬件库匹配专属阈值)
|
||||
if items := EvaluateResult(res, cpuModel); len(items) > 0 {
|
||||
card.Benchmarks = items
|
||||
if lvl := OverallLevel(items); lvl != "" {
|
||||
d := LevelMeta(lvl)
|
||||
card.OverallLevel = lvl
|
||||
card.OverallText = d.Text
|
||||
card.OverallColor = d.Color
|
||||
}
|
||||
}
|
||||
if len(res.Samples) > 0 {
|
||||
if payload, ok := buildChart(res); ok {
|
||||
card.Chart = &payload
|
||||
|
||||
@@ -65,7 +65,7 @@ type Config struct {
|
||||
TempLogInt time.Duration
|
||||
TempLimit float64 // 温度上限(°C),0=不判定
|
||||
// 压测统一在 Docker 容器中执行(工具预装在镜像里)
|
||||
DockerImage string // 压测镜像名称,如 stress-test:latest
|
||||
DockerImage string // 压测镜像名称,如 stress-test:20260824-153045(由 build.sh 自动维护)
|
||||
DockerTar string // 本地导出的镜像文件(docker save),目标机无镜像时上传并 load
|
||||
DockerArgs string // Docker 运行参数,如 "--network=host --privileged"
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ header .meta b{color:#fff;font-weight:600}
|
||||
.metric{display:flex;flex-direction:column;gap:2px;padding:8px 12px;background:#f1f5f9;border-radius:8px;min-width:88px}
|
||||
.metric .m-label{font-size:11px;color:#64748b}
|
||||
.metric .m-value{font-size:15px;font-weight:700;color:#0f172a}
|
||||
.bench{margin-top:14px;padding:12px 14px;background:#f8fafc;border-radius:10px;border:1px dashed #e2e8f0}
|
||||
.bench-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;gap:10px;flex-wrap:wrap}
|
||||
.bench-title{font-size:12px;font-weight:600;color:#475569;letter-spacing:.3px}
|
||||
.bench-overall{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:700;color:#fff}
|
||||
.bench-list{display:flex;flex-direction:column;gap:6px}
|
||||
.bench-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:6px 0;border-bottom:1px dashed #eef2f7}
|
||||
.bench-row:last-child{border-bottom:none}
|
||||
.bench-name{font-size:13px;font-weight:600;color:#0f172a;min-width:84px}
|
||||
.bench-val{font-size:13px;color:#334155;font-variant-numeric:tabular-nums;min-width:96px}
|
||||
.bench-tag{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:700;color:#fff;line-height:1.4}
|
||||
.bench-ref{font-size:11px;color:#64748b;flex:1;min-width:0}
|
||||
.bench-note{font-size:11px;color:#94a3b8;margin-top:4px;line-height:1.5}
|
||||
.chart-box{position:relative;height:260px;margin-top:6px}
|
||||
details.output{margin-top:14px;border-top:1px dashed #e2e8f0;padding-top:10px}
|
||||
details.output summary{cursor:pointer;font-size:12px;color:#64748b;user-select:none}
|
||||
@@ -84,6 +96,33 @@ function buildCard(card,idx){
|
||||
card.metrics.forEach(function(x){h+='<div class="metric"><span class="m-label">'+esc(x.label)+'</span><span class="m-value">'+esc(x.value)+'</span></div>';});
|
||||
h+='</div>';
|
||||
}
|
||||
if(card.benchmarks&&card.benchmarks.length){
|
||||
var overall=card.overall_level?card.overall_level:'';
|
||||
var overallText=card.overall_text||'';
|
||||
var overallColor=card.overall_color||'#64748b';
|
||||
h+='<div class="bench">';
|
||||
h+='<div class="bench-head"><span class="bench-title">基准评估(参考消费/主流企业级硬件)</span>';
|
||||
if(overall){h+='<span class="bench-overall" style="background:'+overallColor+'" title="综合评级(各项最差)">综合: '+esc(overallText)+'</span>';}
|
||||
h+='</div>';
|
||||
h+='<div class="bench-list">';
|
||||
card.benchmarks.forEach(function(b){
|
||||
h+='<div class="bench-row">';
|
||||
h+='<span class="bench-name">'+esc(b.label)+'</span>';
|
||||
h+='<span class="bench-val">'+esc(b.value)+'</span>';
|
||||
h+='<span class="bench-tag" style="background:'+esc(b.level_color)+'">'+esc(b.level_text)+'</span>';
|
||||
h+='<span class="bench-ref">'+esc(b.reference)+'</span>';
|
||||
h+='</div>';
|
||||
});
|
||||
h+='</div>';
|
||||
// 备注合并:去重后展示
|
||||
var notes={};
|
||||
card.benchmarks.forEach(function(b){if(b.note)notes[b.note]=1;});
|
||||
var noteArr=Object.keys(notes);
|
||||
if(noteArr.length){
|
||||
h+='<div class="bench-note">说明: '+esc(noteArr.join(' · '))+'</div>';
|
||||
}
|
||||
h+='</div>';
|
||||
}
|
||||
if(card.chart){h+='<div class="chart-box"><canvas id="c'+idx+'"></canvas></div>';}
|
||||
if(card.output&&card.status!=='pass'){
|
||||
h+='<details class="output"><summary>详细输出</summary><pre>'+esc(card.output)+'</pre></details>';
|
||||
|
||||
@@ -290,8 +290,12 @@ func (r *Runner) runInDocker() (string, error) {
|
||||
}
|
||||
|
||||
// 2. 构建 Docker 运行命令(通过环境变量传入参数)
|
||||
// 固定挂载:/sys 只读(容器内是独立 sysfs,sensors 需读宿主机硬件传感器节点才能采集温度);
|
||||
// /etc/localtime 只读(容器时区与宿主机一致,dmesg -T 与采样时间戳才准确)。
|
||||
containerName := fmt.Sprintf("auto-check-%d", time.Now().UnixNano())
|
||||
dockerRunCmd := fmt.Sprintf("docker run --rm --name %s "+
|
||||
"-v /sys:/sys:ro "+
|
||||
"-v /etc/localtime:/etc/localtime:ro "+
|
||||
"-e AUTOCHECK_TYPES=%s "+
|
||||
"-e AUTOCHECK_DURATION=%d "+
|
||||
"-e AUTOCHECK_THREADS=%d "+
|
||||
|
||||
@@ -40,17 +40,17 @@ func runPrivileged(client *ssh.Client, cmd, sudoPwd string) (string, error) {
|
||||
if out, err := sshclient.RunCommand(client, "id -u"); err == nil && strings.TrimSpace(out) == "0" {
|
||||
return sshclient.RunCommand(client, cmd)
|
||||
}
|
||||
// 非 root:尝试 sudo
|
||||
// 非 root:尝试 sudo(-p '' 静默密码提示,避免提示文本污染命令输出)
|
||||
if sudoPwd != "" {
|
||||
// echo 密码 | sudo -S 执行;整条命令用双引号包裹,避免命令内部单引号嵌套冲突。
|
||||
// 仅对命令内的双引号转义(docker 命令均不含双引号,安全)。
|
||||
quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`)
|
||||
wrapped := fmt.Sprintf("echo %s | sudo -S -- sh -c \"%s\"", shellQuote(sudoPwd), quoted)
|
||||
wrapped := fmt.Sprintf("echo %s | sudo -S -p '' -- sh -c \"%s\"", shellQuote(sudoPwd), quoted)
|
||||
return sshclient.RunCommand(client, wrapped)
|
||||
}
|
||||
// 无密码:先尝试免密 sudo (-n),失败则回退普通 sudo(会提示需要密码)
|
||||
quoted := strings.ReplaceAll(singleLine(cmd), `"`, `\"`)
|
||||
if out, err := sshclient.RunCommand(client, "sudo -n -- sh -c \""+quoted+"\""); err == nil {
|
||||
if out, err := sshclient.RunCommand(client, "sudo -n -p '' -- sh -c \""+quoted+"\""); err == nil {
|
||||
return out, nil
|
||||
}
|
||||
return sshclient.RunCommand(client, "sudo -- sh -c \""+quoted+"\"")
|
||||
|
||||
@@ -196,6 +196,11 @@ func (r *Report) Summary() string {
|
||||
|
||||
// ToText 文本格式报告
|
||||
func (r *Report) ToText() string {
|
||||
// 从 sysInfo 提取 CPU 型号用于按硬件库匹配基准
|
||||
cpuModel := ""
|
||||
if r.SysInfo != nil {
|
||||
cpuModel = r.SysInfo["cpu"]
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("══ [%s] 压力测试报告 ══\n", r.IP))
|
||||
sb.WriteString(fmt.Sprintf(" 耗时: %s\n\n", r.Duration.Round(time.Millisecond)))
|
||||
@@ -214,6 +219,13 @@ func (r *Report) ToText() string {
|
||||
if res.Score > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" 跑分: %.1f %s\n", res.Score, ScoreUnit(res.Type)))
|
||||
}
|
||||
// 基准评级(让用户对结果有感性认知,按硬件库匹配专属阈值)
|
||||
if items := EvaluateResult(res, cpuModel); len(items) > 0 {
|
||||
for _, it := range items {
|
||||
sb.WriteString(fmt.Sprintf(" [%s] %s %s — %s\n",
|
||||
it.LevelText, it.Label, it.Value, it.Reference))
|
||||
}
|
||||
}
|
||||
for _, name := range sortedMetricNames(res.Metrics) {
|
||||
unit := MetricUnit(name)
|
||||
sb.WriteString(fmt.Sprintf(" %s: %.1f%s\n", MetricLabel(name), res.Metrics[name], unit))
|
||||
|
||||
@@ -75,7 +75,7 @@ func (w *Workflow) round() {
|
||||
fmt.Printf(" [测试] %s SSH 连接失败,跳过报告输出: %s\n", ip, rpt.Results[0].Error)
|
||||
continue
|
||||
}
|
||||
report.SaveAndPrintDeviceReport(&dev, rpt, cfg.Report.Path, cfg.Report.Print)
|
||||
report.SaveAndPrintDeviceReport(&dev, rpt, cfg.Report.Path, cfg.Report.Print, cfg.Report.HTML)
|
||||
}
|
||||
|
||||
// 3. 打印状态变化(首次或与上次不一致)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build ignore
|
||||
|
||||
// 临时验证程序:验证 镜像上传 + docker load 分发链路(用完即删)
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"auto-check/pkg/sshclient"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ip := os.Args[1]
|
||||
cfg := sshclient.NewConfig("admin", "admin123", "", 22, 10*time.Second)
|
||||
cli, err := sshclient.Connect(ip, cfg)
|
||||
if err != nil {
|
||||
fmt.Println("SSH 连接失败:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer cli.Close()
|
||||
sshclient.EnsureHome(cli)
|
||||
|
||||
v, _ := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker version --format '{{.Server.Version}}' 2>/dev/null")
|
||||
fmt.Printf("Docker 版本: %q\n", strings.TrimSpace(v))
|
||||
|
||||
exists, _ := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker images stress-test:latest --format '{{.Repository}}:{{.Tag}}' 2>/dev/null")
|
||||
fmt.Printf("镜像状态: %q\n", strings.TrimSpace(exists))
|
||||
|
||||
if strings.TrimSpace(exists) == "" {
|
||||
if err := sshclient.UploadFile(cli, "docker/stress-test/stress-test.tar", "/tmp/auto-check-image.tar"); err != nil {
|
||||
fmt.Println("上传失败:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
out, err := sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker load -i /tmp/auto-check-image.tar 2>&1")
|
||||
sshclient.RunCommand(cli, "rm -f /tmp/auto-check-image.tar")
|
||||
fmt.Printf("load 输出: %s (err=%v)\n", out, err)
|
||||
exists, _ = sshclient.RunCommand(cli, "echo 'admin123' | sudo -S -- docker images stress-test:latest --format '{{.Repository}}:{{.Tag}}' 2>/dev/null")
|
||||
fmt.Printf("load 后镜像状态: %q\n", strings.TrimSpace(exists))
|
||||
}
|
||||
fmt.Println("验证完成")
|
||||
}
|
||||
Reference in New Issue
Block a user