diff --git a/internal/protocol/message.go b/internal/protocol/message.go index a95114d..84eb530 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -194,8 +194,7 @@ type GPUData struct { type TemperatureData struct { SensorKey string `json:"sensorKey"` Temperature float64 `json:"temperature"` - High float64 `json:"high,omitempty"` - Critical float64 `json:"critical,omitempty"` + Type string `json:"type"` } // CommandRequest 指令请求 diff --git a/internal/service/metric_service.go b/internal/service/metric_service.go index 42bca83..5c645da 100644 --- a/internal/service/metric_service.go +++ b/internal/service/metric_service.go @@ -358,10 +358,16 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me // 保存每个温度传感器的数据 var tempMetrics []models.TemperatureMetric for _, tempData := range tempDataList { + // 使用 Type 作为 SensorLabel,更易读 + sensorLabel := tempData.Type + if sensorLabel == "" { + sensorLabel = tempData.SensorKey // 降级使用 SensorKey + } + metric := models.TemperatureMetric{ AgentID: agentID, SensorKey: tempData.SensorKey, - SensorLabel: tempData.SensorKey, // protocol 中没有 SensorLabel,使用 SensorKey + SensorLabel: sensorLabel, Temperature: tempData.Temperature, Timestamp: now, } diff --git a/pkg/agent/collector/temperature.go b/pkg/agent/collector/temperature.go index 936a66f..0801808 100644 --- a/pkg/agent/collector/temperature.go +++ b/pkg/agent/collector/temperature.go @@ -1,12 +1,12 @@ package collector import ( - "os/exec" "runtime" - "strconv" "strings" + "unicode/utf8" "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/sensors" ) // TemperatureCollector 温度监控采集器 @@ -19,107 +19,140 @@ func NewTemperatureCollector() *TemperatureCollector { // Collect 采集温度数据(某些系统可能不支持) func (t *TemperatureCollector) Collect() ([]*protocol.TemperatureData, error) { - switch runtime.GOOS { - case "linux": - return t.collectLinux() - case "darwin": - return t.collectDarwin() - default: - // 不支持的系统,返回空数组 - return []*protocol.TemperatureData{}, nil - } -} - -// collectLinux 在 Linux 系统上采集温度数据 -func (t *TemperatureCollector) collectLinux() ([]*protocol.TemperatureData, error) { - // 尝试使用 sensors 命令 - _, err := exec.LookPath("sensors") + // 使用 gopsutil 的 sensors 包采集温度数据 + temps, err := sensors.TemperaturesWithContext(nil) if err != nil { - // sensors 命令不可用,返回空数组 + // 如果获取失败,返回空数组(某些系统可能不支持) return []*protocol.TemperatureData{}, nil } - cmd := exec.Command("sensors", "-A") - output, err := cmd.Output() - if err != nil { - return []*protocol.TemperatureData{}, nil + // 按类型聚合温度数据(同类型取最大值) + typeMaxTemp := make(map[string]*protocol.TemperatureData) + + for _, temp := range temps { + // 跳过温度值为 0 的传感器(可能是未激活或不支持的传感器) + if temp.Temperature == 0 { + continue + } + // https://github.com/shirou/gopsutil/issues/1832 + if runtime.GOOS == "darwin" && !utf8.ValidString(temp.SensorKey) { + continue + } + + // 识别传感器类型 + sensorType := guessSensorType(temp.SensorKey) + if sensorType == "OTHER" { + continue + } + + // 对于同一类型的传感器,取温度最大值 + if existing, ok := typeMaxTemp[sensorType]; !ok || temp.Temperature > existing.Temperature { + typeMaxTemp[sensorType] = &protocol.TemperatureData{ + SensorKey: temp.SensorKey, + Temperature: temp.Temperature, + Type: sensorType, + } + } } + // 将聚合后的数据转换为列表 var tempDataList []*protocol.TemperatureData - lines := strings.Split(string(output), "\n") - currentSensor := "" - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - // 检测传感器名称 - if !strings.Contains(line, ":") { - currentSensor = line - continue - } - - // 解析温度行 - if strings.Contains(line, "°C") { - parts := strings.Split(line, ":") - if len(parts) < 2 { - continue - } - - label := strings.TrimSpace(parts[0]) - valueStr := strings.TrimSpace(parts[1]) - - // 提取温度值 - tempStr := strings.Split(valueStr, "°C")[0] - tempStr = strings.TrimSpace(strings.TrimPrefix(tempStr, "+")) - - temp, err := strconv.ParseFloat(tempStr, 64) - if err != nil { - continue - } - - sensorKey := currentSensor + "_" + label - tempData := &protocol.TemperatureData{ - SensorKey: sensorKey, - Temperature: temp, - } - - tempDataList = append(tempDataList, tempData) - } + for _, tempData := range typeMaxTemp { + tempDataList = append(tempDataList, tempData) } return tempDataList, nil } -// collectDarwin 在 macOS 系统上采集温度数据 -func (t *TemperatureCollector) collectDarwin() ([]*protocol.TemperatureData, error) { - // macOS 的温度监控需要特殊工具,如 osx-cpu-temp - _, err := exec.LookPath("osx-cpu-temp") - if err != nil { - // 工具不可用,返回空数组 - return []*protocol.TemperatureData{}, nil +// guessSensorType 根据操作系统和传感器名称推断类型 +func guessSensorType(key string) string { + keyLower := strings.ToLower(key) + + // ---------------- Linux 常见规则 ---------------- + if runtime.GOOS == "linux" { + // CPU 温度传感器 + if strings.Contains(keyLower, "coretemp") || // Intel Core 系列 + strings.Contains(keyLower, "k10temp") || // AMD Ryzen/EPYC + strings.Contains(keyLower, "zenpower") || // AMD Zen 架构 + strings.Contains(keyLower, "cpu_thermal") || // 通用 CPU 热传感器 + strings.Contains(keyLower, "soc_thermal") || // ARM SoC + strings.Contains(keyLower, "tctl") || // AMD Tctl (控制温度) + strings.Contains(keyLower, "tdie") || // AMD Tdie (芯片温度) + strings.Contains(keyLower, "package id") || // Intel 封装温度 + strings.Contains(keyLower, "core ") { // CPU 核心温度 + return "CPU" + } + + // GPU 温度传感器 + if strings.Contains(keyLower, "amdgpu") || // AMD GPU + strings.Contains(keyLower, "radeon") || // AMD Radeon + strings.Contains(keyLower, "nvidia") || // NVIDIA GPU + strings.Contains(keyLower, "nouveau") || // NVIDIA 开源驱动 + strings.Contains(keyLower, "gpu") { // 通用 GPU + return "GPU" + } + + // 硬盘/SSD 温度传感器 + if strings.Contains(keyLower, "nvme") || // NVMe SSD + strings.Contains(keyLower, "drivetemp") || // 硬盘温度 + strings.Contains(keyLower, "sata") || // SATA 设备 + strings.Contains(keyLower, "scsi") || // SCSI 设备 + strings.Contains(keyLower, "composite") { // NVMe Composite 温度 + return "DISK" + } + + // 主板芯片组温度 + if strings.Contains(keyLower, "pch") || // Platform Controller Hub + strings.Contains(keyLower, "chipset") { + return "CHIPSET" + } + + // ACPI 温度区域 (通常是主板/系统温度) + if strings.Contains(keyLower, "acpitz") || // ACPI Thermal Zone + strings.Contains(keyLower, "thermal_zone") { + return "SYSTEM" + } + + // 电源温度 + if strings.Contains(keyLower, "psu") || + strings.Contains(keyLower, "power supply") { + return "PSU" + } } - cmd := exec.Command("osx-cpu-temp") - output, err := cmd.Output() - if err != nil { - return []*protocol.TemperatureData{}, nil + // ---------------- macOS 常见规则 ---------------- + if runtime.GOOS == "darwin" { + // 电池温度传感器 + if strings.Contains(keyLower, "battery") || strings.Contains(keyLower, "gas gauge") { + return "BATTERY" + } + + // 存储设备温度传感器 + if strings.Contains(keyLower, "nand") || strings.Contains(keyLower, "disk") { + return "DISK" + } + + // GPU 温度传感器 + if strings.Contains(keyLower, "gpu") { + return "GPU" + } + + // PMU tdie* 系列是 CPU 核心温度 (die temperature) + // PMU tdev* 系列是设备/外围温度 (device temperature) + // PMU tcal 是校准温度 + if strings.HasPrefix(keyLower, "pmu") { + if strings.Contains(keyLower, "tdie") { + return "CPU" + } + // tdev 和 tcal 等其他 PMU 传感器归类为 OTHER + return "OTHER" + } + + // 其他包含 die 的可能是 CPU + if strings.Contains(keyLower, "die") || strings.Contains(keyLower, "cpu") { + return "CPU" + } } - // 解析输出,格式类似:60.0°C - outputStr := strings.TrimSpace(string(output)) - tempStr := strings.Split(outputStr, "°C")[0] - temp, err := strconv.ParseFloat(tempStr, 64) - if err != nil { - return []*protocol.TemperatureData{}, nil - } - - tempData := &protocol.TemperatureData{ - SensorKey: "CPU", - Temperature: temp, - } - - return []*protocol.TemperatureData{tempData}, nil + return "OTHER" } diff --git a/web/src/pages/Public/ServerDetail.tsx b/web/src/pages/Public/ServerDetail.tsx index c06ae9e..d429a7e 100644 --- a/web/src/pages/Public/ServerDetail.tsx +++ b/web/src/pages/Public/ServerDetail.tsx @@ -512,6 +512,7 @@ const ServerDetail = () => { const [timeRange, setTimeRange] = useState('15m'); const [selectedInterface, setSelectedInterface] = useState('all'); + const [selectedTempType, setSelectedTempType] = useState('all'); const {agent, latestMetrics, loading} = useAgentOverview(id); const metricsData = useAggregatedMetrics(id, timeRange, selectedInterface); @@ -670,9 +671,9 @@ const ServerDetail = () => { })); }, [metricsData.gpu]); - // Temperature 图表数据(所有传感器的平均温度) + // Temperature 图表数据(按类型分组显示各类型的温度) const temperatureChartData = useMemo(() => { - const aggregated: Record = {}; + const aggregated: Record = {}; metricsData.temperature.forEach((item) => { const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', { @@ -681,20 +682,58 @@ const ServerDetail = () => { }); if (!aggregated[time]) { - aggregated[time] = {time, temperature: 0, count: 0, timestamp: item.timestamp}; + aggregated[time] = {time, timestamp: item.timestamp}; } - aggregated[time].temperature += item.maxTemperature; - aggregated[time].count += 1; + // 使用 sensorLabel 作为数据键(类型名称,如 CPU、GPU 等) + const label = item.sensorLabel || 'Unknown'; + // 对于同一时间点的同一类型,取最大温度 + if (!aggregated[time][label] || item.maxTemperature > aggregated[time][label]) { + aggregated[time][label] = Number(item.maxTemperature.toFixed(2)); + } }); - return Object.values(aggregated).map((item) => ({ - time: item.time, - temperature: Number((item.temperature / item.count).toFixed(2)), - timestamp: item.timestamp, - })); + return Object.values(aggregated); }, [metricsData.temperature]); + // 提取所有唯一的温度类型(用于图表 Line 渲染和下拉选择器) + const temperatureTypes = useMemo(() => { + const types = new Set(); + metricsData.temperature.forEach((item) => { + const label = item.sensorLabel || 'Unknown'; + types.add(label); + }); + return Array.from(types).sort(); + }, [metricsData.temperature]); + + // 根据选中的类型过滤温度数据 + const filteredTemperatureTypes = useMemo(() => { + if (selectedTempType === 'all') { + return temperatureTypes; + } + return temperatureTypes.filter(type => type === selectedTempType); + }, [temperatureTypes, selectedTempType]); + + // 当温度类型列表变化时,如果当前选中的类型不在列表中,重置为 'all' + useEffect(() => { + if (selectedTempType !== 'all' && temperatureTypes.length > 0) { + if (!temperatureTypes.includes(selectedTempType)) { + setSelectedTempType('all'); + } + } + }, [temperatureTypes, selectedTempType]); + + // 温度类型颜色映射 + const temperatureColors: Record = { + 'CPU': '#f97316', // 橙色 + 'GPU': '#8b5cf6', // 紫色 + 'DISK': '#06b6d4', // 青色 + 'BATTERY': '#10b981', // 绿色 + 'CHIPSET': '#f59e0b', // 琥珀色 + 'SYSTEM': '#6366f1', // 靛蓝色 + 'PSU': '#ec4899', // 粉色 + }; + const snapshotCards: SnapshotCardData[] = useMemo(() => { if (!latestMetrics) { return [] as Array<{ @@ -1401,23 +1440,33 @@ const ServerDetail = () => { )} - {temperatureChartData.length > 0 && ( + {temperatureChartData.length > 0 && temperatureTypes.length > 0 && (
-

- - - - 系统温度 -

+
+

+ + + + 系统温度 +

+ {temperatureTypes.length > 1 && ( + + )} +
- - - - - - - + { tickFormatter={(value) => `${value}°C`} /> }/> - - + + {/* 为选中的温度类型渲染线条 */} + {filteredTemperatureTypes.map((type, index) => { + // 使用预定义颜色,如果没有则使用默认颜色 + const color = temperatureColors[type] || `hsl(${(index * 60) % 360}, 70%, 50%)`; + return ( + + ); + })} +
)} @@ -1507,7 +1566,7 @@ const ServerDetail = () => { {latestMetrics?.temperature && latestMetrics.temperature.length > 0 && (
- {latestMetrics.temperature.map((temp) => ( + {latestMetrics.temperature.sort((a, b) => a.sensorKey.localeCompare(b.sensorKey)).map((temp) => (