Compare commits

...
Author SHA1 Message Date
guoyuqi 200ddbfa28 feat: redis sync and image upload 2026-09-30 17:37:33 +08:00
guoyuqi f34418da6d refactor: select bind 2026-09-29 17:11:06 +08:00
guoyuqi 7ff86c0ee3 Merge remote-tracking branch 'origin/dev-v2' into pr@dev-v2@fix-cleanDockerNetwork 2026-09-29 11:09:13 +08:00
guoyuqi 5a58f44548 refactor: search bind 2026-09-29 11:00:30 +08:00
ssongliu d27d6db1ea feat: enhance native GPU NPU and XPU monitoring (#13939) 2026-09-28 18:32:12 +08:00
guoyuqi c75cb5d7ce fix: fix log text 2026-09-28 14:06:40 +08:00
guoyuqi 2207147e7f fix: fix operation log 2026-09-28 11:03:33 +08:00
蘭 d2bb3813d9 fix: enhance SSH login alert by adding local IP addresses to success login whitelist (#13920) 2026-09-24 18:23:59 +08:00
蘭 cac73e6d45 fix: add success alert message for cron jobs and enhance alert handling (#13919) 2026-09-24 18:23:39 +08:00
guoyuqi b5604eab11 fix: fix container list 2026-09-24 15:06:32 +08:00
guoyuqi b498882708 Merge remote-tracking branch 'origin/dev-v2' into pr@dev-v2@fix-cleanDockerNetwork 2026-09-24 14:09:56 +08:00
ssongliu 4712ceaf2c fix: allow read-only firewall APIs in demo mode (#13914) 2026-09-24 14:01:05 +08:00
guoyuqi 381d71663c Merge remote-tracking branch 'origin/pr@dev-v2@fix-cleanDockerNetwork' into pr@dev-v2@fix-cleanDockerNetwork
# Conflicts:
#	agent/app/api/v2/container.go
#	agent/app/dto/container_network_cleanup.go
#	agent/app/service/container_network_cleanup.go
#	agent/utils/docker/network_cleanup.go
2026-09-24 09:43:10 +08:00
guoyuqi f7f4135193 fix: fix clean network 2026-09-24 09:41:59 +08:00
guoyuqi dbf6d9e5fe fix: fix clean network 2026-09-23 17:54:15 +08:00
151 changed files with 5075 additions and 1875 deletions
+18 -3
View File
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
return
}
total, list, err := containerService.Page(req)
total, list, err := containerService.Page(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
// @Success 200 {object} dto.ContainerStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Param containersOnly query boolean false "Only count containers"
// @Router /containers/status [get]
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
data, err := containerService.LoadStatus()
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
if err != nil {
helper.InternalServer(c, err)
return
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
// @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth
// @Security Timestamp
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
// @Router /containers/list/stats [get]
func (b *BaseApi) ContainerListStats(c *gin.Context) {
data, err := containerService.ContainerListStats()
var ids []string
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
}
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
if err != nil {
helper.InternalServer(c, err)
return
@@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
containerService.StreamLogs(c, streamLog)
}
func (b *BaseApi) CleanNetworks(c *gin.Context) {
result, err := containerService.CleanNetworks()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+24 -3
View File
@@ -86,17 +86,38 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
// @Tags Database Redis
// @Summary Install redis-cli
// @Success 200
// @Accept json
// @Param request body dto.RedisCliInstall true "request"
// @Success 200 {object} dto.RedisCliStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/install/cli [post]
func (b *BaseApi) InstallCli(c *gin.Context) {
if err := redisService.InstallCli(); err != nil {
var req dto.RedisCliInstall
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := redisService.InstallCli(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
helper.Success(c)
// @Tags Database Redis
// @Summary Load redis-cli installation status
// @Success 200 {object} dto.RedisCliStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/cli/status [get]
func (b *BaseApi) LoadRedisCliStatus(c *gin.Context) {
data, err := redisService.LoadCliStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Redis
+1 -1
View File
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /runtimes/operate [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
func (b *BaseApi) OperateRuntime(c *gin.Context) {
var req request.RuntimeOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+2
View File
@@ -21,6 +21,7 @@ type AlertBase struct {
}
type PushAlert struct {
Result string `json:"result,omitempty"`
TaskName string `json:"taskName"`
AlertType string `json:"alertType"`
EntryID uint `json:"entryID"`
@@ -53,6 +54,7 @@ type AlertDTO struct {
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
TaskName string `json:"taskName,omitempty"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
@@ -0,0 +1,17 @@
package dto
type NetworkCleanupReport struct {
Deleted []NetworkCleanupItem `json:"deleted"`
Skipped []NetworkCleanupItem `json:"skipped"`
Failed []NetworkCleanupItem `json:"failed"`
}
type NetworkCleanupItem struct {
ID string `json:"id"`
Name string `json:"name"`
Reason string `json:"reason,omitempty"`
}
type NetworkCleanupTask struct {
TaskID string `json:"taskID"`
}
+10 -7
View File
@@ -51,9 +51,10 @@ type CronjobOperate struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
CleanLogConfig
}
@@ -126,7 +127,8 @@ type CronjobInfo struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertCount uint `json:"alertCount"`
AlertTriggerMode string `json:"alertTriggerMode"`
}
type CronjobImport struct {
@@ -169,9 +171,10 @@ type CronjobTrans struct {
SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
}
type TransHelper struct {
Name string `json:"name"`
+11
View File
@@ -22,6 +22,17 @@ type DBBaseInfo struct {
Port int64 `json:"port"`
}
type RedisCliInstall struct {
TaskID string `json:"taskID" validate:"omitempty,uuid"`
}
type RedisCliStatus struct {
Installed bool `json:"installed"`
TaskID string `json:"taskID"`
Status string `json:"status"`
ErrorMsg string `json:"errorMsg"`
}
// mysql
type MysqlDBSearch struct {
PageInfo
+44 -11
View File
@@ -44,6 +44,8 @@ type MonitorGPUOptions struct {
Options []string `json:"options"`
}
type GPUChartHide struct {
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
Type string `json:"type"`
Process bool `json:"process"`
@@ -55,23 +57,54 @@ type GPUChartHide struct {
Speed bool `json:"speed"`
}
type MonitorGPUSearch struct {
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type MonitorGPUData struct {
Date []time.Time `json:"date"`
GPUValue []float64 `json:"gpuValue"`
TemperatureValue []float64 `json:"temperatureValue"`
PowerTotal []float64 `json:"powerTotal"`
PowerUsed []float64 `json:"powerUsed"`
PowerPercent []float64 `json:"powerPercent"`
MemoryTotal []float64 `json:"memoryTotal"`
MemoryUsed []float64 `json:"memoryUsed"`
MemoryPercent []float64 `json:"memoryPercent"`
SpeedValue []int `json:"speedValue"`
MemoryActivity []*float64 `json:"memoryActivity"`
EncoderUtil []*float64 `json:"encoderUtil"`
DecoderUtil []*float64 `json:"decoderUtil"`
JPEGUtil []*float64 `json:"jpegUtil"`
OFAUtil []*float64 `json:"ofaUtil"`
MediaUtil []*float64 `json:"mediaUtil"`
ComputeUtil []*float64 `json:"computeUtil"`
CopyUtil []*float64 `json:"copyUtil"`
HotspotTemperature []*float64 `json:"hotspotTemperature"`
FanRPM []*float64 `json:"fanRPM"`
AICPUUtil []*float64 `json:"aiCPUUtil"`
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
DDRUsed []*float64 `json:"ddrUsed"`
DDRTotal []*float64 `json:"ddrTotal"`
HBMUsed []*float64 `json:"hbmUsed"`
HBMTotal []*float64 `json:"hbmTotal"`
DDRBandwidth []*float64 `json:"ddrBandwidth"`
HBMBandwidth []*float64 `json:"hbmBandwidth"`
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
MediaFrequency []*float64 `json:"mediaFrequency"`
HugepagesUsed []*float64 `json:"hugepagesUsed"`
HugepagesTotal []*float64 `json:"hugepagesTotal"`
ProcessCount []int `json:"processCount"`
BucketSeconds int64 `json:"bucketSeconds"`
SampleCount int64 `json:"sampleCount"`
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
FrequencyValue []*float64 `json:"frequencyValue"`
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
Date []time.Time `json:"date"`
GPUValue []*float64 `json:"gpuValue"`
TemperatureValue []*float64 `json:"temperatureValue"`
PowerTotal []*float64 `json:"powerTotal"`
PowerUsed []*float64 `json:"powerUsed"`
PowerPercent []*float64 `json:"powerPercent"`
MemoryTotal []*float64 `json:"memoryTotal"`
MemoryUsed []*float64 `json:"memoryUsed"`
MemoryPercent []*float64 `json:"memoryPercent"`
SpeedValue []*float64 `json:"speedValue"`
ProcessCount []*float64 `json:"processCount"`
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
}
+40 -9
View File
@@ -33,14 +33,45 @@ type MonitorNetwork struct {
}
type MonitorGPU struct {
MemoryUtil *float64 `json:"memoryUtil"`
MemoryActivity *float64 `json:"memoryActivity"`
EncoderUtil *float64 `json:"encoderUtil"`
DecoderUtil *float64 `json:"decoderUtil"`
JPEGUtil *float64 `json:"jpegUtil"`
OFAUtil *float64 `json:"ofaUtil"`
MediaUtil *float64 `json:"mediaUtil"`
ComputeUtil *float64 `json:"computeUtil"`
CopyUtil *float64 `json:"copyUtil"`
HotspotTemperature *float64 `json:"hotspotTemperature"`
FanRPM *float64 `json:"fanRPM"`
AICPUUtil *float64 `json:"aiCPUUtil"`
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
DDRUsed *float64 `json:"ddrUsed"`
DDRTotal *float64 `json:"ddrTotal"`
HBMUsed *float64 `json:"hbmUsed"`
HBMTotal *float64 `json:"hbmTotal"`
DDRBandwidth *float64 `json:"ddrBandwidth"`
HBMBandwidth *float64 `json:"hbmBandwidth"`
MemoryBandwidth *float64 `json:"memoryBandwidth"`
MediaFrequency *float64 `json:"mediaFrequency"`
HugepagesUsed *float64 `json:"hugepagesUsed"`
HugepagesTotal *float64 `json:"hugepagesTotal"`
MemoryTemperature *float64 `json:"memoryTemperature"`
DeviceID string `json:"deviceID"`
DeviceType string `json:"deviceType"`
ProcessStatus string `json:"processStatus"`
Frequency *float64 `json:"frequency"`
MemoryFrequency *float64 `json:"memoryFrequency"`
IntervalSeconds int `json:"intervalSeconds"`
BaseModel
ProductName string `json:"productName"`
GPUUtil float64 `json:"gpuUtil"`
Temperature float64 `json:"temperature"`
PowerDraw float64 `json:"powerDraw"`
MaxPowerLimit float64 `json:"maxPowerLimit"`
MemUsed float64 `json:"memUsed"`
MemTotal float64 `json:"memTotal"`
FanSpeed int `json:"fanSpeed"`
Processes string `json:"processes"`
ProductName string `json:"productName"`
GPUUtil *float64 `json:"gpuUtil"`
Temperature *float64 `json:"temperature"`
PowerDraw *float64 `json:"powerDraw"`
MaxPowerLimit *float64 `json:"maxPowerLimit"`
MemUsed *float64 `json:"memUsed"`
MemTotal *float64 `json:"memTotal"`
FanSpeed *float64 `json:"fanSpeed"`
Processes string `json:"processes"`
}
+79
View File
@@ -1,6 +1,8 @@
package repo
import (
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -10,9 +12,20 @@ import (
type MonitorRepo struct{}
type GPUHistoryPoint struct {
model.MonitorGPU
Bucket int64
PowerPercent *float64
MemoryPercent *float64
ProcessCount *float64
}
type IMonitorRepo interface {
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
CountGPU(opts ...DBOption) (int64, error)
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
GetGPUDevices() ([]model.MonitorGPU, error)
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
@@ -26,6 +39,7 @@ type IMonitorRepo interface {
DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
}
func NewIMonitorRepo() IMonitorRepo {
@@ -102,3 +116,68 @@ func (s *MonitorRepo) WithByProductName(name string) DBOption {
return g.Where("product_name = ?", name)
}
}
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
return data, err
}
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
return func(db *gorm.DB) *gorm.DB {
if deviceID != "" {
return db.Where("device_id = ?", deviceID)
}
db = db.Where("product_name = ?", name)
if legacy {
db = db.Where("device_id IS NULL OR device_id = ''")
}
return db
}
}
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
for _, opt := range opts {
db = opt(db)
}
var count int64
err := db.Count(&count).Error
return count, err
}
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
for _, opt := range opts {
db = opt(db)
}
expressions := []string{
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
}
aliases := []string{"power_percent", "memory_percent", "process_count"}
columns := []string{"*"}
if bucketSeconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
columns = append(columns, operation+"("+column+") AS "+column)
}
for i := range expressions {
expressions[i] = operation + "(" + expressions[i] + ")"
}
db = db.Group("bucket").Order("bucket ASC")
} else {
db = db.Order("created_at ASC, id ASC")
}
for i, expression := range expressions {
columns = append(columns, expression+" AS "+aliases[i])
}
var data []GPUHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
return data, err
}
+98
View File
@@ -17,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -109,7 +110,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
return 0, nil, err
}
cronjobProjects := make(map[string]uint)
var cronjobIDs []uint
for _, item := range alerts {
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
continue
}
if _, exists := cronjobProjects[item.Project]; exists {
continue
}
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
if parseErr != nil || id == 0 {
continue
}
cronjobProjects[item.Project] = uint(id)
cronjobIDs = append(cronjobIDs, uint(id))
}
cronjobsByID := make(map[uint]model.Cronjob)
if len(cronjobIDs) > 0 {
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
if err != nil {
return 0, nil, err
}
for _, cronjob := range cronjobs {
cronjobsByID[cronjob.ID] = cronjob
}
}
for _, item := range alerts {
var taskName string
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
taskName = cronjob.Name
}
result = append(result, dto.AlertDTO{
ID: item.ID,
@@ -119,6 +151,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
Method: item.Method,
Title: item.Title,
Project: item.Project,
TaskName: taskName,
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
@@ -190,6 +223,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
return err
}
} else {
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
if err != nil {
return err
}
create.AdvancedParams = advanced
if create.Status != constant.AlertDisable {
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
return err
}
}
alertInfo.Status = constant.AlertEnable
if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
@@ -207,11 +250,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
previous, err := alertRepo.Get(repo.WithByID(req.ID))
if err != nil {
return err
}
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
if err != nil {
return err
}
}
methodTypes, err := a.validateAlertMethodReferences(req.Method)
if err != nil {
return err
}
if req.Status != constant.AlertDisable {
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
return err
}
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
@@ -278,6 +334,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
return err
}
if status == constant.AlertEnable {
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
return err
}
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
@@ -1021,6 +1080,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
alertRepo.WithByType(updateAlert.Type),
alertRepo.WithByProject(updateAlert.Project),
)
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
if err != nil {
return err
}
updateAlert.AdvancedParams = advanced
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
upMap["advanced_params"] = advanced
}
if newStatus == constant.AlertEnable {
method := updateAlert.Method
if method == "" {
method = alertInfo.Method
}
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
return err
}
}
if alertInfo.ID > 0 {
shouldUpdate := false
@@ -1034,6 +1110,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
shouldUpdate = true
}
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
shouldUpdate = true
}
if shouldUpdate {
if err := alertRepo.Update(
@@ -1055,3 +1134,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
return nil
}
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
if alertUtil.GetCronJobType(alertType) != "cronJob" {
return incoming, nil
}
return alertUtil.MergeCronJobAlertParams(previous, incoming)
}
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
if alertUtil.GetCronJobType(alertType) != "cronJob" {
return nil
}
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
if err != nil || mode != alertUtil.CronJobAlertSuccess {
return err
}
_, err = a.validateAlertMethodReferences(method)
return err
}
+18 -1
View File
@@ -530,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
if err != nil {
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
}
interfaceAddrs, err := net.InterfaceAddrs()
if err != nil {
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
}
count, records := summarizeSSHLoginHistories(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
)
isAlert := count >= int(alert.Count)
if isAlert {
@@ -570,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
}
}
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
whitelist = append(whitelist, "127.0.0.0/8", "::1")
for _, addr := range interfaceAddrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
continue
}
whitelist = append(whitelist, ipNet.IP.String())
}
return whitelist
}
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
filtered := make([]model.LoginLog, 0, len(records))
for _, record := range records {
+95 -35
View File
@@ -58,10 +58,10 @@ type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions
ListByImage(imageName string) []dto.ContainerOptions
LoadStatus() (dto.ContainerStatus, error)
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
@@ -80,7 +80,7 @@ type IContainerService interface {
ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
ContainerListStats() ([]dto.ContainerListStats, error)
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error
@@ -92,6 +92,7 @@ type IContainerService interface {
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
CleanNetworks() (*dto.NetworkCleanupTask, error)
CreateNetwork(req dto.NetworkCreate) error
DeleteVolume(req dto.BatchDelete) error
CreateVolume(req dto.VolumeCreate) error
@@ -112,7 +113,9 @@ func NewIContainerService() IContainerService {
return &ContainerService{}
}
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
@@ -123,7 +126,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
options.Filters = filters.NewArgs()
options.Filters.Add("label", req.Filters)
}
containers, err := client.ContainerList(context.Background(), options)
containers, err := client.ContainerList(ctx, options)
if err != nil {
return 0, nil, err
}
@@ -206,27 +209,32 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
return options
}
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
var data dto.ContainerStatus
client, err := docker.NewDockerClient()
if err != nil {
return data, err
}
defer client.Close()
c := context.Background()
images, _ := client.ImageList(c, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(c, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(client)
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if !containersOnly {
images, _ := client.ImageList(c, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(c, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(c, client)
}
containers, err := client.ContainerList(c, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers)
for _, item := range containers {
switch item.State {
@@ -292,27 +300,67 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
}
return data, nil
}
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
options := container.ListOptions{All: true}
if ids != nil {
if len(ids) == 0 {
return []dto.ContainerListStats{}, nil
}
options.Filters = filters.NewArgs()
for _, id := range ids {
options.Filters.Add("id", id)
}
}
list, err := client.ContainerList(ctx, options)
if err != nil {
return nil, err
}
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
return loadCpuAndMem(ctx, client, id)
}), nil
}
// A fixed worker pool bounds Docker stats requests, including for legacy callers
// that request all containers. Stopped containers do not need a stats sample.
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup
wg.Add(len(list))
for i := 0; i < len(list); i++ {
go func(index int, item container.Summary) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(i, list[i])
for i, item := range list {
datas[i].ContainerID = item.ID
}
jobs := make(chan int)
var wg sync.WaitGroup
for worker := 0; worker < min(8, len(list)); worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for index := range jobs {
if ctx.Err() != nil || list[index].State != "running" {
continue
}
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
datas[index] = load(sampleCtx, list[index].ID)
cancel()
}
}()
}
dispatch:
for index := range list {
select {
case <-ctx.Done():
break dispatch
case jobs <- index:
}
}
close(jobs)
wg.Wait()
return datas, nil
return datas
}
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
@@ -484,6 +532,10 @@ func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) {
}
func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bool) error {
return u.containerCreate(req, inThread, "")
}
func (u *ContainerService) containerCreate(req dto.ContainerOperate, inThread bool, taskName string) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
@@ -497,7 +549,10 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
return buserr.New("ErrContainerName")
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if taskName == "" {
taskName = task.GetTaskName(req.Name, task.TaskCreate, task.TaskScopeContainer)
}
taskItem, err := task.NewTask(taskName, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
unlock()
_ = client.Close()
@@ -558,6 +613,11 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
}, nil)
if inThread {
if err := taskItem.Prepare(); err != nil {
unlock()
_ = client.Close()
return err
}
go func() {
defer unlock()
defer client.Close()
@@ -1715,11 +1775,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
return selected
}
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
data := dto.ContainerListStats{
ContainerID: containerItem,
}
res, err := client.ContainerStats(context.Background(), containerItem, false)
res, err := client.ContainerStats(ctx, containerItem, false)
if err != nil {
return data
}
@@ -1942,11 +2002,11 @@ func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports)
}
func loadComposeCount(client *client.Client) int {
func loadComposeCount(ctx context.Context, client *client.Client) int {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
list, err := client.ContainerList(ctx, options)
if err != nil {
return 0
}
@@ -0,0 +1,71 @@
package service
import (
"fmt"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
)
var networkCleanupMu sync.Mutex
func (u *ContainerService) CleanNetworks() (*dto.NetworkCleanupTask, error) {
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey("Network"), task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
return nil, err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
networkCleanupMu.Lock()
defer networkCleanupMu.Unlock()
if err := t.TaskCtx.Err(); err != nil {
return err
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
return executeNetworkCleanup(t, cli)
}, nil)
go func() {
if err := taskItem.Execute(); err != nil {
global.LOG.Errorf("network cleanup task %s failed: %v", taskItem.TaskID, err)
}
}()
return &dto.NetworkCleanupTask{TaskID: taskItem.TaskID}, nil
}
func executeNetworkCleanup(t *task.Task, cli docker.NetworkCleanupClient) error {
t.Log(i18n.GetMsgByKey("PruneStart"))
report, err := docker.CleanUnusedNetworks(t.TaskCtx, cli, func(status string, item dto.NetworkCleanupItem) {
key := "NetworkCleanupDeleted"
if status == "skipped" || status == "failed" {
key = map[string]string{
"protected": "NetworkCleanupProtected",
"container_connected": "NetworkCleanupConnected",
"network_in_use": "NetworkCleanupConnected",
"unsupported_network": "NetworkCleanupUnsupported",
"already_removed": "NetworkCleanupGone",
"inspect_failed": "NetworkCleanupInspectFailed",
"remove_failed": "NetworkCleanupRemoveFailed",
}[item.Reason]
}
t.Log(i18n.GetMsgWithMap(key, map[string]interface{}{"name": item.Name, "id": item.ID}))
})
if report != nil {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": len(report.Deleted), "skipped": len(report.Skipped), "failed": len(report.Failed),
}))
}
if err != nil {
return err
}
if len(report.Failed) > 0 {
return fmt.Errorf("%s", i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
@@ -0,0 +1,113 @@
package service
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type networkTaskClient struct{ fail bool }
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
}
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
return nil, nil
}
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
n := network.Inspect{}
if id == "connected" {
n.Containers = map[string]network.EndpointResource{"container-id": {}}
}
return n, nil
}
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
if id == "race" {
return errdefs.Conflict(errors.New("has active endpoints"))
}
if id != "unused" {
return errors.New("unexpected removal")
}
if f.fail {
return errors.New("remove failed")
}
return nil
}
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
dir := t.TempDir()
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
defer sqlDB.Close()
if err := db.AutoMigrate(&model.Task{}); err != nil {
t.Fatal(err)
}
global.DB = nil
global.TaskDB = db
global.Dir.TaskDir = dir
i18n.Init()
for _, fail := range []bool{false, true} {
name := "success"
wantStatus := constant.StatusSuccess
if fail {
name = "partial failure"
wantStatus = constant.StatusFailed
}
t.Run(name, func(t *testing.T) {
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
t.Fatal(err)
}
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
err = item.Execute()
if (err != nil) != fail {
t.Fatalf("unexpected execution error: %v", err)
}
var saved model.Task
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
t.Fatal(err)
}
if saved.Status != wantStatus {
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
}
content, err := os.ReadFile(saved.LogFile)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q in log: %s", want, content)
}
}
want := "Deleted network [free]"
if fail {
want = "Failed to remove network [free]"
}
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q", want)
}
})
}
}
+92 -26
View File
@@ -16,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
EntryID: cronjob.ID,
}
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.SendCount != 0 {
item.AlertCount = alertInfo.SendCount
} else {
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
AlertType: cronjob.Type,
EntryID: cronjob.ID,
}
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
item.AlertMethod = alertInfo.Method
if alertInfo.SendCount != 0 {
item.AlertTitle = alertInfo.Title
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
} else {
item.AlertCount = 0
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
}
}
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
if alertInfo.SendCount != 0 {
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
} else {
item.AlertCount = 0
}
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
}
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
for _, item := range req {
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
if err != nil {
return err
}
if item.AlertCount != 0 {
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
return err
}
}
}
for _, item := range req {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 {
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
} else {
cronjob.Status = constant.StatusDisable
}
_ = cronjobRepo.Create(&cronjob)
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
if item.AlertTitle != "" && item.AlertMethod != "" {
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
status := constant.AlertEnable
if item.AlertCount == 0 {
status = constant.AlertDisable
}
createAlert := dto.AlertCreate{
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: status,
AdvancedParams: advanced,
}
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
return err
}
_ = NewIAlertService().CreateAlert(createAlert, operator)
}
}
return nil
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
}
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
if err != nil {
return err
}
if req.AlertCount != 0 {
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
return err
}
}
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
if cronjob.ID != 0 {
return buserr.New("ErrRecordExist")
@@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
}
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
createAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
AdvancedParams: advanced,
}
err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil {
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
}
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
if err != nil {
return err
}
var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != nil {
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
if err != nil {
return buserr.New("ErrRecordNotFound")
}
if req.AlertCount != 0 {
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
if err != nil {
return err
}
method := req.AlertMethod
if method == "" {
method = previous.Method
}
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
return err
}
}
upMap := make(map[string]interface{})
cronjob.EntryIDs = cronModel.EntryIDs
cronjob.Type = cronModel.Type
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return err
}
updateAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
AdvancedParams: advanced,
}
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil {
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return nil
}
func cronJobAlertAdvancedParams(mode string) (string, error) {
if mode == "" {
return "", nil
}
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
if err != nil {
return "", err
}
return alertUtil.MergeCronJobAlertParams("", string(data))
}
func (u *CronjobService) UpdateStatus(id uint, status string) error {
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
if cronjob.ID == 0 {
+1
View File
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
return nil
}, nil)
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
}
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
+35 -6
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -23,6 +24,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
_ = taskRepo.Save(context.Background(), taskItem.Task)
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
}()
return
}
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
go func() {
if err := taskItem.Execute(); err != nil {
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(taskItem.ID) == 0 {
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(storedTask.ID) == 0 {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
} else {
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
}
}()
}
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
}
func handleCronJobAlert(cronjob *model.Cronjob) {
const cronJobSkippedStep = "cronjob-skipped"
func cronJobAlertResult(taskItem *task.Task, err error) string {
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
return ""
}
if err != nil {
return alertUtil.CronJobAlertFailed
}
if taskItem.Task.Status != constant.StatusSuccess {
return ""
}
for _, subTask := range taskItem.SubTasks {
if subTask.StepAlias != cronJobSkippedStep {
return alertUtil.CronJobAlertSuccess
}
}
return ""
}
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
if result == "" {
return
}
pushAlert := dto.PushAlert{
Result: result,
TaskName: cronjob.Name,
AlertType: cronjob.Type,
EntryID: cronjob.ID,
+8 -1
View File
@@ -588,6 +588,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
xpuData []dto.XPUInfo
)
for _, device := range snapshot.Devices {
if device.ParentID != "" {
continue
}
switch device.Kind {
case accelerator.KindGPU:
if device.GPU == nil {
@@ -597,7 +600,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
if err := copier.Copy(&dataItem, device.GPU); err != nil {
continue
}
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
dataItem.MaxPowerLimit = device.GPU.PowerLimit
dataItem.PowerUsage = dataItem.PowerDraw
if dataItem.MaxPowerLimit != "" {
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
}
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
gpuData = append(gpuData, dataItem)
case accelerator.KindNPU:
+58 -4
View File
@@ -8,6 +8,12 @@ import (
"os"
"os/exec"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -23,6 +29,11 @@ import (
type RedisService struct{}
const redisCliTaskName = "RedisCliEnable"
// The CLI container is shared by all remote Redis databases on this node.
var redisCliInstallMutex sync.Mutex
type IRedisService interface {
UpdateConf(req dto.RedisConfUpdate) error
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
@@ -33,7 +44,8 @@ type IRedisService interface {
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool
InstallCli() error
InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error)
LoadCliStatus() (*dto.RedisCliStatus, error)
}
func NewIRedisService() IRedisService {
@@ -71,20 +83,62 @@ func (u *RedisService) CheckHasCli() bool {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
if len(item.Names) > 0 && strings.TrimPrefix(item.Names[0], "/") == "1Panel-redis-cli-tools" {
return true
}
}
return false
}
func (u *RedisService) InstallCli() error {
func (u *RedisService) LoadCliStatus() (*dto.RedisCliStatus, error) {
result := &dto.RedisCliStatus{}
latest, err := taskRepo.GetFirst(repo.WithByName(redisCliTaskName), repo.WithByType(task.TaskScopeContainer), repo.WithOrderDesc("created_at"))
if errors.Is(err, gorm.ErrRecordNotFound) {
result.Installed = u.CheckHasCli()
return result, nil
}
if err != nil {
return nil, err
}
result.TaskID = latest.ID
result.Status = latest.Status
result.ErrorMsg = latest.ErrorMsg
result.Installed = u.CheckHasCli()
return result, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (*dto.RedisCliStatus, error) {
if !redisCliInstallMutex.TryLock() {
return nil, buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
status, err := u.LoadCliStatus()
if err != nil {
return nil, err
}
if status.Status == constant.StatusExecuting || status.Installed {
return status, nil
}
if req.TaskID == "" {
req.TaskID = uuid.NewString()
}
// Never reuse an existing task ID: doing so would truncate its log.
if _, err := taskRepo.GetFirst(taskRepo.WithByID(req.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return nil, err
}
return nil, buserr.New("TaskIsExecuting")
}
item := dto.ContainerOperate{
TaskID: req.TaskID,
Name: "1Panel-redis-cli-tools",
Image: "redis:7.4.4",
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
}
return NewIContainerService().ContainerCreate(item, false)
if err := (&ContainerService{}).containerCreate(item, true, redisCliTaskName); err != nil {
return nil, err
}
return &dto.RedisCliStatus{TaskID: req.TaskID, Status: constant.StatusExecuting}, nil
}
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
+8 -16
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
@@ -325,18 +324,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
}
go func() {
client, err := docker.NewDockerClient()
if err != nil {
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()
for _, itemPath := range req.Paths {
currentPath := itemPath
itemName := path.Base(currentPath)
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
file, err := os.Open(currentPath)
if err != nil {
return err
@@ -347,14 +344,9 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return err
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
return consumeImageLoadResponse(res.Body, func(message string) {
taskItem.Log(message)
})
}, nil)
}
_ = taskItem.Execute()
+37
View File
@@ -0,0 +1,37 @@
package service
import (
"encoding/json"
"errors"
"io"
"strings"
)
// Docker may report load failures in a successful HTTP response's JSON stream.
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
decoder := json.NewDecoder(reader)
for {
var message struct {
Stream string `json:"stream"`
Error string `json:"error"`
ErrorDetail struct {
Message string `json:"message"`
} `json:"errorDetail"`
}
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != "" {
return errors.New(message.Error)
}
if message.ErrorDetail.Message != "" {
return errors.New(message.ErrorDetail.Message)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}
+216 -104
View File
@@ -128,115 +128,135 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
exist, client := accelerator.New()
if !exist {
return data
}
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
return data
}
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
}
return loadGPUOptions(snapshot)
}
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
hasGPUOrNPU := false
hasXPU := false
for _, item := range snapshot.Devices {
if item.Kind == accelerator.KindXPU {
hasXPU = true
seen := make(map[string]bool)
if exist, client := accelerator.New(); exist {
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
hasGPUOrNPU = true
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
}
}
switch {
case hasGPUOrNPU && hasXPU:
data.GPUType = "mixed"
case hasXPU:
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
devices, err := monitorRepo.GetGPUDevices()
if err != nil {
global.LOG.Warnf("Load accelerator history options failed: %v", err)
return data
}
sort.Slice(snapshot.Devices, func(i, j int) bool {
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
for _, device := range devices {
key := device.DeviceID
if key == "" {
key = "legacy:" + device.ProductName
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
if seen[key] {
continue
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
optionType := "gpu"
if item.Kind == accelerator.KindXPU {
optionType = "xpu"
}
chartHide := dto.GPUChartHide{
ProductName: item.Label,
Type: optionType,
GPU: !item.Capabilities.Utilization,
Memory: !item.Capabilities.Memory,
Power: !item.Capabilities.Power,
PowerLimit: !item.Capabilities.PowerLimit,
Temperature: !item.Capabilities.Temperature,
Speed: !item.Capabilities.FanSpeed,
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
seen[key] = true
data.ChartHide = append(data.ChartHide, dto.GPUChartHide{DeviceID: device.DeviceID, ProductName: device.ProductName, Type: device.DeviceType, Legacy: device.DeviceID == ""})
data.Options = append(data.Options, device.ProductName)
}
return data
}
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
req.StartTime = req.StartTime.In(loc)
req.EndTime = req.EndTime.In(loc)
var data dto.MonitorGPUData
gpuList, err := monitorRepo.GetGPU(repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByProductName(req.ProductName))
if req.StartTime.IsZero() || req.EndTime.IsZero() || !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid GPU history time range")
}
if req.DeviceID == "" && req.ProductName == "" {
return data, fmt.Errorf("GPU history requires a device")
}
if req.Aggregation != "" && req.Aggregation != "avg" && req.Aggregation != "max" {
return data, fmt.Errorf("invalid GPU history aggregation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
for _, gpu := range gpuList {
data.Date = append(data.Date, gpu.CreatedAt)
data.GPUValue = append(data.GPUValue, gpu.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature)
data.PowerUsed = append(data.PowerUsed, gpu.PowerDraw)
data.PowerTotal = append(data.PowerTotal, gpu.MaxPowerLimit)
if gpu.MaxPowerLimit != 0 {
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100)
} else {
data.PowerPercent = append(data.PowerPercent, float64(0))
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
opts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByGPUDevice(req.DeviceID, req.ProductName, req.Legacy)}
data.SampleCount, err = monitorRepo.CountGPU(opts...)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
data.BucketSeconds = (seconds + 599) / 600
}
points, err := monitorRepo.GetGPUHistory(req.StartTime, data.BucketSeconds, req.Aggregation, opts...)
if err != nil {
return data, err
}
samples := make([]repo.GPUHistoryPoint, 0, len(points))
if data.BucketSeconds > 0 {
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
point := repo.GPUHistoryPoint{}
if next < len(points) && points[next].Bucket == bucket {
point = points[next]
next++
}
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
samples = append(samples, point)
}
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed)
if gpu.MemTotal != 0 {
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100)
} else {
data.MemoryPercent = append(data.MemoryPercent, float64(0))
} else {
for i, point := range points {
if i > 0 && points[i-1].IntervalSeconds > 0 {
interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
}
}
samples = append(samples, point)
}
var process []dto.GPUProcess
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil {
data.ProcessCount = append(data.ProcessCount, len(process))
data.GPUProcesses = append(data.GPUProcesses, process)
} else {
data.ProcessCount = append(data.ProcessCount, 0)
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{})
}
for _, point := range samples {
data.Date = append(data.Date, point.CreatedAt)
data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
data.JPEGUtil = append(data.JPEGUtil, point.JPEGUtil)
data.OFAUtil = append(data.OFAUtil, point.OFAUtil)
data.MediaUtil = append(data.MediaUtil, point.MediaUtil)
data.ComputeUtil = append(data.ComputeUtil, point.ComputeUtil)
data.CopyUtil = append(data.CopyUtil, point.CopyUtil)
data.HotspotTemperature = append(data.HotspotTemperature, point.HotspotTemperature)
data.FanRPM = append(data.FanRPM, point.FanRPM)
data.AICPUUtil = append(data.AICPUUtil, point.AICPUUtil)
data.CtrlCPUUtil = append(data.CtrlCPUUtil, point.CtrlCPUUtil)
data.DDRUsed = append(data.DDRUsed, point.DDRUsed)
data.DDRTotal = append(data.DDRTotal, point.DDRTotal)
data.HBMUsed = append(data.HBMUsed, point.HBMUsed)
data.HBMTotal = append(data.HBMTotal, point.HBMTotal)
data.DDRBandwidth = append(data.DDRBandwidth, point.DDRBandwidth)
data.HBMBandwidth = append(data.HBMBandwidth, point.HBMBandwidth)
data.MemoryBandwidth = append(data.MemoryBandwidth, point.MemoryBandwidth)
data.MediaFrequency = append(data.MediaFrequency, point.MediaFrequency)
data.HugepagesUsed = append(data.HugepagesUsed, point.HugepagesUsed)
data.HugepagesTotal = append(data.HugepagesTotal, point.HugepagesTotal)
data.GPUValue = append(data.GPUValue, point.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, point.Temperature)
data.MemoryTemperatureValue = append(data.MemoryTemperatureValue, point.MemoryTemperature)
data.PowerUsed = append(data.PowerUsed, point.PowerDraw)
data.PowerTotal = append(data.PowerTotal, point.MaxPowerLimit)
data.PowerPercent = append(data.PowerPercent, point.PowerPercent)
data.MemoryPercent = append(data.MemoryPercent, point.MemoryPercent)
data.MemoryTotal = append(data.MemoryTotal, point.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, point.MemUsed)
data.SpeedValue = append(data.SpeedValue, point.FanSpeed)
data.FrequencyValue = append(data.FrequencyValue, point.Frequency)
data.MemoryFrequencyValue = append(data.MemoryFrequencyValue, point.MemoryFrequency)
data.ProcessCount = append(data.ProcessCount, point.ProcessCount)
var processes []dto.GPUProcess
if data.BucketSeconds == 0 && point.ProcessCount != nil {
_ = json.Unmarshal([]byte(point.Processes), &processes)
}
data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed)
data.GPUProcesses = append(data.GPUProcesses, processes)
}
return data, nil
}
@@ -307,7 +327,6 @@ func (m *MonitorService) CleanData() error {
}
func (m *MonitorService) Run() {
saveAcceleratorDataToDB()
var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 {
@@ -343,6 +362,7 @@ func (m *MonitorService) Run() {
m.loadDiskIO()
m.loadNetIO()
m.saveGPUData()
MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays"))
if err != nil {
@@ -602,7 +622,62 @@ func StartMonitor(removeBefore bool, interval string) error {
return nil
}
func saveAcceleratorDataToDB() {
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
hasGPUOrNPU := false
hasXPU := false
for _, item := range snapshot.Devices {
if item.Kind == accelerator.KindXPU {
hasXPU = true
} else {
hasGPUOrNPU = true
}
}
switch {
case hasGPUOrNPU && hasXPU:
data.GPUType = "mixed"
case hasXPU:
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
}
sort.Slice(snapshot.Devices, func(i, j int) bool {
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
chartHide := dto.GPUChartHide{
DeviceID: item.ID,
ProductName: item.Label,
Type: string(item.Kind),
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
}
return data
}
func (m *MonitorService) saveGPUData() {
status, err := settingRepo.GetValueByKey("MonitorStatus")
if err != nil {
global.LOG.Errorf("load monitor status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
exist, client := accelerator.New()
if !exist {
return
@@ -615,27 +690,64 @@ func saveAcceleratorDataToDB() {
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
}
intervalSeconds := 0
if setting, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval")); err == nil {
intervalSeconds, _ = strconv.Atoi(setting.Value)
}
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
for _, device := range snapshot.Devices {
list = append(list, newMonitorGPU(device))
item := newMonitorGPU(device)
item.CreatedAt = snapshot.Info.CollectedAt
item.IntervalSeconds = intervalSeconds
list = append(list, item)
}
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
if err := monitorRepo.BatchCreateMonitorGPU(list); err != nil {
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
}
}
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
item := model.MonitorGPU{
ProductName: device.Label,
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
Temperature: device.Metrics.Temperature.ValueOrZero(),
PowerDraw: device.Metrics.Power.ValueOrZero(),
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
MemoryUtil: device.Metrics.MemoryUtil.Value,
MemoryActivity: device.Metrics.MemoryActivity.Value,
EncoderUtil: device.Metrics.EncoderUtil.Value,
DecoderUtil: device.Metrics.DecoderUtil.Value,
JPEGUtil: device.Metrics.JPEGUtil.Value,
OFAUtil: device.Metrics.OFAUtil.Value,
MediaUtil: device.Metrics.MediaUtil.Value,
ComputeUtil: device.Metrics.ComputeUtil.Value,
CopyUtil: device.Metrics.CopyUtil.Value,
HotspotTemperature: device.Metrics.HotspotTemperature.Value,
FanRPM: device.Metrics.FanRPM.Value,
AICPUUtil: device.Metrics.AICPUUtil.Value,
CtrlCPUUtil: device.Metrics.CtrlCPUUtil.Value,
DDRUsed: device.Metrics.DDRUsed.Value,
DDRTotal: device.Metrics.DDRTotal.Value,
HBMUsed: device.Metrics.HBMUsed.Value,
HBMTotal: device.Metrics.HBMTotal.Value,
DDRBandwidth: device.Metrics.DDRBandwidth.Value,
HBMBandwidth: device.Metrics.HBMBandwidth.Value,
MemoryBandwidth: device.Metrics.MemoryBandwidth.Value,
MediaFrequency: device.Metrics.MediaFrequency.Value,
HugepagesUsed: device.Metrics.HugepagesUsed.Value,
HugepagesTotal: device.Metrics.HugepagesTotal.Value,
ProductName: device.Label,
DeviceID: device.ID,
DeviceType: string(device.Kind),
ProcessStatus: device.ProcessStatus,
Frequency: device.Metrics.Frequency.Value,
MemoryFrequency: device.Metrics.MemoryFrequency.Value,
MemoryTemperature: device.Metrics.MemoryTemperature.Value,
GPUUtil: device.Metrics.Utilization.Value,
Temperature: device.Metrics.Temperature.Value,
PowerDraw: device.Metrics.Power.Value,
MaxPowerLimit: device.Metrics.PowerLimit.Value,
MemUsed: device.Metrics.MemoryUsed.Value,
MemTotal: device.Metrics.MemoryTotal.Value,
FanSpeed: device.Metrics.FanSpeed.Value,
}
if len(device.Processes) == 0 {
if device.ProcessStatus != "ok" {
return item
}
processes := make([]dto.GPUProcess, 0, len(device.Processes))
+12 -2
View File
@@ -154,7 +154,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
logger := logrus.New()
logger.SetFormatter(&SimpleFormatter{})
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
@@ -283,8 +283,18 @@ func (t *Task) updateTask(task *model.Task) {
_ = t.taskRepo.Update(context.Background(), task)
}
func (t *Task) Execute() error {
// Prepare makes a task visible before dispatching it to a background worker.
func (t *Task) Prepare() error {
if err := t.taskRepo.Save(context.Background(), t.Task); err != nil {
_ = t.logFile.Close()
global.RemoveTaskCancel(t.TaskID)
return err
}
return nil
}
func (t *Task) Execute() error {
if err := t.Prepare(); err != nil {
return err
}
var err error
+20 -4
View File
@@ -644,6 +644,13 @@
"formatZH": "创建容器网络 name",
"formatEN": "create container network [name]"
},
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": {
"bodyKeys": [
"names"
@@ -4028,12 +4035,21 @@
},
"/runtimes/operate": {
"bodyKeys": [
"id"
"ID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作运行环境 [id]",
"formatEN": "Operate runtime [id]"
"beforeFunctions": [
{
"input_column": "id",
"input_value": "ID",
"isList": false,
"db": "runtimes",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "操作运行环境 [name]",
"formatEN": "Operate runtime [name]"
},
"/runtimes/php/config": {
"bodyKeys": [
+11
View File
@@ -649,6 +649,7 @@ CommonAlert: "Panel {{ .node }}{{ .ip }}: {{ .msg }}. Log in to view details."
NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details."
LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details."
SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details."
CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details."
# disk
DeviceNotFound: "Device {{ .name }} not found"
@@ -730,3 +731,13 @@ FileTaskRename: 'Target filename: {{ .name }}'
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})"
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network"
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network"
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed"
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted"
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .msg }}. Inicie sesión en el pa
NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.'
LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.'
SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} no encontrado'
+2
View File
@@ -649,6 +649,8 @@ CommonAlert: "پنل {{ .node }}{{ .ip }}: {{ .msg }}. برای مشاهده ج
NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید."
LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید."
SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید."
CronJobSuccessAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی‌شده {{ .name }} با موفقیت تکمیل شد. برای مشاهده جزئیات وارد شوید."
# دیسک
DeviceNotFound: "دستگاه {{ .name }} یافت نشد"
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .msg }}。詳細
NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして'
LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして'
SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして'
CronJobSuccessAlert: 'パネル {{ .node }}{{ .ip }}: スケジュールタスク {{ .name }} が正常に完了しました。詳細はパネルにログインして確認してください。'
# ディスク
DeviceNotFound: 'デバイス {{ .name }} が見つかりません'
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .msg }}。자세한 내
NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
CronJobSuccessAlert: '패널 {{ .node }}{{ .ip }}: 예약 작업 {{ .name }}이(가) 성공적으로 완료되었습니다. 로그인하여 자세한 내용을 확인하십시오.'
# 디스크
DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다'
+1
View File
@@ -639,6 +639,7 @@ CommonAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: {{ .msg }}. ເ
NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
CronJobSuccessAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ສຳເລັດແລ້ວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
#disk
DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}"
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .msg }}. Sila log masuk ke pan
NodeExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} nod tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
LicenseExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} lesen tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
SSHAndPanelLoginAlert: 'Panel {{ .node }}{{ .ip }} Anda, log masuk {{ .name }} dari {{ .loginIp }} tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: tugas berjadual {{ .name }} berjaya diselesaikan. Log masuk untuk melihat butiran.'
# cakera
DeviceNotFound: 'Peranti {{ .name }} tidak ditemui'
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .msg }}. Faça login no painel
NodeExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} nós estão anormais. Faça login no painel para obter detalhes.'
LicenseExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} licenças estão anormais. Faça login no painel para obter detalhes.'
SSHAndPanelLoginAlert: 'Seu Painel {{ .node }}{{ .ip }}, o login {{ .name }} a partir de {{ .loginIp }} é anormal. Faça login no painel para obter detalhes.'
CronJobSuccessAlert: 'Painel {{ .node }}{{ .ip }}: a tarefa agendada {{ .name }} foi concluída com sucesso. Faça login para ver os detalhes.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} não encontrado'
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .msg }}. Войдит
NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.'
LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.'
SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.'
CronJobSuccessAlert: 'Панель {{ .node }}{{ .ip }}: запланированная задача {{ .name }} успешно завершена. Войдите для просмотра подробностей.'
# диск
DeviceNotFound: 'Устройство {{ .name }} не найдено'
+1
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .msg }}. Detaylar için panelin
NodeExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} düğüm anormal durumda. Detaylar için paneline giriş yapın.'
LicenseExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} lisans anormal durumda. Detaylar için paneline giriş yapın.'
SSHAndPanelLoginAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .loginIp }} adresinden {{ .name }} girişi anormal. Detaylar için paneline giriş yapın.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: zamanlanmış görev {{ .name }} başarıyla tamamlandı. Ayrıntıları görmek için giriş yapın.'
# disk
DeviceNotFound: 'Cihaz {{ .name }} bulunamadı'
+11
View File
@@ -649,6 +649,7 @@ CommonAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},詳情請登入
NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。'
LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。'
SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。'
CronJobSuccessAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務 {{ .name }} 執行成功,詳情請登入面板檢視。'
# 磁碟
DeviceNotFound: '裝置 {{ .name }} 未找到'
@@ -730,3 +731,13 @@ FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除"
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除"
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除"
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在"
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除"
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
+11
View File
@@ -649,6 +649,7 @@ CommonAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},请登录面板
NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。"
LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。"
SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。"
CronJobSuccessAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行成功,请登录面板查看详情。"
# 磁盘
DeviceNotFound: "设备 {{ .name }} 未找到"
@@ -730,3 +731,13 @@ FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除"
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除"
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除"
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在"
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除"
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
+1
View File
@@ -111,6 +111,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
}
}
+7
View File
@@ -1908,3 +1908,10 @@ var AddDockerPortGuardReadOnly = &gormigrate.Migration{
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
},
}
var AddAcceleratorMetrics = &gormigrate.Migration{
ID: "20260928-accelerator-vendor-metrics",
Migrate: func(tx *gorm.DB) error {
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
+1
View File
@@ -77,6 +77,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.GET("/network", baseApi.ListNetwork)
baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/clean", baseApi.CleanNetworks)
baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume)
+1
View File
@@ -43,6 +43,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.GET("/redis/cli/status", baseApi.LoadRedisCliStatus)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf)
@@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -52,6 +53,11 @@ func (c Client) LoadInfo() (*Info, error) {
}
func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
results := make([]providerResult, len(c.providers))
var wg sync.WaitGroup
for index, item := range c.providers {
@@ -66,8 +72,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
}()
}
wg.Wait()
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot := &Snapshot{DriverVersions: make(map[string]string)}
snapshot := &Snapshot{}
var (
errs []error
active []*ProviderSnapshot
@@ -78,6 +87,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
errs = append(errs, result.err)
continue
}
if result.snapshot != nil {
for _, warning := range result.snapshot.Warnings {
errs = append(errs, errors.New(warning))
}
}
if result.snapshot == nil || len(result.snapshot.Devices) == 0 {
continue
}
@@ -90,11 +104,8 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if item.CudaVersion != "" {
snapshot.Info.CudaVersion = item.CudaVersion
}
if item.DriverVersion != "" {
snapshot.DriverVersions[item.Type] = item.DriverVersion
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
}
snapshot.Warnings = append(snapshot.Warnings, errs...)
@@ -102,6 +113,10 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
return nil, fmt.Errorf("calling accelerator monitoring tools failed: %w", errors.Join(errs...))
}
snapshot.Info.CollectedAt = time.Now()
for _, err := range errs {
snapshot.Info.Warnings = append(snapshot.Info.Warnings, err.Error())
}
snapshot.Info.XPUDriverVersion = xpuVersion
snapshot.Info.Type, snapshot.Info.DriverVersion = mergeProviderMetadata(active)
return snapshot, nil
+3 -2
View File
@@ -1,6 +1,7 @@
package accelerator
import (
"math"
"strconv"
"strings"
@@ -18,11 +19,11 @@ func metric(display, unit string) Metric {
return result
}
parsed, err := strconv.ParseFloat(matched[1], 64)
if err != nil {
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || (parsed < 0 && unit != "°C") {
return result
}
normalized, ok := convertMetricUnit(parsed, matched[2], unit)
if !ok {
if !ok || math.IsNaN(normalized) || math.IsInf(normalized, 0) || (unit == "%" && normalized > 100) {
return result
}
result.Value = &normalized
+79 -63
View File
@@ -12,26 +12,38 @@ import (
func normalizeGPU(item *gpu.Device) Device {
metrics := Metrics{
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.MaxPowerLimit, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
MemoryActivity: metric(item.MemoryActivity, "%"),
EncoderUtil: metric(item.EncoderUtil, "%"),
DecoderUtil: metric(item.DecoderUtil, "%"),
JPEGUtil: metric(item.JPEGUtil, "%"),
OFAUtil: metric(item.OFAUtil, "%"),
MediaUtil: metric(item.MediaUtil, "%"),
HotspotTemperature: metric(item.HotspotTemperature, "°C"),
FanRPM: metric(item.FanRPM, "RPM"),
MediaFrequency: metric(item.MediaFrequency, "MHz"),
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
MemoryTemperature: metric(item.MemoryTemperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.PowerLimit, "W"),
Frequency: metric(item.Frequency, "MHz"),
MemoryFrequency: metric(item.MemoryFrequency, "MHz"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
}
device := Device{
ID: stableID(item.Type, item.BusID, strconv.FormatUint(uint64(item.Index), 10)),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID(item.Type, item.UUID, stableID("pci", item.BusID, strconv.FormatUint(uint64(item.Index), 10))),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -45,26 +57,36 @@ func normalizeGPU(item *gpu.Device) Device {
func normalizeNPU(item *npu.Device) Device {
metrics := Metrics{
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
AICPUUtil: metric(item.AICPUUtil, "%"),
CtrlCPUUtil: metric(item.CtrlCPUUtil, "%"),
DDRBandwidth: metric(item.DDRBandwidth, "%"),
HBMBandwidth: metric(item.HBMBandwidth, "%"),
DDRUsed: metric(item.MemoryUsed, "MiB"),
DDRTotal: metric(item.MemoryTotal, "MiB"),
HBMUsed: metric(item.HBMUsed, "MiB"),
HBMTotal: metric(item.HBMTotal, "MiB"),
HugepagesUsed: metric(item.HugepagesUsed, "pages"),
HugepagesTotal: metric(item.HugepagesTotal, "pages"),
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
}
device := Device{
ID: fmt.Sprintf("ascend:%d:%d", item.NPUIndex, item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
ProcessStatus: item.ProcessStatus,
ID: fmt.Sprintf("%s:%d", stableID("ascend", item.BusID, strconv.FormatUint(uint64(item.NPUIndex), 10)), item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -78,26 +100,32 @@ func normalizeNPU(item *npu.Device) Device {
func normalizeXPU(item *xpu.Device) Device {
metrics := Metrics{
Utilization: metric(item.Stats.GPUUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"),
Power: metric(item.Stats.Power, "W"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed),
MemoryTotal: memoryMetric(item.Basic.Memory),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"),
MediaUtil: metric(item.Stats.MediaUtil, "%"),
ComputeUtil: metric(item.Stats.ComputeUtil, "%"),
CopyUtil: metric(item.Stats.CopyUtil, "%"),
MediaFrequency: metric(item.Stats.MediaFrequency, "MHz"),
MemoryTemperature: metric(item.Stats.MemoryTemperature, "°C"),
MemoryBandwidth: metric(item.Stats.MemoryBandwidthUtil, "%"),
Utilization: metric(item.Stats.GPUUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"),
Power: metric(item.Stats.Power, "W"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed),
MemoryTotal: memoryMetric(item.Basic.Memory),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"),
}
device := Device{
ID: stableID("xpu", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID)),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID("xpu", item.Basic.UUID, stableID("pci", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID))),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: strconv.Itoa(process.PID),
@@ -110,18 +138,6 @@ func normalizeXPU(item *xpu.Device) Device {
return device
}
func capabilities(metrics Metrics) Capabilities {
return Capabilities{
Utilization: metrics.Utilization.Available(),
Temperature: metrics.Temperature.Available(),
Power: metrics.Power.Available(),
PowerLimit: metrics.PowerLimit.Available(),
Memory: metrics.MemoryUsed.Available() || metrics.MemoryTotal.Available(),
FanSpeed: metrics.FanSpeed.Available(),
Frequency: metrics.Frequency.Available(),
}
}
func stableID(vendor, busID, fallback string) string {
if busID != "" && !strings.EqualFold(busID, "N/A") {
return vendor + ":" + busID
+17 -1
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -26,6 +27,7 @@ func (p gpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
CudaVersion: info.CudaVersion,
GPUs: info.Devices,
@@ -49,6 +51,7 @@ func (p npuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
NPUs: info.Devices,
}
@@ -71,11 +74,24 @@ func (p xpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
XPUs: info.Devices,
}
for index := range result.XPUs {
result.Devices = append(result.Devices, normalizeXPU(&result.XPUs[index]))
parent := normalizeXPU(&result.XPUs[index])
result.Devices = append(result.Devices, parent)
for _, tile := range result.XPUs[index].Tiles {
raw := xpu.Device{Basic: result.XPUs[index].Basic, Stats: tile.Stats, ProcessStatus: "unavailable"}
raw.Basic.Memory = ""
raw.Basic.FreeMemory = ""
device := normalizeXPU(&raw)
device.ParentID = parent.ID
device.ID = fmt.Sprintf("%s:tile:%d", parent.ID, tile.TileID)
device.Label = fmt.Sprintf("%s / Tile %d", parent.Label, tile.TileID)
result.Devices = append(result.Devices, device)
}
}
return result, nil
}
+54 -34
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"errors"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -17,6 +18,9 @@ const (
)
type Info struct {
CollectedAt time.Time `json:"collectedAt"`
Warnings []string `json:"warnings"`
Type string `json:"type"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
@@ -44,25 +48,40 @@ func (m Metric) ValueOrZero() float64 {
}
type Metrics struct {
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
}
MemoryActivity Metric
EncoderUtil Metric
DecoderUtil Metric
JPEGUtil Metric
OFAUtil Metric
MediaUtil Metric
ComputeUtil Metric
CopyUtil Metric
HotspotTemperature Metric
FanRPM Metric
AICPUUtil Metric
CtrlCPUUtil Metric
DDRUsed Metric
DDRTotal Metric
HBMUsed Metric
HBMTotal Metric
DDRBandwidth Metric
HBMBandwidth Metric
MemoryBandwidth Metric
MediaFrequency Metric
HugepagesUsed Metric
HugepagesTotal Metric
type Capabilities struct {
Utilization bool
Temperature bool
Power bool
PowerLimit bool
Memory bool
FanSpeed bool
Frequency bool
MemoryTemperature Metric
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
MemoryFrequency Metric
}
type Process struct {
@@ -74,18 +93,19 @@ type Process struct {
}
type Device struct {
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Capabilities Capabilities
Processes []Process
ParentID string
ProcessStatus string
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Processes []Process
GPU *gpu.Device `json:"-"`
NPU *npu.Device `json:"-"`
@@ -93,10 +113,9 @@ type Device struct {
}
type Snapshot struct {
Info Info
Devices []Device
DriverVersions map[string]string
Warnings []error
Info Info
Devices []Device
Warnings []error
}
func (s Snapshot) Warning() error {
@@ -104,6 +123,7 @@ func (s Snapshot) Warning() error {
}
type ProviderSnapshot struct {
Warnings []string
Type string
DriverVersion string
CudaVersion string
+58 -17
View File
@@ -3,12 +3,13 @@ package gpu
import (
"context"
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -32,16 +33,22 @@ func findAMDSMI() (string, bool) {
func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
var (
staticData string
metricData string
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
staticData string
metricData string
extendedData string
extendedErr error
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
)
wg.Add(3)
wg.Add(4)
go func() {
defer wg.Done()
extendedData, extendedErr = runAMDSMI(ctx, a.command, "metric", "--clock", "--ecc", "--json")
}()
go func() {
defer wg.Done()
staticData, staticErr = runAMDSMI(ctx, a.command, "static", "--asic", "--bus", "--driver", "--limit", "--json")
@@ -64,16 +71,25 @@ func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
return nil, fmt.Errorf("parsing %s static output failed: %w", a.command, err)
}
if metricErr == nil {
metricErr = applyAMDMetrics(info, metricData)
}
if metricErr != nil {
global.LOG.Warnf("calling %s metric failed, metrics will be omitted: %v", a.command, metricErr)
} else if err := applyAMDMetrics(info, metricData); err != nil {
global.LOG.Warnf("parsing %s metric output failed, metrics will be omitted: %v", a.command, err)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s metrics: %v", a.command, metricErr))
}
if extendedErr == nil {
extendedErr = applyAMDMetrics(info, extendedData)
}
if extendedErr != nil {
info.Warnings = append(info.Warnings, fmt.Sprintf("%s extended metrics: %v", a.command, extendedErr))
}
if processErr == nil {
processErr = applyAMDProcesses(info, processData)
}
if processErr != nil {
global.LOG.Warnf("calling %s process failed, process information will be omitted: %v", a.command, processErr)
} else if err := applyAMDProcesses(info, processData); err != nil {
global.LOG.Warnf("parsing %s process output failed, process information will be omitted: %v", a.command, err)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s processes: %v", a.command, processErr))
}
return info, nil
}
@@ -96,6 +112,9 @@ func parseAMDStatic(data string) (*Info, error) {
}
device := Device{
Type: "amd",
UUID: amdStringAt(row, "uuid", "asic.uuid"),
DriverVersion: amdStringAt(row, "driver.version", "driver_version", "amdgpu_version"),
ProcessStatus: "unavailable",
Index: index,
ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"),
PersistenceMode: "N/A",
@@ -111,6 +130,7 @@ func parseAMDStatic(data string) (*Info, error) {
"limit.max_power_limit",
"limit.max_power",
),
PowerLimit: amdMetricAt(row, "W", "limit.ppt0.socket_power_limit", "limit.socket_power_limit"),
MemUsed: "N/A",
MemTotal: "N/A",
GPUUtil: "N/A",
@@ -150,11 +170,31 @@ func applyAMDMetrics(info *Info, data string) error {
continue
}
setAMDMetric(&device.GPUUtil, row, "%", "usage.gfx_activity", "usage.gfx", "gfx_activity", "gfx_usage")
setAMDMetric(&device.Temperature, row, "°C", "temperature.hotspot", "temperature.edge", "hotspot_temperature", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.Temperature, row, "°C", "temperature.edge", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.PowerDraw, row, "W", "power.socket_power", "socket_power", "power_usage")
setAMDMetric(&device.MemUsed, row, "MB", "mem_usage.used_vram", "vram.used", "used_vram", "vram_used")
setAMDMetric(&device.MemTotal, row, "MB", "mem_usage.total_vram", "vram.total", "total_vram", "vram_total")
setAMDMetric(&device.FanSpeed, row, "%", "fan.speed", "fan_speed")
setAMDMetric(&device.FanSpeed, row, "%", "fan.usage")
if amdMetricAt(row, "%", "fan.usage") == "" {
speed, speedErr := strconv.ParseFloat(amdStringAt(row, "fan.speed"), 64)
maximum, maxErr := strconv.ParseFloat(amdStringAt(row, "fan.max"), 64)
if speedErr == nil && maxErr == nil && !math.IsNaN(speed) && !math.IsInf(speed, 0) && maximum > 0 && !math.IsInf(maximum, 0) && speed >= 0 && speed <= maximum {
device.FanSpeed = fmt.Sprintf("%.2f %%", speed/maximum*100)
}
}
setAMDMetric(&device.FanRPM, row, "RPM", "fan.rpm")
setAMDMetric(&device.HotspotTemperature, row, "°C", "temperature.hotspot", "hotspot_temperature")
setAMDMetric(&device.MemoryTemperature, row, "°C", "temperature.mem")
setAMDMetric(&device.MemoryActivity, row, "%", "usage.umc_activity")
setAMDMetric(&device.MediaUtil, row, "%", "usage.mm_activity")
setAMDMetric(&device.Frequency, row, "MHz", "clock.gfx_0.clk")
setAMDMetric(&device.MemoryFrequency, row, "MHz", "clock.mem_0.clk")
correctable := amdStringAt(row, "ecc.total_correctable_count", "ecc.correctable_count")
uncorrectable := amdStringAt(row, "ecc.total_uncorrectable_count", "ecc.uncorrectable_count")
if correctable != "" || uncorrectable != "" {
device.ECCErrors = []ECCError{{Scope: "Total", Correctable: correctable, Uncorrectable: uncorrectable}}
}
if value := amdStringAt(row, "perf_level", "performance_level"); value != "" {
device.PerformanceState = value
}
@@ -177,6 +217,7 @@ func applyAMDProcesses(info *Info, data string) error {
if !ok {
continue
}
device.ProcessStatus = "ok"
processList, _ := amdValueAt(row, "process_list")
items := amdObjectList(processList)
if len(items) == 0 {
+4
View File
@@ -64,6 +64,7 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if result.info == nil {
continue
}
merged.Warnings = append(merged.Warnings, result.info.Warnings...)
if result.info.Type != "" {
types = append(types, result.info.Type)
}
@@ -86,6 +87,9 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if len(merged.Devices) == 0 && len(errs) > 0 {
return nil, fmt.Errorf("calling GPU monitoring tools failed: %w", errors.Join(errs...))
}
for _, err := range errs {
merged.Warnings = append(merged.Warnings, err.Error())
}
return merged, nil
}
+76 -69
View File
@@ -1,16 +1,12 @@
package gpu
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -24,41 +20,10 @@ func (n nvidiaSMI) LoadInfo(ctx context.Context) (*Info, error) {
if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", nvidiaSMICommand, err)
}
data := []byte(itemData)
version := "v11"
buf := bytes.NewBuffer(data)
decoder := xml.NewDecoder(buf)
for {
token, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("reading token failed: %w", err)
}
d, ok := token.(xml.Directive)
if !ok {
continue
}
directive := string(d)
if !strings.HasPrefix(directive, "DOCTYPE") {
continue
}
parts := strings.Split(directive, " ")
s := strings.Trim(parts[len(parts)-1], "\" ")
if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") {
version = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
} else {
global.LOG.Debugf("Cannot find schema version in %q", directive)
}
break
}
return parseNvidiaSMI(data, version)
return parseNvidiaSMI([]byte(itemData))
}
func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
func parseNvidiaSMI(buf []byte) (*Info, error) {
var (
s nvidiaSMIResponse
info Info
@@ -72,42 +37,84 @@ func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
info.DriverVersion = s.DriverVersion
for i := range s.Gpu {
gpuItem := Device{
Type: "nvidia",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccErrors.Volatile.DramUncorrectable,
FanSpeed: s.Gpu[i].FanSpeed,
Temperature: s.Gpu[i].Temperature.GpuTemp,
PerformanceState: s.Gpu[i].PerformanceState,
MemUsed: s.Gpu[i].FbMemoryUsage.Used,
MemTotal: s.Gpu[i].FbMemoryUsage.Total,
GPUUtil: s.Gpu[i].Utilization.GpuUtil,
ComputeMode: s.Gpu[i].ComputeMode,
MigMode: s.Gpu[i].MigMode.CurrentMig,
}
if version == "v12" || version == "v13" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
if gpuItem.PowerDraw == "" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.InstantPowerDraw
}
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.CurrentPowerLimit
} else {
gpuItem.PowerDraw = s.Gpu[i].PowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].PowerReadings.MaxPowerLimit
Type: "nvidia",
MemoryActivity: s.Gpu[i].Utilization.MemoryUtil,
EncoderUtil: s.Gpu[i].Utilization.EncoderUtil,
DecoderUtil: s.Gpu[i].Utilization.DecoderUtil,
JPEGUtil: s.Gpu[i].Utilization.JpegUtil,
OFAUtil: s.Gpu[i].Utilization.OfaUtil,
MediaFrequency: s.Gpu[i].Clocks.VideoClock,
UUID: s.Gpu[i].UUID,
DriverVersion: s.DriverVersion,
Architecture: s.Gpu[i].ProductArchitecture,
Frequency: s.Gpu[i].Clocks.GraphicsClock,
MemoryFrequency: s.Gpu[i].Clocks.MemClock,
MemoryTemperature: s.Gpu[i].Temperature.MemoryTemp,
MemoryFree: s.Gpu[i].FbMemoryUsage.Free,
MemoryReserved: s.Gpu[i].FbMemoryUsage.Reserved,
PCIeGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.DeviceCurrentLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.CurrentLinkGen),
PCIeMaxGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxDeviceLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxLinkGen),
PCIeWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.CurrentLinkWidth,
PCIeMaxWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.MaxLinkWidth,
ProcessStatus: "unavailable",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccMode.CurrentEcc,
FanSpeed: s.Gpu[i].FanSpeed,
Temperature: s.Gpu[i].Temperature.GpuTemp,
PerformanceState: s.Gpu[i].PerformanceState,
MemUsed: s.Gpu[i].FbMemoryUsage.Used,
MemTotal: s.Gpu[i].FbMemoryUsage.Total,
GPUUtil: s.Gpu[i].Utilization.GpuUtil,
ComputeMode: s.Gpu[i].ComputeMode,
MigMode: s.Gpu[i].MigMode.CurrentMig,
}
gpuItem.ECCPending = s.Gpu[i].EccMode.PendingEcc
gpuItem.ECCErrors = []ECCError{
{Scope: "Volatile Total", Correctable: s.Gpu[i].EccErrors.Volatile.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DoubleBit.Total},
{Scope: "Aggregate Total", Correctable: s.Gpu[i].EccErrors.Aggregate.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DoubleBit.Total},
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
{Scope: "Volatile DRAM", Correctable: s.Gpu[i].EccErrors.Volatile.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DramUncorrectable},
{Scope: "Volatile SRAM", Correctable: s.Gpu[i].EccErrors.Volatile.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.SramUncorrectable},
{Scope: "Aggregate DRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DramUncorrectable},
{Scope: "Aggregate SRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.SramUncorrectable},
}
gpuItem.PowerDraw = firstSMIValue(s.Gpu[i].GpuPowerReadings.PowerDraw, s.Gpu[i].GpuPowerReadings.InstantPowerDraw, s.Gpu[i].PowerReadings.PowerDraw)
gpuItem.PowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.CurrentPowerLimit, s.Gpu[i].PowerReadings.EnforcedPowerLimit, s.Gpu[i].PowerReadings.PowerLimit)
gpuItem.MaxPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.MaxPowerLimit, s.Gpu[i].PowerReadings.MaxPowerLimit)
gpuItem.DefaultPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.DefaultPowerLimit, s.Gpu[i].PowerReadings.DefaultPowerLimit)
for _, event := range append(s.Gpu[i].ClocksEventReasons.Reasons, s.Gpu[i].ClocksThrottleReasons.Reasons...) {
if strings.EqualFold(strings.TrimSpace(event.Value), "Active") {
gpuItem.ClockEvents = append(gpuItem.ClockEvents, strings.TrimPrefix(strings.TrimPrefix(event.XMLName.Local, "clocks_event_reason_"), "clocks_throttle_reason_"))
}
}
if s.Gpu[i].Processes != nil && strings.TrimSpace(s.Gpu[i].Processes.Text) == "" {
gpuItem.ProcessStatus = "ok"
}
if s.Gpu[i].Processes != nil {
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
}
}
info.Devices = append(info.Devices, gpuItem)
}
return &info, nil
}
func firstSMIValue(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" && !strings.EqualFold(value, "N/A") && !strings.EqualFold(value, "Not Supported") {
return value
}
}
return ""
}
+30 -13
View File
@@ -1,9 +1,12 @@
package gpu
import "encoding/xml"
type nvidiaSMIResponse struct {
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
XMLName xml.Name `xml:"nvidia_smi_log"`
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
Gpu []struct {
ID string `xml:"id,attr"`
AccountedProcesses struct{} `xml:"accounted_processes"`
@@ -37,16 +40,17 @@ type nvidiaSMIResponse struct {
VideoClock string `xml:"video_clock"`
} `xml:"clocks"`
ClocksEventReasons struct {
ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"`
ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"`
ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"`
ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"`
ClocksEventReasonHwSlowdown string `xml:"clocks_event_reason_hw_slowdown"`
ClocksEventReasonHwThermalSlowdown string `xml:"clocks_event_reason_hw_thermal_slowdown"`
ClocksEventReasonSwPowerCap string `xml:"clocks_event_reason_sw_power_cap"`
ClocksEventReasonSwThermalSlowdown string `xml:"clocks_event_reason_sw_thermal_slowdown"`
ClocksEventReasonSyncBoost string `xml:"clocks_event_reason_sync_boost"`
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `xml:"clocks_event_reasons"`
ClocksThrottleReasons struct {
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `xml:"clocks_throttle_reasons"`
ComputeMode string `xml:"compute_mode"`
DefaultApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"`
@@ -63,12 +67,24 @@ type nvidiaSMIResponse struct {
} `xml:"driver_model"`
EccErrors struct {
Aggregate struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"aggregate"`
Volatile struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
@@ -226,7 +242,8 @@ type nvidiaSMIResponse struct {
MinPowerLimit string `xml:"min_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
} `xml:"power_readings"`
Processes struct {
Processes *struct {
Text string `xml:",chardata"`
ProcessInfo []struct {
Pid string `xml:"pid"`
Type string `xml:"type"`
+37
View File
@@ -1,6 +1,8 @@
package gpu
type Info struct {
Warnings []string `json:"warnings"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
Type string `json:"type"`
@@ -8,7 +10,42 @@ type Info struct {
Devices []Device `json:"gpu"`
}
type ECCError struct {
Scope string `json:"scope"`
Correctable string `json:"correctable"`
Uncorrectable string `json:"uncorrectable"`
}
type Device struct {
ECCPending string `json:"eccPending"`
ECCErrors []ECCError `json:"eccErrors"`
MemoryActivity string `json:"memoryActivity"`
EncoderUtil string `json:"encoderUtil"`
DecoderUtil string `json:"decoderUtil"`
JPEGUtil string `json:"jpegUtil"`
OFAUtil string `json:"ofaUtil"`
MediaUtil string `json:"mediaUtil"`
HotspotTemperature string `json:"hotspotTemperature"`
FanRPM string `json:"fanRPM"`
MediaFrequency string `json:"mediaFrequency"`
UUID string `json:"uuid"`
DriverVersion string `json:"driverVersion"`
Architecture string `json:"architecture"`
Frequency string `json:"frequency"`
MemoryFrequency string `json:"memoryFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryFree string `json:"memoryFree"`
MemoryReserved string `json:"memoryReserved"`
PowerLimit string `json:"powerLimit"`
DefaultPowerLimit string `json:"defaultPowerLimit"`
PCIeGeneration string `json:"pcieGeneration"`
PCIeMaxGeneration string `json:"pcieMaxGeneration"`
PCIeWidth string `json:"pcieWidth"`
PCIeMaxWidth string `json:"pcieMaxWidth"`
ProcessStatus string `json:"processStatus"`
ClockEvents []string `json:"clockEvents"`
Type string `json:"type"`
Index uint `json:"index"`
ProductName string `json:"productName"`
+61 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -34,7 +35,31 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", ascendSMICommand, err)
}
return parseAscendSMI(itemData), nil
info := parseAscendSMI(itemData)
var wg sync.WaitGroup
warnings := make([]string, len(info.Devices))
for i := range info.Devices {
wg.Add(1)
go func(index int) {
defer wg.Done()
device := &info.Devices[index]
mgr := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second))
data, err := mgr.RunWithStdout(ascendSMICommand, "info", "-t", "usages", "-i", strconv.FormatUint(uint64(device.NPUIndex), 10), "-c", strconv.FormatUint(uint64(device.ChipIndex), 10))
if err != nil {
warnings[index] = fmt.Sprintf("npu-smi usages %d/%d: %v", device.NPUIndex, device.ChipIndex, err)
return
}
applyAscendUsages(device, data)
}(i)
}
wg.Wait()
for _, warning := range warnings {
if warning != "" {
info.Warnings = append(info.Warnings, warning)
}
}
return info, nil
}
func parseAscendSMI(data string) *Info {
@@ -158,6 +183,12 @@ func parseAscendSMI(data string) *Info {
pending = nil
}
for i := range info.Devices {
info.Devices[i].ProcessStatus = "unavailable"
if processSection {
info.Devices[i].ProcessStatus = "ok"
}
}
return info
}
@@ -208,6 +239,9 @@ func ascendMemoryPools(value, header string) (string, string, string, string) {
}
hbm := usage[len(usage)-1]
if !strings.Contains(normalizedHeader, "MEMORYUSAGE") {
memoryUsed, memoryTotal = "", ""
}
return memoryUsed, memoryTotal, hbm[0] + " MB", hbm[1] + " MB"
}
@@ -233,3 +267,29 @@ func ascendValueWithUnit(value, unit string) string {
}
return value + " " + unit
}
func applyAscendUsages(device *Device, data string) {
for _, line := range strings.Split(data, "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.ToLower(strings.Join(strings.Fields(key), ""))
value = strings.TrimSpace(value)
switch key {
case "aicoreusagerate(%)":
usage, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(value, "%")), 64)
if err == nil && usage >= 0 && usage <= 100 {
device.AICore = ascendValueWithUnit(value, "%")
}
case "aicpuusagerate(%)":
device.AICPUUtil = ascendValueWithUnit(value, "%")
case "ctrlcpuusagerate(%)":
device.CtrlCPUUtil = ascendValueWithUnit(value, "%")
case "ddrbandwidthusagerate(%)", "memorybandwidthusagerate(%)":
device.DDRBandwidth = ascendValueWithUnit(value, "%")
case "hbmbandwidthusagerate(%)":
device.HBMBandwidth = ascendValueWithUnit(value, "%")
}
}
}
+8
View File
@@ -1,12 +1,20 @@
package npu
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
Devices []Device `json:"npu"`
}
type Device struct {
AICPUUtil string `json:"aiCPUUtil"`
CtrlCPUUtil string `json:"ctrlCPUUtil"`
DDRBandwidth string `json:"ddrBandwidth"`
HBMBandwidth string `json:"hbmBandwidth"`
ProcessStatus string `json:"processStatus"`
Type string `json:"type"`
Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
+28 -2
View File
@@ -33,16 +33,29 @@ type discoveryInfo struct {
}
type DeviceLevelMetric struct {
MetricsType string `json:"metrics_type"`
Value float64 `json:"value"`
MetricsType string `json:"metrics_type"`
Value *float64 `json:"value"`
}
type TileMetrics struct {
TileID int `json:"tile_id"`
DataList []DeviceLevelMetric `json:"data_list"`
}
type TileStats struct {
TileID int `json:"tileID"`
Stats Stats `json:"stats"`
}
type DeviceStats struct {
TileLevel []TileMetrics `json:"tile_level"`
DeviceID int `json:"device_id"`
DeviceLevel []DeviceLevelMetric `json:"device_level"`
}
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
@@ -50,12 +63,17 @@ type Info struct {
}
type Device struct {
Tiles []TileStats `json:"tiles"`
ProcessStatus string `json:"processStatus"`
Basic Basic `json:"basic"`
Stats Stats `json:"stats"`
Processes []Process `json:"processes"`
}
type Basic struct {
UUID string `json:"uuid"`
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
VendorName string `json:"vendorName"`
@@ -66,6 +84,14 @@ type Basic struct {
}
type Stats struct {
MediaUtil string `json:"mediaUtil"`
ComputeUtil string `json:"computeUtil"`
CopyUtil string `json:"copyUtil"`
MediaFrequency string `json:"mediaFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryBandwidthUtil string `json:"memoryBandwidthUtil"`
Power string `json:"power"`
GPUUtil string `json:"gpuUtil"`
Frequency string `json:"frequency"`
+53 -13
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"sync"
@@ -51,12 +52,15 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
processData, err := cmdMgr.RunWithStdout(xpuSMICommand, "ps", "-j")
if err != nil {
global.LOG.Warnf("calling xpu-smi ps failed, process information will be omitted: %v", err)
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps: %v", err))
} else {
var psList DeviceUtilByProcList
if err := json.Unmarshal([]byte(processData), &psList); err != nil {
global.LOG.Warnf("processData json unmarshal failed, process information will be omitted: %v", err)
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps output: %v", err))
} else {
for i := range res.Devices {
res.Devices[i].ProcessStatus = "ok"
}
for _, ps := range psList.DeviceUtilByProcList {
process := Process{
PID: ps.ProcessID,
@@ -87,7 +91,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
defer wg.Done()
xpu := Device{
ProcessStatus: "unavailable",
Basic: Basic{
UUID: device.UUID,
DeviceID: device.DeviceID,
DeviceName: device.DeviceName,
VendorName: device.VendorName,
@@ -115,13 +121,19 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
wgCmd.Wait()
if xpuErr != nil {
global.LOG.Errorf("calling xpu-smi discovery failed for device %d, %v", device.DeviceID, xpuErr)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d: %v", device.DeviceID, xpuErr))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return
}
var info discoveryDevice
if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
global.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v", device.DeviceID, err)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d output: %v", device.DeviceID, err))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return
}
@@ -142,13 +154,22 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
}
if statsErr != nil {
global.LOG.Warnf("calling xpu-smi stats failed for device %d, metrics will be omitted: %v", device.DeviceID, statsErr)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d: %v", device.DeviceID, statsErr))
mu.Unlock()
} else {
var stats DeviceStats
if err := json.Unmarshal([]byte(statsData), &stats); err != nil {
global.LOG.Warnf("statsData json unmarshal failed for device %d, metrics will be omitted: %v", device.DeviceID, err)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d output: %v", device.DeviceID, err))
mu.Unlock()
} else {
loadStats(&xpu.Stats, stats.DeviceLevel)
for _, tile := range stats.TileLevel {
item := TileStats{TileID: tile.TileID}
loadStats(&item.Stats, tile.DataList)
xpu.Tiles = append(xpu.Tiles, item)
}
}
}
@@ -162,19 +183,38 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
func loadStats(stats *Stats, metrics []DeviceLevelMetric) {
for _, stat := range metrics {
if stat.Value == nil || math.IsNaN(*stat.Value) || math.IsInf(*stat.Value, 0) {
continue
}
value := *stat.Value
if value < 0 && stat.MetricsType != "XPUM_STATS_GPU_CORE_TEMPERATURE" && stat.MetricsType != "XPUM_STATS_MEMORY_TEMPERATURE" {
continue
}
switch stat.MetricsType {
case "XPUM_STATS_MEMORY_TEMPERATURE":
stats.MemoryTemperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_ENGINE_GROUP_COMPUTE_ALL_UTILIZATION":
stats.ComputeUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_MEDIA_ALL_UTILIZATION":
stats.MediaUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_COPY_ALL_UTILIZATION":
stats.CopyUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEDIA_ENGINE_FREQUENCY":
stats.MediaFrequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_POWER":
stats.Power = fmt.Sprintf("%.1fW", stat.Value)
stats.Power = fmt.Sprintf("%.1fW", value)
case "XPUM_STATS_GPU_UTILIZATION":
stats.GPUUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.GPUUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_GPU_FREQUENCY":
stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value)
stats.Frequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_GPU_CORE_TEMPERATURE":
stats.Temperature = fmt.Sprintf("%.1f°C", stat.Value)
stats.Temperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_MEMORY_USED":
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", stat.Value)
case "XPUM_STATS_MEMORY_UTILIZATION", "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", value)
case "XPUM_STATS_MEMORY_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryBandwidthUtil = fmt.Sprintf("%.1f%%", value)
}
}
}
+48 -7
View File
@@ -24,10 +24,10 @@ import (
"github.com/jinzhu/copier"
)
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"}
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp", "syncIpGroup", "cleanLog"}
func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule
@@ -76,14 +76,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient,
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
}
msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("PanelAlertTitle"),
Body: content,
IsHTML: true,
IsHTML: GetCronJobType(alert.Type) != "cronJob",
}
if err = email.SendMail(smtpConfig, msg, transport); err != nil {
@@ -110,7 +110,7 @@ func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []
return SaveAlertLog(create, &alertLog)
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
}
@@ -268,7 +268,7 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
alertDetail := dto.AlertDetail{
Type: GetCronJobType(alert.Type),
SubType: alert.Type,
Title: alert.Title,
Title: TaskAlertTitle(alert.Type, alert.Title, project, params),
Method: method,
Project: project,
Params: params,
@@ -281,6 +281,14 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
return string(marshal)
}
func TaskAlertTitle(alertType, title, project string, params []dto.Param) string {
if GetCronJobType(alertType) != "cronJob" || CronJobAlertResultFromParams(params) != CronJobAlertSuccess {
return title
}
name := cronJobTaskName(project, params)
return i18n.GetMsgWithMap("TaskSuccess", map[string]interface{}{"name": name})
}
func ProcessAlertRule(alert dto.AlertDTO) string {
marshal, err := json.Marshal(alert)
if err != nil {
@@ -324,6 +332,10 @@ func GetCronJobTypeName(cronJobType string) string {
module = "系统快照"
case "ntp":
module = "同步服务器时间"
case "syncIpGroup":
module = "同步 IP 组"
case "cleanLog":
module = "清理日志"
default:
}
return module
@@ -444,6 +456,30 @@ func isWithinTimeRange(savedTimeString string) bool {
}
func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentInfo) string {
return GetAlertDetailContent(dto.AlertDetail{Type: alertType, Params: params}, agentInfo)
}
func GetAlertLogContent(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, agentInfo *dto.AgentInfo) string {
detail := dto.AlertDetail{Type: alert.Type, Params: params}
var stored dto.AlertDetail
if json.Unmarshal([]byte(create.AlertDetail), &stored) == nil {
detail = stored
if detail.Type == "" {
detail.Type = alert.Type
}
if detail.Params == nil {
detail.Params = params
}
}
return GetAlertDetailContent(detail, agentInfo)
}
func GetAlertDetailContent(detail dto.AlertDetail, agentInfo *dto.AgentInfo) string {
alertType := detail.SubType
if alertType == "" {
alertType = detail.Type
}
params := detail.Params
switch GetCronJobType(alertType) {
case "ssl":
return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
@@ -464,7 +500,12 @@ func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentIn
case "disk":
return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "cronJob":
return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
messageKey := "CronJobFailedAlert"
if CronJobAlertResultFromParams(params) == CronJobAlertSuccess {
messageKey = "CronJobSuccessAlert"
}
name := cronJobTaskName(detail.Project, params)
return i18n.GetMsgWithMap(messageKey, map[string]interface{}{"name": name, "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "clams":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "panelLogin":
+130
View File
@@ -0,0 +1,130 @@
package alert
import (
"encoding/json"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
const (
CronJobAlertFailed = "failed"
CronJobAlertSuccess = "success"
CronJobAlertBoth = "both"
)
func cronJobAlertParams(advanced string) (map[string]json.RawMessage, error) {
params := make(map[string]json.RawMessage)
if strings.TrimSpace(advanced) != "" {
if err := json.Unmarshal([]byte(advanced), &params); err != nil {
return nil, fmt.Errorf("invalid cronjob alert advanced parameters: %w", err)
}
}
if params == nil {
params = make(map[string]json.RawMessage)
}
return params, nil
}
func CronJobAlertTriggerMode(advanced string) (string, error) {
params, err := cronJobAlertParams(advanced)
if err != nil {
return CronJobAlertFailed, err
}
mode := CronJobAlertFailed
if raw, ok := params["alertTriggerMode"]; ok {
if err := json.Unmarshal(raw, &mode); err != nil {
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode: %w", err)
}
}
switch mode {
case CronJobAlertFailed, CronJobAlertSuccess, CronJobAlertBoth:
return mode, nil
default:
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode %q", mode)
}
}
func MergeCronJobAlertParams(previous, incoming string) (string, error) {
params, err := cronJobAlertParams(previous)
if err != nil {
return "", err
}
updates, err := cronJobAlertParams(incoming)
if err != nil {
return "", err
}
for key, value := range updates {
params[key] = value
}
if _, ok := params["alertTriggerMode"]; !ok {
params["alertTriggerMode"] = json.RawMessage(`"failed"`)
}
data, err := json.Marshal(params)
if err != nil {
return "", err
}
if _, err := CronJobAlertTriggerMode(string(data)); err != nil {
return "", err
}
return string(data), nil
}
func MatchCronJobAlertResult(advanced, result string) bool {
mode, err := CronJobAlertTriggerMode(advanced)
if err != nil {
return false
}
if result == "" {
result = CronJobAlertFailed
}
if result != CronJobAlertFailed && result != CronJobAlertSuccess {
return false
}
return mode == CronJobAlertBoth || mode == result
}
func CreateTaskAlertParams(pushAlert dto.PushAlert) []dto.Param {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
if GetCronJobType(pushAlert.AlertType) != "cronJob" {
return params
}
params = append(params, CreateCronJobResultParam(pushAlert.Result))
return params
}
func cronJobTaskName(project string, params []dto.Param) string {
if project != "" {
return project
}
if name := getValueByIndex(params, "taskName"); name != "" {
return name
}
return getValueByIndex(params, "1")
}
func CreateCronJobResultParam(result string) dto.Param {
value := "失败"
if result == CronJobAlertSuccess {
value = "成功"
}
return dto.Param{Index: "2", Key: "result", Value: value}
}
func CronJobAlertResultFromParams(params []dto.Param) string {
for _, param := range params {
if param.Index == "result" {
if param.Value == CronJobAlertSuccess {
return CronJobAlertSuccess
}
return CronJobAlertFailed
}
}
for _, param := range params {
if param.Index == "2" && param.Key == "result" && param.Value == "成功" {
return CronJobAlertSuccess
}
}
return CronJobAlertFailed
}
+2 -2
View File
@@ -46,7 +46,7 @@ func CreateTaskScanCustomWebhookAlertLog(
transport *http.Transport,
agentInfo *dto.AgentInfo,
) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertInfo := info
alertInfo.Type = alertType
create.Type = GetCronJobType(alertType)
@@ -99,7 +99,7 @@ func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occur
if businessType == "" {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
}
content := GetSendContent(businessType, detail.Params, agentInfo)
content := GetAlertDetailContent(detail, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title})
}
+4 -1
View File
@@ -26,6 +26,9 @@ func PushAlert(pushAlert dto.PushAlert) error {
}
var alert dto.AlertDTO
_ = copier.Copy(&alert, &alertInfo)
if alertUtil.GetCronJobType(pushAlert.AlertType) == "cronJob" && !alertUtil.MatchCronJobAlertResult(alert.AdvancedParams, pushAlert.Result) {
return nil
}
methods := strings.Split(alert.Method, ",")
for _, m := range methods {
@@ -126,7 +129,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
params := alertUtil.CreateTaskAlertParams(pushAlert)
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule
+108
View File
@@ -0,0 +1,108 @@
package docker
import (
"context"
"sort"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type NetworkCleanupClient interface {
NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error)
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
NetworkInspect(context.Context, string, network.InspectOptions) (network.Inspect, error)
NetworkRemove(context.Context, string) error
}
func CleanUnusedNetworks(ctx context.Context, cli NetworkCleanupClient, onResult ...func(string, dto.NetworkCleanupItem)) (*dto.NetworkCleanupReport, error) {
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, err
}
used := make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
report := &dto.NetworkCleanupReport{Deleted: []dto.NetworkCleanupItem{}, Skipped: []dto.NetworkCleanupItem{}, Failed: []dto.NetworkCleanupItem{}}
record := func(status string, item dto.NetworkCleanupItem) {
switch status {
case "deleted":
report.Deleted = append(report.Deleted, item)
case "skipped":
report.Skipped = append(report.Skipped, item)
case "failed":
report.Failed = append(report.Failed, item)
}
for _, notify := range onResult {
if notify != nil {
notify(status, item)
}
}
}
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return report, err
}
item := dto.NetworkCleanupItem{ID: n.ID, Name: n.Name}
switch {
case n.Name == "none" || n.Name == "host" || n.Name == "bridge" || n.Name == "1panel-network":
item.Reason = "protected"
case n.Scope != "local" || n.Ingress || n.ConfigOnly:
item.Reason = "unsupported_network"
case used[n.Name] || used[n.ID]:
item.Reason = "container_connected"
}
if item.Reason != "" {
record("skipped", item)
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if errdefs.IsNotFound(err) {
item.Reason = "already_removed"
record("skipped", item)
} else {
item.Reason = "inspect_failed"
record("failed", item)
}
continue
}
if len(inspected.Containers) > 0 {
item.Reason = "container_connected"
record("skipped", item)
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
switch {
case errdefs.IsNotFound(err):
item.Reason = "already_removed"
record("skipped", item)
case errdefs.IsConflict(err):
item.Reason = "network_in_use"
record("skipped", item)
default:
item.Reason = "remove_failed"
record("failed", item)
}
continue
}
record("deleted", item)
}
return report, nil
}
@@ -0,0 +1,72 @@
package docker
import (
"context"
"errors"
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type cleanupFake struct {
networks []network.Inspect
containers []container.Summary
inspected map[string]network.Inspect
inspectErrors, removeErrors map[string]error
listError error
removed []string
}
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return f.networks, nil
}
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
if !o.All {
panic("must include stopped containers")
}
return f.containers, f.listError
}
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
return f.inspected[id], f.inspectErrors[id]
}
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
f.removed = append(f.removed, id)
return f.removeErrors[id]
}
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
}
// Compose ownership labels do not protect an otherwise unused network.
for i := range f.networks {
if f.networks[i].Name == "configured" {
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
}
}
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
report, err := CleanUnusedNetworks(context.Background(), f)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
t.Fatal(f.removed)
}
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
t.Fatalf("%+v", report)
}
}
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
t.Fatal("expected error")
}
if len(f.removed) > 0 {
t.Fatal(f.removed)
}
}
+67 -1
View File
@@ -15010,6 +15010,30 @@ const docTemplate = `{
]
}
},
"/databases/redis/cli/status": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RedisCliStatus"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load redis-cli installation status",
"tags": [
"Database Redis"
]
}
},
"/databases/redis/conf": {
"post": {
"consumes": [
@@ -15092,9 +15116,26 @@ const docTemplate = `{
},
"/databases/redis/install/cli": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RedisCliStatus"
}
}
},
"security": [
@@ -39731,6 +39772,31 @@ const docTemplate = `{
],
"type": "object"
},
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisCliStatus": {
"type": "object",
"properties": {
"errorMsg": {
"type": "string"
},
"installed": {
"type": "boolean"
},
"status": {
"type": "string"
},
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": {
"properties": {
"containerName": {
+67 -1
View File
@@ -15006,6 +15006,30 @@
]
}
},
"/databases/redis/cli/status": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RedisCliStatus"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load redis-cli installation status",
"tags": [
"Database Redis"
]
}
},
"/databases/redis/conf": {
"post": {
"consumes": [
@@ -15088,9 +15112,26 @@
},
"/databases/redis/install/cli": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RedisCliStatus"
}
}
},
"security": [
@@ -39727,6 +39768,31 @@
],
"type": "object"
},
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisCliStatus": {
"type": "object",
"properties": {
"errorMsg": {
"type": "string"
},
"installed": {
"type": "boolean"
},
"status": {
"type": "string"
},
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": {
"properties": {
"containerName": {
+20 -4
View File
@@ -644,6 +644,13 @@
"formatZH": "创建容器网络 name",
"formatEN": "create container network [name]"
},
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": {
"bodyKeys": [
"names"
@@ -4028,12 +4035,21 @@
},
"/runtimes/operate": {
"bodyKeys": [
"id"
"ID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作运行环境 [id]",
"formatEN": "Operate runtime [id]"
"beforeFunctions": [
{
"input_column": "id",
"input_value": "ID",
"isList": false,
"db": "runtimes",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "操作运行环境 [name]",
"formatEN": "Operate runtime [name]"
},
"/runtimes/php/config": {
"bodyKeys": [
+5
View File
@@ -27,6 +27,11 @@ var demoAllowedRoutes = map[demoRoute]struct{}{
{http.MethodPost, "/api/v2/toolbox/clam/base"}: {},
{http.MethodPost, "/api/v2/backups/record/size"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/base"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/forward/base"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/rules/native/detail"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/rules/sync/preview"}: {},
{http.MethodPost, "/api/v2/core/auth/login"}: {},
{http.MethodPost, "/api/v2/core/logs/login"}: {},
{http.MethodPost, "/api/v2/core/logs/operation"}: {},
+33
View File
@@ -0,0 +1,33 @@
# 批量清理未使用的容器网络
`POST /api/v2/containers/network/clean`,使用现有 1Panel 认证,不需要请求体。
这是异步接口,立即返回 `data.taskID`。实际结果在任务中心和任务日志查看,HTTP 请求结束不会取消任务。
规则:
- 永远保留 `none`、`host`、`bridge`、`1panel-network`。
- 保留所有容器(包括停止容器)引用的网络,删除前再通过 NetworkInspect 检查连接。
- 不检查 Compose 文件或应用配置引用;仅被这些配置引用、没有容器连接的普通网络也会被删除。
- 当前仅清理本地普通网络,跳过 Swarm、ingress 和 config-only 网络。
- Docker 删除时发现新连接会拒绝删除;不会强制断开容器。
成功响应示例:
```json
{"code":200,"data":{"taskID":"服务端生成的任务 UUID"}}
```
任务类型为 Container,操作为 TaskClean。网络页“清理网络”按钮调用此接口,拿到 taskID 后立即打开任务日志,并刷新任务中心。
每个网络处理完成后写入名称、ID 和结果;有容器连接或 Docker 返回占用冲突时,明确记录“未删除”。最后记录已删除、跳过和失败数量。
跳过在用网络属于正常结果;检查/删除失败会将任务标记为失败,已删除的网络不回滚。
日志调用链:
- `agent/app/service/container_network_cleanup.go`:NewTaskWithOps 创建任务,AddSubTask 注册清理步骤,Execute 执行任务,t.Log 写逐项日志。
- `agent/app/task/task.go`:任务持久化,维护状态,写入开始、结束标记;日志文件为 `<global.Dir.TaskDir>/Container/<taskID>.log`。
- `frontend/src/views/container/network/index.vue`:请求成功后调用 openTaskLog(taskID)。
- `frontend/src/components/log/task/index.vue`:openWithTaskID 打开日志弹窗。
- `frontend/src/components/log/file/index.vue`:通过 `/logs/tasks/read` 按行读取任务日志。
范围:不检查 Compose 文件或应用配置引用;仅清理本地普通网络。原 `/containers/prune` 接口仍保持原逻辑。
测试使用假 Docker 客户端和临时任务数据库,不执行真实网络删除。
+10 -173
View File
@@ -126,7 +126,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -1014,7 +1013,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1031,7 +1029,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1048,7 +1045,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1065,7 +1061,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1082,7 +1077,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1099,7 +1093,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1116,7 +1109,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1133,7 +1125,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1150,7 +1141,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1167,7 +1157,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1184,7 +1173,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1201,7 +1189,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1218,7 +1205,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1235,7 +1221,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1252,7 +1237,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1269,7 +1253,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1286,7 +1269,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1303,7 +1285,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1320,7 +1301,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1337,7 +1317,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1354,7 +1333,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1371,7 +1349,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1388,7 +1365,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1405,7 +1381,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1422,7 +1397,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1439,7 +1413,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1773,18 +1746,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -2013,7 +1974,6 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2053,7 +2013,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2074,7 +2033,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2095,7 +2053,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2116,7 +2073,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2137,7 +2093,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2158,7 +2113,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2179,7 +2133,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2200,7 +2153,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2221,7 +2173,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2242,7 +2193,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2263,7 +2213,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2284,7 +2233,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2305,7 +2253,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2350,7 +2297,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2367,7 +2313,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2384,7 +2329,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2401,7 +2345,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2418,7 +2361,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2435,7 +2377,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2452,7 +2393,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2469,7 +2409,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2486,7 +2425,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2503,7 +2441,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2520,7 +2457,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2537,7 +2473,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2554,7 +2489,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2571,7 +2505,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2984,7 +2917,6 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -3016,9 +2948,8 @@
"version": "26.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
"integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~8.3.0"
}
@@ -3321,7 +3252,6 @@
"integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.68.0",
"@typescript-eslint/types": "8.68.0",
@@ -4005,7 +3935,6 @@
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.4.0",
@@ -4052,15 +3981,13 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4317,7 +4244,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -4347,7 +4273,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
@@ -4688,7 +4613,6 @@
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -5373,7 +5297,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"bin": {
@@ -5766,7 +5689,6 @@
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -5828,7 +5750,6 @@
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
"dev": true,
"license": "MIT",
"peer": true,
"workspaces": [
"packages/*"
],
@@ -5888,7 +5809,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -6240,7 +6160,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6342,7 +6261,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -6822,7 +6740,7 @@
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/imurmurhash": {
@@ -6895,7 +6813,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6921,7 +6839,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -6966,7 +6884,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -7052,7 +6969,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -7743,15 +7660,13 @@
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -7955,7 +7870,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.5.0",
@@ -8253,7 +8167,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -8441,7 +8354,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
@@ -8697,7 +8609,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -8722,7 +8633,6 @@
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
@@ -8803,7 +8713,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
@@ -8915,7 +8824,6 @@
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9284,7 +9192,6 @@
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.143.0",
"@rolldown/pluginutils": "^1.0.0"
@@ -9394,9 +9301,8 @@
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.103.1.tgz",
"integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^5.0.0",
"immutable": "^5.1.5",
@@ -9622,29 +9528,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/spdx-correct": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@@ -9989,34 +9872,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/text-extensions": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz",
@@ -10098,7 +9953,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -10110,7 +9964,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -10175,7 +10028,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10214,7 +10066,7 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/unimport": {
@@ -10604,7 +10456,6 @@
"integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
@@ -10879,7 +10730,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10900,7 +10750,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10921,7 +10770,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10942,7 +10790,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10963,7 +10810,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10984,7 +10830,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11005,7 +10850,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11026,7 +10870,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11047,7 +10890,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11068,7 +10910,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11089,7 +10930,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -11128,7 +10968,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.42",
"@vue/compiler-sfc": "3.5.42",
@@ -11182,7 +11021,6 @@
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
@@ -11209,7 +11047,6 @@
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0 || ^9.0.0",
+90 -11
View File
@@ -22,6 +22,8 @@ export namespace AI {
}
export interface Info {
collectedAt?: string;
warnings?: string[];
cudaVersion: string;
driverVersion: string;
type: string;
@@ -31,6 +33,34 @@ export namespace AI {
xpu: XpuInfo['xpu'];
}
export interface GPU {
eccPending?: string;
eccErrors?: { scope: string; correctable: string; uncorrectable: string }[];
memoryActivity?: string;
encoderUtil?: string;
decoderUtil?: string;
jpegUtil?: string;
ofaUtil?: string;
mediaUtil?: string;
hotspotTemperature?: string;
fanRPM?: string;
mediaFrequency?: string;
uuid?: string;
driverVersion?: string;
architecture?: string;
frequency?: string;
memoryFrequency?: string;
memoryTemperature?: string;
memoryFree?: string;
memoryReserved?: string;
powerLimit?: string;
defaultPowerLimit?: string;
pcieGeneration?: string;
pcieMaxGeneration?: string;
pcieWidth?: string;
pcieMaxWidth?: string;
clockEvents?: string[];
processStatus?: string;
type: 'nvidia' | 'amd';
index: number;
productName: string;
@@ -52,6 +82,12 @@ export namespace AI {
processes: Process[];
}
export interface NPU {
aiCPUUtil?: string;
ctrlCPUUtil?: string;
ddrBandwidth?: string;
hbmBandwidth?: string;
processStatus?: string;
type: 'ascend';
index: number;
npuIndex: number;
@@ -73,6 +109,9 @@ export namespace AI {
processes: NPUProcess[];
}
export interface MonitorGPUSearch {
aggregation?: 'avg' | 'max';
deviceID?: string;
legacy?: boolean;
productName: string;
startTime: Date;
endTime: Date;
@@ -83,8 +122,10 @@ export namespace AI {
chartHide: Array<ChartHide>;
}
export interface ChartHide {
deviceID?: string;
legacy?: boolean;
productName: string;
type?: 'gpu' | 'xpu';
type?: 'gpu' | 'npu' | 'xpu';
process: boolean;
gpu: boolean;
memory: boolean;
@@ -94,17 +135,45 @@ export namespace AI {
speed: boolean;
}
export interface MonitorGPUData {
memoryActivity: Array<number | null>;
encoderUtil: Array<number | null>;
decoderUtil: Array<number | null>;
jpegUtil: Array<number | null>;
ofaUtil: Array<number | null>;
mediaUtil: Array<number | null>;
computeUtil: Array<number | null>;
copyUtil: Array<number | null>;
hotspotTemperature: Array<number | null>;
fanRPM: Array<number | null>;
aiCPUUtil: Array<number | null>;
ctrlCPUUtil: Array<number | null>;
ddrUsed: Array<number | null>;
ddrTotal: Array<number | null>;
hbmUsed: Array<number | null>;
hbmTotal: Array<number | null>;
ddrBandwidth: Array<number | null>;
hbmBandwidth: Array<number | null>;
memoryBandwidth: Array<number | null>;
mediaFrequency: Array<number | null>;
hugepagesUsed: Array<number | null>;
hugepagesTotal: Array<number | null>;
bucketSeconds: number;
sampleCount: number;
memoryTemperatureValue: Array<number | null>;
frequencyValue: Array<number | null>;
memoryFrequencyValue: Array<number | null>;
date: Array<Date>;
gpuValue: Array<number>;
temperatureValue: Array<number>;
powerTotal: Array<number>;
powerUsed: Array<number>;
powerPercent: Array<number>;
memoryTotal: Array<number>;
memoryUsed: Array<number>;
memoryPercent: Array<number>;
speedValue: Array<number>;
processCount: Array<number>;
gpuValue: Array<number | null>;
temperatureValue: Array<number | null>;
powerTotal: Array<number | null>;
powerUsed: Array<number | null>;
powerPercent: Array<number | null>;
memoryTotal: Array<number | null>;
memoryUsed: Array<number | null>;
memoryPercent: Array<number | null>;
speedValue: Array<number | null>;
processCount: Array<number | null>;
gpuProcesses: Array<Array<GPUProcess>>;
}
export interface GPUProcess {
@@ -132,12 +201,15 @@ export namespace AI {
}
interface Xpu {
tiles?: { tileID: number; stats: Stats }[];
processStatus?: string;
basic: Basic;
stats: Stats;
processes: XpuProcess[];
}
interface Basic {
uuid?: string;
deviceID: number;
deviceName: string;
vendorName: string;
@@ -148,6 +220,13 @@ export namespace AI {
}
interface Stats {
mediaUtil?: string;
computeUtil?: string;
copyUtil?: string;
mediaFrequency?: string;
memoryTemperature?: string;
memoryBandwidthUtil?: string;
power: string;
gpuUtil: string;
frequency: string;
+7 -1
View File
@@ -1,4 +1,5 @@
import { CommonModel, ReqPage } from '@/api/interface';
import type { CronjobAlertTriggerMode } from '@/utils/cronjob-alert';
export namespace Alert {
export interface AlertInfo extends CommonModel {
@@ -9,22 +10,25 @@ export namespace Alert {
interval: number;
method: string;
title: string;
taskName?: string;
project: string;
status: string;
sendCount: number;
sendMethod: string[];
advancedParams: string;
alertTriggerMode?: CronjobAlertTriggerMode;
createUser?: string;
updateUser?: string;
}
export interface AlertDetail {
type: string;
subType?: string;
licenseId: string;
title: string;
project: string;
method: string;
params: string;
params: { index: string; key: string; value: string }[] | string;
}
export interface AlertUpdateStatusReq {
@@ -74,6 +78,7 @@ export namespace Alert {
project: string;
status: string;
sendCount: number;
advancedParams?: string;
}
export interface AlertUpdateReq extends CommonModel {
@@ -86,6 +91,7 @@ export namespace Alert {
project: string;
status: string;
sendCount: number;
advancedParams?: string;
}
export interface DelReq {
+4
View File
@@ -1,4 +1,5 @@
import { ReqPage } from '.';
import type { CronjobAlertTriggerMode } from '@/utils/cronjob-alert';
export namespace Cronjob {
export interface Search extends ReqPage {
@@ -62,6 +63,7 @@ export namespace Cronjob {
secret: string;
hasAlert: boolean;
alertCount: number;
alertTriggerMode?: CronjobAlertTriggerMode;
alertTitle: string;
alertMethod: string;
alertMethodItems: Array<string>;
@@ -111,6 +113,7 @@ export namespace Cronjob {
secret: string;
alertCount: number;
alertTriggerMode?: CronjobAlertTriggerMode;
alertTitle: string;
alertMethod: string;
@@ -154,6 +157,7 @@ export namespace Cronjob {
downloadAccount: string;
alertCount: number;
alertTriggerMode?: CronjobAlertTriggerMode;
}
export interface TransHelper {
name: string;
+7
View File
@@ -1,6 +1,13 @@
import { ReqPage } from '.';
export namespace Database {
export interface RedisCliStatus {
installed: boolean;
taskID: string;
status: string;
errorMsg: string;
}
export interface SearchDBWithPage {
info: string;
database: string;
+26 -21
View File
@@ -3,13 +3,12 @@ import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum';
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string) => {
return http.post<ResPage<Container.ContainerInfo>>(
`/containers/search`,
params,
TimeoutEnum.T_40S,
currentNode ? { CurrentNode: currentNode } : undefined,
);
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string, signal?: AbortSignal) => {
return http.postWithConfig<ResPage<Container.ContainerInfo>>(`/containers/search`, params, {
timeout: TimeoutEnum.T_40S,
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const listContainer = () => {
return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
@@ -44,12 +43,11 @@ export const downloadContainerFile = (params: { containerID: string; path: strin
timeout: TimeoutEnum.T_40S,
});
};
export const loadContainerStatus = (currentNode?: string) => {
return http.get<Container.ContainerStatus>(
`/containers/status`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
export const loadContainerStatus = (currentNode?: string, containersOnly = false, signal?: AbortSignal) => {
return http.get<Container.ContainerStatus>(`/containers/status`, containersOnly ? { containersOnly: true } : {}, {
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const loadResourceLimit = () => {
return http.get<Container.ResourceLimit>(`/containers/limit`);
@@ -90,12 +88,11 @@ export const containerItemStats = (containerID: string, currentNode?: string) =>
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const containerListStats = (currentNode?: string) => {
return http.get<Array<Container.ContainerListStats>>(
`/containers/list/stats`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => {
return http.get<Array<Container.ContainerListStats>>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, {
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const containerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
@@ -144,8 +141,13 @@ export const imagePull = (params: Container.ImagePull) => {
export const imagePush = (params: Container.ImagePush) => {
return http.post<string>(`/containers/image/push`, params);
};
export const imageLoad = (params: Container.ImageLoad) => {
return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M);
export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => {
return http.post(
`/containers/image/load`,
params,
TimeoutEnum.T_10M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const imageSave = (params: Container.ImageSave) => {
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
@@ -164,6 +166,9 @@ export const searchNetwork = (params: SearchWithPage) => {
export const listNetwork = () => {
return http.get<Array<Container.Options>>(`/containers/network`);
};
export const cleanNetworks = () => {
return http.post<{ taskID: string }>(`/containers/network/clean`, {});
};
export const deleteNetwork = (params: Container.BatchDelete) => {
return http.post(`/containers/network/del`, params);
};
+7 -2
View File
@@ -193,8 +193,13 @@ export const redisPersistenceConf = (type: string, database: string) => {
export const checkRedisCli = () => {
return http.get<boolean>(`/databases/redis/check`);
};
export const installRedisCli = () => {
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M);
export const loadRedisCliStatus = (node: string) => {
return http.get<Database.RedisCliStatus>(`/databases/redis/cli/status?operateNode=${encodeURIComponent(node)}`);
};
export const installRedisCli = (taskID: string, node: string) => {
return http.post<Database.RedisCliStatus>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
taskID,
});
};
export const changeRedisPassword = (database: string, password: string) => {
if (password) {
@@ -16,10 +16,12 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({ name: 'TableSearch' });
const emit = defineEmits(['search', 'update:searchName']);
const searchInfo = ref();
const route = useRoute();
const props = defineProps({
placeholder: String,
disabled: {
@@ -42,6 +44,16 @@ watch(
{ immediate: true },
);
// Also discard text typed without pressing Enter when leaving a cached tab.
watch(
() => route.path,
() => {
searchInfo.value = '';
emit('update:searchName', '');
},
{ flush: 'sync' },
);
const search = () => {
emit('update:searchName', searchInfo.value);
emit('search');
@@ -11,6 +11,7 @@
<fu-table
v-if="currentViewMode === 'table'"
v-bind="$attrs"
:data="props.data"
ref="tableRef"
@select="handleSelect"
@selection-change="handleSelectionChange"
@@ -166,16 +167,19 @@
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, useAttrs } from 'vue';
import { computed, onActivated, onMounted, ref, useAttrs } from 'vue';
import { useRoute } from 'vue-router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
import { useCardColumns } from './useCardColumns';
import { useContextMenu } from './useContextMenu';
import { useResponsivePagination } from './useResponsivePagination';
import { useTableSelection } from './useTableSelection';
import { useTablePageState } from './useTablePageState';
const slots = useSlots();
const attrs = useAttrs();
const { isMobile, openMenuTabs } = useGlobalStore();
const route = useRoute();
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
defineOptions({ name: 'ComplexTable' });
export type DropdownProps = FuTableOperationButton;
@@ -184,6 +188,8 @@ type ViewMode = 'table' | 'card';
const props = defineProps({
header: String,
data: { type: Array as PropType<any[]>, default: () => [] },
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
paginationConfig: {
type: Object,
required: false,
@@ -239,7 +245,7 @@ const selectionColumn = computed(() =>
columnNodes.value.find((column) => (column.props as Record<string, any> | null)?.type === 'selection'),
);
const tableData = computed(() => {
const data = attrs.data;
const data = props.data;
return Array.isArray(data) ? data : [];
});
const cardContentStyle = computed(() => {
@@ -295,7 +301,7 @@ const {
isDisabled: isRightButtonDisabled,
click: rightButtonClick,
} = useContextMenu(() => props.rightButtons);
const getTableData = () => (Array.isArray(attrs.data) ? attrs.data : []);
const getTableData = () => props.data || [];
const isRowSelectable = (row: any) => {
const selectable = (selectionColumn.value?.props as Record<string, any> | null)?.selectable;
return typeof selectable === 'function' ? selectable(row) : true;
@@ -304,7 +310,6 @@ const {
selectedRows,
isRowSelected,
clearSelects,
pruneSelection,
toggleSelection,
selectRow,
syncTableSelection,
@@ -313,7 +318,18 @@ const {
handleRowClick,
handleKeyDown: handleSelectionKeyDown,
handleKeyUp,
} = useTableSelection(tableRef, getTableData, (rows) => emit('update:selects', rows), isRowSelectable);
} = useTableSelection(
tableRef,
getTableData,
(rows) => emit('update:selects', rows),
isRowSelectable,
(row) => {
const key = attrs.rowKey ?? attrs['row-key'];
if (typeof key === 'function') return row == null ? undefined : key(row);
if (typeof key === 'string') return key.split('.').reduce((value, part) => value?.[part], row);
return row?.id;
},
);
const toggleCardSelection = (row: any, selected: boolean) => {
selectRow(row, selected);
};
@@ -329,10 +345,12 @@ const handleRightClick = (row, column, event) => {
};
function currentChange() {
clearSelects();
emit('search');
}
function sizeChange() {
clearSelects();
props.paginationConfig.currentPage = 1;
localStorage.setItem(props.paginationConfig.cacheSizeKey, props.paginationConfig.pageSize);
emit('search');
@@ -446,7 +464,22 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
flush: 'post',
});
watch(tableData, pruneSelection);
useTablePageState(
() => props.paginationConfig,
() => [
route.path,
currentNode.value,
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
],
clearSelects,
);
// Kept-alive tabs do not mount again. Reload their reset query on subsequent visits.
let hasActivated = false;
onActivated(() => {
if (hasActivated) emit('search');
hasActivated = true;
});
onBeforeUnmount(() => {
window.removeEventListener('resize', calcHeight);
@@ -0,0 +1,21 @@
import { watch } from 'vue';
type Pagination = { currentPage?: number; pageSize?: number } | undefined;
/** Refreshing data leaves page state alone; only user navigation or a new query clears selection. */
export const useTablePageState = (
getPagination: () => Pagination,
getContext: () => unknown,
clearSelection: () => void,
) => {
watch(() => [getPagination()?.currentPage, getPagination()?.pageSize], clearSelection, { flush: 'sync' });
watch(
() => JSON.stringify(getContext()),
() => {
clearSelection();
const pagination = getPagination();
if (pagination) pagination.currentPage = 1;
},
{ flush: 'sync' },
);
};
@@ -1,4 +1,4 @@
import { shallowRef, ref, toRaw, type Ref } from 'vue';
import { shallowRef, ref, toRaw, watch, type Ref } from 'vue';
type TableRef = { refElTable?: any } | undefined;
@@ -7,6 +7,7 @@ export const useTableSelection = (
getTableData: () => any[],
onSelectionChange: (rows: any[]) => void,
isRowSelectable: (row: any) => boolean = () => true,
getRowKey: (row: any) => unknown = (row) => row?.id,
) => {
const selectedRows = shallowRef<any[]>([]);
const shiftPressed = ref(false);
@@ -15,8 +16,13 @@ export const useTableSelection = (
let isSyncingTableSelection = false;
let skipNextSelectionChange = false;
// Element Plus can return reactive proxies for rows supplied as plain objects.
const sameRow = (left: any, right: any) => toRaw(left) === toRaw(right);
let isRefreshingData = false;
const sameRow = (left: any, right: any) => {
if (left == null || right == null) return left === right;
const leftKey = getRowKey(left);
const rightKey = getRowKey(right);
return leftKey != null && rightKey != null ? leftKey === rightKey : toRaw(left) === toRaw(right);
};
const hasRow = (rows: any[], row: any) => rows.some((item) => sameRow(item, row));
const isRowSelected = (row: any) => hasRow(selectedRows.value, row);
const getTable = () => tableRef.value?.refElTable;
@@ -39,8 +45,12 @@ export const useTableSelection = (
const tableData = getTableData();
const nextRows = selectedRows.value.filter((row) => hasRow(tableData, row) && isRowSelectable(row));
const currentRows = table.getSelectionRows();
currentRows.filter((row) => !hasRow(nextRows, row)).forEach((row) => table.toggleRowSelection(row, false));
nextRows.filter((row) => !hasRow(currentRows, row)).forEach((row) => table.toggleRowSelection(row, true));
currentRows
.filter((row) => !nextRows.some((item) => toRaw(item) === toRaw(row)))
.forEach((row) => table.toggleRowSelection(row, false));
nextRows
.filter((row) => !currentRows.some((item) => toRaw(item) === toRaw(row)))
.forEach((row) => table.toggleRowSelection(row, true));
} finally {
isSyncingTableSelection = false;
}
@@ -73,7 +83,7 @@ export const useTableSelection = (
return true;
};
const handleSelectionChange = (rows: any[]) => {
if (isSyncingTableSelection) {
if (isSyncingTableSelection || isRefreshingData) {
return;
}
if (skipNextSelectionChange) {
@@ -104,16 +114,35 @@ export const useTableSelection = (
rangeBaseRows.value = [];
};
const pruneSelection = () => {
const nextRows = selectedRows.value.filter((row) => hasRow(getTableData(), row));
if (nextRows.length !== selectedRows.value.length) {
const data = getTableData();
const nextRows = data.filter((row) => hasRow(selectedRows.value, row) && isRowSelectable(row));
if (nextRows.length !== selectedRows.value.length || nextRows.some((row, i) => row !== selectedRows.value[i])) {
setSelectedRows(nextRows);
}
if (lastSelectedRow.value && !hasRow(nextRows, lastSelectedRow.value)) {
lastSelectedRow.value = null;
rangeBaseRows.value = [];
}
syncTableSelection();
lastSelectedRow.value = nextRows.find((row) => sameRow(row, lastSelectedRow.value)) ?? null;
rangeBaseRows.value = lastSelectedRow.value ? nextRows.filter((row) => hasRow(rangeBaseRows.value, row)) : [];
};
// Guard before the child table reacts to replacement data and emits an empty selection.
watch(
getTableData,
() => {
isRefreshingData = true;
skipNextSelectionChange = false;
pruneSelection();
},
{ flush: 'sync' },
);
watch(
getTableData,
() => {
try {
syncTableSelection();
} finally {
isRefreshingData = false;
}
},
{ flush: 'post' },
);
const toggleSelection = () => {
const selectableRows = getTableData().filter(isRowSelectable);
const allSelected = selectableRows.length > 0 && selectableRows.every(isRowSelected);
@@ -134,10 +134,11 @@ function initChart() {
series.push({
name: item?.name,
type: 'line',
itemStyle: seriesStyle[index + 2],
areaStyle: seriesStyle[index],
itemStyle: item.itemStyle ?? seriesStyle[index + 2],
areaStyle: item.areaStyle ?? seriesStyle[index],
data: item?.data,
showSymbol: false,
showSymbol: item.showSymbol ?? false,
symbolSize: item.symbolSize,
yAxisIndex: item.yAxisIndex ? 1 : null,
});
});
@@ -307,7 +308,9 @@ function initChart() {
series: series,
dataZoom: [
{
...(props.option.xData?.[0] ? { startValue: props.option.xData[0] } : {}),
...((props.option.xAxis?.min ?? props.option.xData?.[0]) != null
? { startValue: props.option.xAxis?.min ?? props.option.xData[0] }
: {}),
left: grid.left,
right: grid.right,
show: props.dataZoom,
+2 -7
View File
@@ -1,11 +1,6 @@
import { reactive, type UnwrapNestedRefs } from 'vue';
import { useRoute } from 'vue-router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { getPageState } from '@/utils/page-state-cache';
/** A new page visit starts from its defaults instead of restoring the previous page number. */
export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => {
const route = useRoute();
const { currentNode } = useGlobalStore();
const key = `${currentNode.value}:${String(route.name || route.path)}`;
return reactive(getPageState(key, factory));
return reactive(factory());
};
@@ -0,0 +1,14 @@
import { storeToRefs } from 'pinia';
import GlobalStore from '@/store/modules/global';
import router from '@/routers/router';
import { bindSearchReset } from '@/utils/search-reset';
/** Kept as a compatible entry point; searches now reset on tab, node or resource changes. */
export const useSearchPersistence = (
page: string,
fields: Parameters<typeof bindSearchReset>[1],
scope?: () => string | number | undefined,
) => {
const { currentNode } = storeToRefs(GlobalStore());
return bindSearchReset(() => [router.currentRoute.value.path, currentNode.value, page, scope?.()], fields);
};
+73 -1
View File
@@ -739,6 +739,11 @@ const message = {
keyspaceMisses: 'Number of failed attempts to find the database key.',
hit: 'Find the database key hit ratio.',
latestForkUsec: 'The number of microseconds spent on the last fork() operation.',
redisCliEnableTask: 'Enable remote Redis terminal',
redisCliEnabling: 'Enabling remote Redis terminal',
redisCliEnabled: 'Remote Redis terminal enabled',
redisCliEnableFailed: 'Failed to enable remote Redis terminal. Check the task log and retry.',
redisCliTaskSubmitted: 'Task submitted. View progress in the task center.',
redisCliHelper: '"redis-cli" not detected. Enable the service first.',
redisQuickCmd: 'Redis quick commands',
recoverHelper: 'This will overwrite data with [{0}]. Continue?',
@@ -1508,6 +1513,59 @@ const message = {
extraHeaders: 'Extra Headers',
},
gpu: {
memoryActivity: 'Memory read/write activity',
hotspotTemperature: 'Hotspot temperature',
fanRPM: 'Fan speed (RPM)',
eccCorrectable: 'Correctable errors',
eccUncorrectable: 'Uncorrectable errors',
eccPending: 'Pending ECC mode',
engineUtil: 'Engine utilization',
cpuUtil: 'CPU · utilization',
aiCore: 'AICore · utilization',
aiCPUUtil: 'AICPU · utilization',
ctrlCPUUtil: 'CtrlCPU · utilization',
mediaFrequency: 'Media · frequency',
encoderUtil: 'Encoder · utilization',
decoderUtil: 'Decoder · utilization',
jpegUtil: 'JPEG · utilization',
ofaUtil: 'OFA · utilization',
mediaUtil: 'Media · utilization',
computeUtil: 'Compute · utilization',
copyUtil: 'Copy · utilization',
ddrUsage: 'DDR · capacity',
ddrUsed: 'DDR · used',
ddrTotal: 'DDR · total',
ddrBandwidth: 'DDR · bandwidth utilization',
hbmUsage: 'HBM · capacity',
hbmUsed: 'HBM · used',
hbmTotal: 'HBM · total',
hbmBandwidth: 'HBM · bandwidth utilization',
hugepagesUsage: 'Huge pages · count',
hugepagesUsed: 'Huge pages · used',
hugepagesTotal: 'Huge pages · total',
historyAverage: 'Average',
historyPeak: 'Peak',
historyAggregated:
'{count} samples aggregated into {seconds}-second intervals; processes show counts only.',
historyRaw: '{count} original samples.',
historyMetricEmpty: 'No data for this metric in the selected time range',
historyLoadFailed: 'Failed to load history. Please retry.',
collectedAt: 'Collected at',
processUnavailable: 'Process information is unavailable',
architecture: 'Architecture',
memoryFrequency: 'Memory frequency',
memoryTemperature: 'Memory temperature',
memoryReserved: 'Reserved memory',
powerDefaultLimit: 'Default power limit',
powerMaxLimit: 'Maximum configurable power limit',
clockEvents: 'Clock limiting reasons',
memoryBandwidth: 'Memory bandwidth utilization',
legacyDevice: 'Legacy record',
legacyHistory:
'These records have no stable device ID. Their identity and missing values cannot be verified.',
pcieGeneration: 'PCIe generation (current / maximum)',
pcieWidth: 'PCIe width (current / maximum)',
gpu: 'GPU Monitoring',
gpuHelper: 'No supported GPU/XPU/NPU management command was detected. Please check and try again!',
process: 'Process Information',
@@ -1523,7 +1581,7 @@ const message = {
temperature: 'Temperature',
performanceState: 'Performance State',
powerUsage: 'Power Consumption',
memoryUsage: 'Memory Utilization',
memoryUsage: 'Memory capacity usage',
fanSpeed: 'Fan Speed',
power: 'Power',
powerCurrent: 'Current Power',
@@ -1788,6 +1846,11 @@ const message = {
imageName: 'Image name',
pull: 'Pull',
path: 'Path',
imageImportSource: 'Import source',
imageUploadDrop: 'Drop an image file here, or click to upload',
imageUploadHelper:
'Any file format is allowed. Docker validates the contents. Uploaded files are retained for retrying imports or manual deletion.',
imageImportSubmitted: 'Image import task submitted. Check the task log for the result.',
importImage: 'Import',
buildArgs: 'Build Arguments',
imageBuild: 'Build',
@@ -6723,6 +6786,15 @@ const message = {
snapshot: 'System Snapshot',
clamsRulesHelper: 'Virus scanning tasks that require alerts',
cronJobRulesHelper: 'This type of scheduled task needs to be configured',
alertTriggerMode: 'Notify on',
alertTriggerFailed: 'Failure only',
alertTriggerSuccess: 'Success only',
alertTriggerBoth: 'Success or failure',
alertNotificationHelper:
'Notify after the final execution result, not during retries. Success and failure notifications share the daily limit and sending hours.',
notificationTitle: 'Scheduled task - {0} 「{1}」 execution notification',
notificationSuccessTitle: 'Scheduled task - {0} 「{1}」 completed successfully',
notificationRule: '{0}: {1}, up to {2} notifications per day',
clamsTitle: 'Virus scan task 「 {0} 」 detected infected file alert',
cronJobAppTitle: 'Cronjob - Backup App 「 {0} 」 Task Failure Alert',
cronJobWebsiteTitle: 'Cronjob - Backup Website「 {0} 」Task Failure Alert',
+63 -1
View File
@@ -1517,6 +1517,59 @@ const message = {
extraHeaders: 'Encabezados adicionales',
},
gpu: {
memoryActivity: 'Actividad de lectura/escritura de memoria',
hotspotTemperature: 'Temperatura del punto caliente',
fanRPM: 'Velocidad del ventilador (RPM)',
eccCorrectable: 'Errores corregibles',
eccUncorrectable: 'Errores no corregibles',
eccPending: 'Modo ECC pendiente',
engineUtil: 'Utilización de motores',
cpuUtil: 'CPU · utilización',
aiCore: 'AICore · utilización',
aiCPUUtil: 'AICPU · utilización',
ctrlCPUUtil: 'CtrlCPU · utilización',
mediaFrequency: 'Medios · frecuencia',
encoderUtil: 'Codificador · utilización',
decoderUtil: 'Decodificador · utilización',
jpegUtil: 'JPEG · utilización',
ofaUtil: 'OFA · utilización',
mediaUtil: 'Medios · utilización',
computeUtil: 'Cómputo · utilización',
copyUtil: 'Copia · utilización',
ddrUsage: 'DDR · capacidad',
ddrUsed: 'DDR · usado',
ddrTotal: 'DDR · total',
ddrBandwidth: 'DDR · utilización del ancho de banda',
hbmUsage: 'HBM · capacidad',
hbmUsed: 'HBM · usado',
hbmTotal: 'HBM · total',
hbmBandwidth: 'HBM · utilización del ancho de banda',
hugepagesUsage: 'Páginas grandes · cantidad',
hugepagesUsed: 'Páginas grandes · usado',
hugepagesTotal: 'Páginas grandes · total',
historyAverage: 'Promedio',
historyPeak: 'Máximo',
historyAggregated:
'{count} muestras agrupadas en intervalos de {seconds} segundos; los procesos solo muestran cantidades.',
historyRaw: '{count} muestras originales.',
historyMetricEmpty: 'No hay datos de esta métrica en el período seleccionado',
historyLoadFailed: 'No se pudo cargar el historial. Inténtelo de nuevo.',
collectedAt: 'Fecha de recopilación',
processUnavailable: 'Información de procesos no disponible',
architecture: 'Arquitectura',
memoryFrequency: 'Frecuencia de memoria',
memoryTemperature: 'Temperatura de memoria',
memoryReserved: 'Memoria reservada',
powerDefaultLimit: 'Límite de potencia predeterminado',
powerMaxLimit: 'Potencia máxima configurable',
clockEvents: 'Motivos de limitación de frecuencia',
memoryBandwidth: 'Uso del ancho de banda de memoria',
legacyDevice: 'Registro antiguo',
legacyHistory:
'Estos registros no tienen un ID de dispositivo estable. No se pueden verificar su identidad ni los valores ausentes.',
pcieGeneration: 'Generación PCIe (actual / máxima)',
pcieWidth: 'Ancho PCIe (actual / máximo)',
gpu: 'Monitoreo de GPU',
gpuHelper:
'No se detectó ningún comando de administración de GPU/XPU/NPU compatible. ¡Compruebe e inténtelo de nuevo!',
@@ -1532,7 +1585,7 @@ const message = {
temperature: 'Temperatura',
performanceState: 'Estado de Rendimiento',
powerUsage: 'Consumo de Energía',
memoryUsage: 'Utilización de Memoria',
memoryUsage: 'Uso de capacidad de memoria',
fanSpeed: 'Velocidad del Ventilador',
power: 'Energía',
powerCurrent: 'Energía Actual',
@@ -6835,6 +6888,15 @@ const message = {
snapshot: 'Snapshot del sistema',
clamsRulesHelper: 'Tareas de escaneo antivirus que requieren alerta',
cronJobRulesHelper: 'Este tipo de tareas programadas necesita configuración',
alertTriggerMode: 'Notificar cuando',
alertTriggerFailed: 'Solo falle',
alertTriggerSuccess: 'Solo tenga éxito',
alertTriggerBoth: 'Tenga éxito o falle',
alertNotificationHelper:
'Notificar al finalizar la ejecución, no durante los reintentos. Las notificaciones de éxito y fallo comparten el límite diario y el horario de envío.',
notificationTitle: 'Tarea programada - {0} 「{1}」 notificación de ejecución',
notificationSuccessTitle: 'Tarea programada - {0} 「{1}」 completada correctamente',
notificationRule: '{0}: {1}, hasta {2} notificaciones al día',
clamsTitle: 'Tarea antivirus 「 {0} 」 detectó archivo infectado',
cronJobAppTitle: 'Cronjob - Backup de app 「 {0} 」 falló',
cronJobWebsiteTitle: 'Cronjob - Backup de sitio 「 {0} 」 falló',
+62 -1
View File
@@ -1493,6 +1493,58 @@ const message = {
extraHeaders: 'هدرهای اضافی',
},
gpu: {
memoryActivity: 'فعالیت خواندن/نوشتن حافظه',
hotspotTemperature: 'دمای نقطه داغ',
fanRPM: 'سرعت فن (RPM)',
eccCorrectable: 'خطاهای قابل اصلاح',
eccUncorrectable: 'خطاهای غیرقابل اصلاح',
eccPending: 'حالت ECC در انتظار اعمال',
engineUtil: 'میزان استفاده از موتور',
cpuUtil: 'CPU · میزان استفاده',
aiCore: 'AICore · میزان استفاده',
aiCPUUtil: 'AICPU · میزان استفاده',
ctrlCPUUtil: 'CtrlCPU · میزان استفاده',
mediaFrequency: 'رسانه · فرکانس',
encoderUtil: 'رمزگذار · میزان استفاده',
decoderUtil: 'رمزگشا · میزان استفاده',
jpegUtil: 'JPEG · میزان استفاده',
ofaUtil: 'OFA · میزان استفاده',
mediaUtil: 'رسانه · میزان استفاده',
computeUtil: 'محاسبه · میزان استفاده',
copyUtil: 'کپی · میزان استفاده',
ddrUsage: 'DDR · ظرفیت',
ddrUsed: 'DDR · استفاده‌شده',
ddrTotal: 'DDR · کل',
ddrBandwidth: 'DDR · میزان استفاده از پهنای باند',
hbmUsage: 'HBM · ظرفیت',
hbmUsed: 'HBM · استفاده‌شده',
hbmTotal: 'HBM · کل',
hbmBandwidth: 'HBM · میزان استفاده از پهنای باند',
hugepagesUsage: 'صفحات بزرگ · تعداد',
hugepagesUsed: 'صفحات بزرگ · استفاده‌شده',
hugepagesTotal: 'صفحات بزرگ · کل',
historyAverage: 'میانگین',
historyPeak: 'بیشینه',
historyAggregated:
'{count} نمونه در بازه‌های {seconds} ثانیه‌ای تجمیع شده‌اند؛ فقط تعداد فرایندها نمایش داده می‌شود.',
historyRaw: '{count} نمونه اصلی.',
historyMetricEmpty: 'در بازه زمانی انتخاب‌شده داده‌ای برای این شاخص وجود ندارد',
historyLoadFailed: 'بارگیری سابقه ناموفق بود. دوباره تلاش کنید.',
collectedAt: 'زمان گردآوری',
processUnavailable: 'اطلاعات فرایند در دسترس نیست',
architecture: 'معماری',
memoryFrequency: 'فرکانس حافظه',
memoryTemperature: 'دمای حافظه',
memoryReserved: 'حافظه رزروشده',
powerDefaultLimit: 'محدودیت توان پیش‌فرض',
powerMaxLimit: 'حداکثر توان قابل تنظیم',
clockEvents: 'دلایل محدودیت فرکانس',
memoryBandwidth: 'میزان استفاده از پهنای باند حافظه',
legacyDevice: 'رکورد قدیمی',
legacyHistory: 'این رکوردها شناسه ثابت دستگاه ندارند. هویت دستگاه و مقادیر مفقود قابل تأیید نیستند.',
pcieGeneration: 'نسل PCIe (فعلی / حداکثر)',
pcieWidth: 'عرض PCIe (فعلی / حداکثر)',
gpu: 'نظارت بر GPU',
gpuHelper: 'هیچ دستور مدیریت GPU/XPU/NPU پشتیبانی‌شده‌ای شناسایی نشد. لطفاً بررسی و دوباره تلاش کنید!',
process: 'اطلاعات فرآیند',
@@ -1508,7 +1560,7 @@ const message = {
temperature: 'دما',
performanceState: 'وضعیت عملکرد',
powerUsage: 'مصرف برق',
memoryUsage: 'استفاده از حافظه',
memoryUsage: 'میزان اشغال ظرفیت حافظه',
fanSpeed: 'سرعت فن',
power: 'برق',
powerCurrent: 'برق فعلی',
@@ -6677,6 +6729,15 @@ const message = {
snapshot: 'تصویر لحظه‌ای سیستم',
clamsRulesHelper: 'وظایف اسکن ویروس که نیاز به هشدار دارند',
cronJobRulesHelper: 'این نوع وظیفه زمان‌بندی باید پیکربندی شود',
alertTriggerMode: 'شرط اعلان',
alertTriggerFailed: 'فقط شکست',
alertTriggerSuccess: 'فقط موفقیت',
alertTriggerBoth: 'موفقیت یا شکست',
alertNotificationHelper:
'اعلان پس از نتیجه نهایی اجرا ارسال می‌شود، نه هنگام تلاش مجدد. اعلان‌های موفقیت و شکست سقف روزانه و ساعات ارسال مشترک دارند.',
notificationTitle: 'وظیفه زمان‌بندی - {0} «{1}» اعلان اجرا',
notificationSuccessTitle: 'وظیفه زمان‌بندی - {0} «{1}» با موفقیت انجام شد',
notificationRule: '{0}: {1}، حداکثر {2} اعلان در روز',
clamsTitle: 'وظیفه اسکن ویروس « {0} » هشدار فایل آلوده شناسایی شد',
cronJobAppTitle: 'وظیفه زمان‌بندی - پشتیبان‌گیری از برنامه « {0} » هشدار شکست وظیفه',
cronJobWebsiteTitle: 'وظیفه زمان‌بندی - پشتیبان‌گیری از وب‌سایت « {0} » هشدار شکست وظیفه',
+61 -1
View File
@@ -1496,6 +1496,57 @@ const message = {
extraHeaders: '追加ヘッダー',
},
gpu: {
memoryActivity: 'メモリ読み書き稼働率',
hotspotTemperature: 'ホットスポット温度',
fanRPM: 'ファン回転数(RPM)',
eccCorrectable: '訂正可能なエラー数',
eccUncorrectable: '訂正不能なエラー数',
eccPending: '適用待ちの ECC モード',
engineUtil: 'エンジン使用率',
cpuUtil: 'CPU · 使用率',
aiCore: 'AICore · 使用率',
aiCPUUtil: 'AICPU · 使用率',
ctrlCPUUtil: 'CtrlCPU · 使用率',
mediaFrequency: 'メディア · 周波数',
encoderUtil: 'エンコーダー · 使用率',
decoderUtil: 'デコーダー · 使用率',
jpegUtil: 'JPEG · 使用率',
ofaUtil: 'OFA · 使用率',
mediaUtil: 'メディア · 使用率',
computeUtil: '計算 · 使用率',
copyUtil: 'コピー · 使用率',
ddrUsage: 'DDR · 容量',
ddrUsed: 'DDR · 使用量',
ddrTotal: 'DDR · 総容量',
ddrBandwidth: 'DDR · 帯域使用率',
hbmUsage: 'HBM · 容量',
hbmUsed: 'HBM · 使用量',
hbmTotal: 'HBM · 総容量',
hbmBandwidth: 'HBM · 帯域使用率',
hugepagesUsage: 'ヒュージページ · 数',
hugepagesUsed: 'ヒュージページ · 使用量',
hugepagesTotal: 'ヒュージページ · 総容量',
historyAverage: '平均値',
historyPeak: '最大値',
historyAggregated: '{count} 件のサンプルを {seconds} 秒間隔で集計。プロセスは件数のみ表示します。',
historyRaw: '元のサンプル {count} 件。',
historyMetricEmpty: '選択した期間にこの指標のデータはありません',
historyLoadFailed: '履歴の読み込みに失敗しました。再試行してください。',
collectedAt: '収集日時',
processUnavailable: 'プロセス情報を取得できません',
architecture: 'アーキテクチャ',
memoryFrequency: 'メモリ周波数',
memoryTemperature: 'メモリ温度',
memoryReserved: '予約済みメモリ',
powerDefaultLimit: '既定の電力制限',
powerMaxLimit: '設定可能な最大電力',
clockEvents: 'クロック制限の理由',
memoryBandwidth: 'メモリ帯域幅使用率',
legacyDevice: '旧記録',
legacyHistory: 'これらの記録には固定のデバイス ID がなく、デバイスの識別や欠損値を確認できません。',
pcieGeneration: 'PCIe 世代(現在 / 最大)',
pcieWidth: 'PCIe 幅(現在 / 最大)',
gpu: 'GPU 監視',
gpuHelper: '対応する GPU/XPU/NPU 管理コマンドが検出されませんでした。確認して再試行してください!',
process: 'プロセス情報',
@@ -1510,7 +1561,7 @@ const message = {
temperature: '温度',
performanceState: 'パフォーマンス状態',
powerUsage: '消費電力',
memoryUsage: 'メモリ使用率',
memoryUsage: 'メモリ容量使用率',
fanSpeed: 'ファン速度',
power: '電力',
powerCurrent: '現在の電力',
@@ -6717,6 +6768,15 @@ const message = {
snapshot: 'システムスナップショット',
clamsRulesHelper: 'アラートが必要なウイルススキャンタスク',
cronJobRulesHelper: 'このタイプのスケジュールタスクには設定が必要です',
alertTriggerMode: '通知条件',
alertTriggerFailed: '失敗時のみ',
alertTriggerSuccess: '成功時のみ',
alertTriggerBoth: '成功時または失敗時',
alertNotificationHelper:
'再試行中ではなく、最終的な実行結果に基づいて通知します。成功と失敗の通知は、1日の上限と送信時間帯を共有します。',
notificationTitle: '定期タスク - {0}「{1}」実行通知',
notificationSuccessTitle: '定期タスク - {0}「{1}」が正常に完了しました',
notificationRule: '{0}:{1}、1日最大 {2} 回通知',
clamsTitle: 'ウイルススキャンタスク「{0}」が感染ファイルを検出したアラート',
cronJobAppTitle: 'Cronジョブ - アプリバックアップ「{0}」タスク失敗アラート',
cronJobWebsiteTitle: 'Cronジョブ - ウェブサイトバックアップ「{0}」タスク失敗アラート',
+61 -1
View File
@@ -1482,6 +1482,57 @@ const message = {
extraHeaders: '추가 헤더',
},
gpu: {
memoryActivity: '메모리 읽기/쓰기 활성률',
hotspotTemperature: '핫스폿 온도',
fanRPM: '팬 회전 속도 (RPM)',
eccCorrectable: '수정 가능한 오류 수',
eccUncorrectable: '수정 불가능한 오류 수',
eccPending: '적용 대기 중인 ECC 모드',
engineUtil: '엔진 사용률',
cpuUtil: 'CPU · 사용률',
aiCore: 'AICore · 사용률',
aiCPUUtil: 'AICPU · 사용률',
ctrlCPUUtil: 'CtrlCPU · 사용률',
mediaFrequency: '미디어 · 주파수',
encoderUtil: '인코더 · 사용률',
decoderUtil: '디코더 · 사용률',
jpegUtil: 'JPEG · 사용률',
ofaUtil: 'OFA · 사용률',
mediaUtil: '미디어 · 사용률',
computeUtil: '컴퓨팅 · 사용률',
copyUtil: '복사 · 사용률',
ddrUsage: 'DDR · 용량',
ddrUsed: 'DDR · 사용량',
ddrTotal: 'DDR · 총량',
ddrBandwidth: 'DDR · 대역폭 사용률',
hbmUsage: 'HBM · 용량',
hbmUsed: 'HBM · 사용량',
hbmTotal: 'HBM · 총량',
hbmBandwidth: 'HBM · 대역폭 사용률',
hugepagesUsage: '대용량 페이지 · 수',
hugepagesUsed: '대용량 페이지 · 사용량',
hugepagesTotal: '대용량 페이지 · 총량',
historyAverage: '평균',
historyPeak: '최댓값',
historyAggregated: '샘플 {count}개를 {seconds}초 간격으로 집계합니다. 프로세스는 개수만 표시합니다.',
historyRaw: '원본 샘플 {count}개.',
historyMetricEmpty: '선택한 기간에 이 지표의 데이터가 없습니다',
historyLoadFailed: '기록을 불러오지 못했습니다. 다시 시도하세요.',
collectedAt: '수집 시간',
processUnavailable: '프로세스 정보를 사용할 수 없습니다',
architecture: '아키텍처',
memoryFrequency: '메모리 주파수',
memoryTemperature: '메모리 온도',
memoryReserved: '예약된 메모리',
powerDefaultLimit: '기본 전력 제한',
powerMaxLimit: '설정 가능한 최대 전력',
clockEvents: '클럭 제한 이유',
memoryBandwidth: '메모리 대역폭 사용률',
legacyDevice: '이전 기록',
legacyHistory: '이 기록에는 고정 장치 ID가 없어 장치 식별 및 누락 값을 확인할 수 없습니다.',
pcieGeneration: 'PCIe 세대 (현재 / 최대)',
pcieWidth: 'PCIe 너비 (현재 / 최대)',
gpu: 'GPU 모니터링',
gpuHelper: '지원되는 GPU/XPU/NPU 관리 명령을 감지하지 못했습니다. 확인하고 다시 시도하세요!',
process: '프로세스 정보',
@@ -1496,7 +1547,7 @@ const message = {
temperature: '온도',
performanceState: '성능 상태',
powerUsage: '전력 소비',
memoryUsage: '메모리 사용률',
memoryUsage: '메모리 용량 사용률',
fanSpeed: '팬 속도',
power: '전력',
powerCurrent: '현재 전력',
@@ -6582,6 +6633,15 @@ const message = {
snapshot: '시스템 스냅샷',
clamsRulesHelper: '알림이 필요한 바이러스 검사 작업',
cronJobRulesHelper: '이 유형의 예약된 작업은 구성해야 합니다',
alertTriggerMode: '알림 조건',
alertTriggerFailed: '실패 시에만',
alertTriggerSuccess: '성공 시에만',
alertTriggerBoth: '성공 또는 실패 시',
alertNotificationHelper:
'재시도 중이 아닌 최종 실행 결과에 따라 알립니다. 성공 및 실패 알림은 일일 한도와 전송 시간대를 공유합니다.',
notificationTitle: '예약 작업 - {0} 「{1}」 실행 알림',
notificationSuccessTitle: '예약 작업 - {0} 「{1}」 실행 성공',
notificationRule: '{0}: {1}, 하루 최대 {2}회 알림',
clamsTitle: '바이러스 검사 작업 「 {0} 」 감염된 파일 알림',
cronJobAppTitle: '크론 작업 - 백업 애플리케이션 「 {0} 」 작업 실패 알림',
cronJobWebsiteTitle: '크론 작업 - 백업 웹사이트「 {0} 」작업 실패 알림',
+61 -1
View File
@@ -1475,6 +1475,57 @@ const message = {
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
gpu: {
memoryActivity: 'ກິດຈະກຳອ່ານ/ຂຽນໜ່ວຍຄວາມຈຳ',
hotspotTemperature: 'ອຸນຫະພູມຈຸດຮ້ອນ',
fanRPM: 'ຄວາມໄວພັດລົມ (RPM)',
eccCorrectable: 'ຂໍ້ຜິດພາດທີ່ແກ້ໄຂໄດ້',
eccUncorrectable: 'ຂໍ້ຜິດພາດທີ່ແກ້ໄຂບໍ່ໄດ້',
eccPending: 'ໂໝດ ECC ທີ່ລໍຖ້າ',
engineUtil: 'ອັດຕາການໃຊ້ເຄື່ອງຈັກ',
cpuUtil: 'CPU · ອັດຕາການໃຊ້',
aiCore: 'AICore · ອັດຕາການໃຊ້',
aiCPUUtil: 'AICPU · ອັດຕາການໃຊ້',
ctrlCPUUtil: 'CtrlCPU · ອັດຕາການໃຊ້',
mediaFrequency: 'ສື່ · ຄວາມຖີ່',
encoderUtil: 'ຕົວເຂົ້າລະຫັດ · ອັດຕາການໃຊ້',
decoderUtil: 'ຕົວຖອດລະຫັດ · ອັດຕາການໃຊ້',
jpegUtil: 'JPEG · ອັດຕາການໃຊ້',
ofaUtil: 'OFA · ອັດຕາການໃຊ້',
mediaUtil: 'ສື່ · ອັດຕາການໃຊ້',
computeUtil: 'ຄຳນວນ · ອັດຕາການໃຊ້',
copyUtil: 'ສຳເນົາ · ອັດຕາການໃຊ້',
ddrUsage: 'DDR · ຄວາມຈຸ',
ddrUsed: 'DDR · ໃຊ້ແລ້ວ',
ddrTotal: 'DDR · ທັງໝົດ',
ddrBandwidth: 'DDR · ອັດຕາການໃຊ້ແບນວິດ',
hbmUsage: 'HBM · ຄວາມຈຸ',
hbmUsed: 'HBM · ໃຊ້ແລ້ວ',
hbmTotal: 'HBM · ທັງໝົດ',
hbmBandwidth: 'HBM · ອັດຕາການໃຊ້ແບນວິດ',
hugepagesUsage: 'ໜ້າຂະໜາດໃຫຍ່ · ຈຳນວນ',
hugepagesUsed: 'ໜ້າຂະໜາດໃຫຍ່ · ໃຊ້ແລ້ວ',
hugepagesTotal: 'ໜ້າຂະໜາດໃຫຍ່ · ທັງໝົດ',
historyAverage: 'ຄ່າສະເລ່ຍ',
historyPeak: 'ຄ່າສູງສຸດ',
historyAggregated: 'ລວມ {count} ຕົວຢ່າງເປັນຊ່ວງ {seconds} ວິນາທີ; ສະແດງພຽງຈຳນວນໂປຣເຊສ.',
historyRaw: '{count} ຕົວຢ່າງຕົ້ນສະບັບ.',
historyMetricEmpty: 'ບໍ່ມີຂໍ້ມູນຕົວຊີ້ວັດນີ້ໃນຊ່ວງເວລາທີ່ເລືອກ',
historyLoadFailed: 'ໂຫຼດປະຫວັດບໍ່ສຳເລັດ. ກະລຸນາລອງໃໝ່.',
collectedAt: 'ເວລາເກັບຂໍ້ມູນ',
processUnavailable: 'ບໍ່ມີຂໍ້ມູນໂປຣເຊສ',
architecture: 'ສະຖາປັດຕະຍະກຳ',
memoryFrequency: 'ຄວາມຖີ່ໜ່ວຍຄວາມຈຳ',
memoryTemperature: 'ອຸນຫະພູມໜ່ວຍຄວາມຈຳ',
memoryReserved: 'ໜ່ວຍຄວາມຈຳທີ່ສະຫງວນ',
powerDefaultLimit: 'ຂີດຈຳກັດພະລັງງານເລີ່ມຕົ້ນ',
powerMaxLimit: 'ພະລັງງານສູງສຸດທີ່ຕັ້ງໄດ້',
clockEvents: 'ເຫດຜົນທີ່ຈຳກັດຄວາມຖີ່',
memoryBandwidth: 'ການໃຊ້ແບນວິດໜ່ວຍຄວາມຈຳ',
legacyDevice: 'ບັນທຶກເກົ່າ',
legacyHistory: 'ບັນທຶກເຫຼົ່ານີ້ບໍ່ມີ ID ອຸປະກອນຄົງທີ່. ບໍ່ສາມາດຢືນຢັນອຸປະກອນ ແລະ ຄ່າທີ່ຂາດຫາຍໄດ້.',
pcieGeneration: 'ລຸ້ນ PCIe (ປັດຈຸບັນ / ສູງສຸດ)',
pcieWidth: 'ຄວາມກວ້າງ PCIe (ປັດຈຸບັນ / ສູງສຸດ)',
gpu: 'ຕິດຕາມ GPU',
gpuHelper: 'ກວດບໍ່ພົບຄຳສັ່ງຈັດການ GPU/XPU/NPU ທີ່ຮອງຮັບ. ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່!',
process: 'ຂໍ້ມູນໂປຣເຊສ',
@@ -1490,7 +1541,7 @@ const message = {
temperature: 'ອຸນຫະພູມ',
performanceState: 'ສະຖານະປະສິດທິພາບ',
powerUsage: 'ການໃຊ້ພະລັງງານ',
memoryUsage: 'ການໃຊ້ໜ່ວຍຄວາມຈຳ',
memoryUsage: 'ການໃຊ້ຄວາມຈຸໜ່ວຍຄວາມຈຳ',
fanSpeed: 'ຄວາມໄວພັດລົມ',
power: 'ພະລັງງານ',
powerCurrent: 'ພະລັງງານປັດຈຸບັນ',
@@ -6493,6 +6544,15 @@ const message = {
snapshot: 'System Snapshot',
clamsRulesHelper: 'ງານສະແກນໄວຣັສທີ່ຕ້ອງການແຈ້ງເຕືອນ',
cronJobRulesHelper: 'ຕ້ອງຕັ້ງຄ່າງານ Cronjob ປະເພດນີ້',
alertTriggerMode: 'ເງື່ອນໄຂແຈ້ງເຕືອນ',
alertTriggerFailed: 'ເມື່ອລົ້ມເຫຼວເທົ່ານັ້ນ',
alertTriggerSuccess: 'ເມື່ອສຳເລັດເທົ່ານັ້ນ',
alertTriggerBoth: 'ເມື່ອສຳເລັດ ຫຼື ລົ້ມເຫຼວ',
alertNotificationHelper:
'ແຈ້ງເຕືອນຕາມຜົນສຸດທ້າຍ ບໍ່ແມ່ນລະຫວ່າງລອງໃໝ່. ການແຈ້ງເຕືອນສຳເລັດ ແລະ ລົ້ມເຫຼວໃຊ້ຂີດຈຳກັດລາຍວັນ ແລະ ເວລາສົ່ງຮ່ວມກັນ.',
notificationTitle: 'ວຽກຕາມກຳນົດເວລາ - {0} 「{1}」 ແຈ້ງເຕືອນການເຮັດວຽກ',
notificationSuccessTitle: 'ວຽກຕາມກຳນົດເວລາ - {0} 「{1}」 ສຳເລັດແລ້ວ',
notificationRule: '{0}: {1}, ສູງສຸດ {2} ຄັ້ງຕໍ່ມື້',
clamsTitle: 'ງານສະແກນໄວຣັສ 「 {0} 」 ກວດພົບໄຟລ໌ອັນຕະລາຍ',
cronJobAppTitle: 'ແຈ້ງເຕືອນງານສຳຮອງແອັບ 「 {0} 」 ລົ້ມເຫຼວ',
cronJobWebsiteTitle: 'ແຈ້ງເຕືອນງານສຳຮອງເວັບໄຊ 「 {0} 」 ລົ້ມເຫຼວ',
+63 -1
View File
@@ -1522,6 +1522,59 @@ const message = {
extraHeaders: 'Pengepala Tambahan',
},
gpu: {
memoryActivity: 'Aktiviti baca/tulis memori',
hotspotTemperature: 'Suhu titik panas',
fanRPM: 'Kelajuan kipas (RPM)',
eccCorrectable: 'Ralat boleh dibetulkan',
eccUncorrectable: 'Ralat tidak boleh dibetulkan',
eccPending: 'Mod ECC menunggu',
engineUtil: 'Penggunaan enjin',
cpuUtil: 'CPU · penggunaan',
aiCore: 'AICore · penggunaan',
aiCPUUtil: 'AICPU · penggunaan',
ctrlCPUUtil: 'CtrlCPU · penggunaan',
mediaFrequency: 'Media · frekuensi',
encoderUtil: 'Pengekod · penggunaan',
decoderUtil: 'Penyahkod · penggunaan',
jpegUtil: 'JPEG · penggunaan',
ofaUtil: 'OFA · penggunaan',
mediaUtil: 'Media · penggunaan',
computeUtil: 'Pengiraan · penggunaan',
copyUtil: 'Salinan · penggunaan',
ddrUsage: 'DDR · kapasiti',
ddrUsed: 'DDR · digunakan',
ddrTotal: 'DDR · jumlah',
ddrBandwidth: 'DDR · penggunaan lebar jalur',
hbmUsage: 'HBM · kapasiti',
hbmUsed: 'HBM · digunakan',
hbmTotal: 'HBM · jumlah',
hbmBandwidth: 'HBM · penggunaan lebar jalur',
hugepagesUsage: 'Halaman besar · bilangan',
hugepagesUsed: 'Halaman besar · digunakan',
hugepagesTotal: 'Halaman besar · jumlah',
historyAverage: 'Purata',
historyPeak: 'Puncak',
historyAggregated:
'{count} sampel diagregatkan dalam sela {seconds} saat; proses hanya memaparkan bilangan.',
historyRaw: '{count} sampel asal.',
historyMetricEmpty: 'Tiada data untuk metrik ini dalam julat masa yang dipilih',
historyLoadFailed: 'Gagal memuatkan sejarah. Sila cuba lagi.',
collectedAt: 'Masa pengumpulan',
processUnavailable: 'Maklumat proses tidak tersedia',
architecture: 'Seni bina',
memoryFrequency: 'Frekuensi memori',
memoryTemperature: 'Suhu memori',
memoryReserved: 'Memori terpelihara',
powerDefaultLimit: 'Had kuasa lalai',
powerMaxLimit: 'Kuasa maksimum boleh ditetapkan',
clockEvents: 'Sebab had frekuensi',
memoryBandwidth: 'Penggunaan lebar jalur memori',
legacyDevice: 'Rekod lama',
legacyHistory:
'Rekod ini tiada ID peranti yang tetap. Identiti peranti dan nilai yang hilang tidak dapat disahkan.',
pcieGeneration: 'Generasi PCIe (semasa / maksimum)',
pcieWidth: 'Lebar PCIe (semasa / maksimum)',
gpu: 'Pemantauan GPU',
gpuHelper: 'Tiada arahan pengurusan GPU/XPU/NPU yang disokong dikesan. Sila periksa dan cuba lagi!',
process: 'Maklumat Proses',
@@ -1536,7 +1589,7 @@ const message = {
temperature: 'Suhu',
performanceState: 'Status Prestasi',
powerUsage: 'Penggunaan Kuasa',
memoryUsage: 'Penggunaan Memori',
memoryUsage: 'Penggunaan kapasiti memori',
fanSpeed: 'Kelajuan Kipas',
power: 'Kuasa',
powerCurrent: 'Kuasa Semasa',
@@ -6833,6 +6886,15 @@ const message = {
snapshot: 'Snapshot Sistem',
clamsRulesHelper: 'Tugas imbasan virus yang memerlukan amaran',
cronJobRulesHelper: 'Jenis tugas berjadual ini perlu dikonfigurasikan',
alertTriggerMode: 'Syarat pemberitahuan',
alertTriggerFailed: 'Gagal sahaja',
alertTriggerSuccess: 'Berjaya sahaja',
alertTriggerBoth: 'Berjaya atau gagal',
alertNotificationHelper:
'Maklumkan selepas keputusan akhir pelaksanaan, bukan semasa percubaan semula. Pemberitahuan berjaya dan gagal berkongsi had harian dan waktu penghantaran.',
notificationTitle: 'Tugas berjadual - {0} 「{1}」 pemberitahuan pelaksanaan',
notificationSuccessTitle: 'Tugas berjadual - {0} 「{1}」 berjaya diselesaikan',
notificationRule: '{0}: {1}, sehingga {2} pemberitahuan sehari',
clamsTitle: 'Tugas imbasan virus 「 {0} 」 mengesan amaran fail dijangkiti',
cronJobAppTitle: 'Cronjob - Sandaran Aplikasi 「 {0} 」 Amaran Kegagalan Tugas',
cronJobWebsiteTitle: 'Cronjob - Sandaran Laman Web 「 {0} 」 Amaran Kegagalan Tugas',
+63 -1
View File
@@ -1518,6 +1518,59 @@ const message = {
extraHeaders: 'Cabeçalhos extras',
},
gpu: {
memoryActivity: 'Atividade de leitura/escrita da memória',
hotspotTemperature: 'Temperatura do ponto quente',
fanRPM: 'Rotação da ventoinha (RPM)',
eccCorrectable: 'Erros corrigíveis',
eccUncorrectable: 'Erros incorrigíveis',
eccPending: 'Modo ECC pendente',
engineUtil: 'Utilização dos motores',
cpuUtil: 'CPU · utilização',
aiCore: 'AICore · utilização',
aiCPUUtil: 'AICPU · utilização',
ctrlCPUUtil: 'CtrlCPU · utilização',
mediaFrequency: 'Mídia · frequência',
encoderUtil: 'Codificador · utilização',
decoderUtil: 'Decodificador · utilização',
jpegUtil: 'JPEG · utilização',
ofaUtil: 'OFA · utilização',
mediaUtil: 'Mídia · utilização',
computeUtil: 'Computação · utilização',
copyUtil: 'Cópia · utilização',
ddrUsage: 'DDR · capacidade',
ddrUsed: 'DDR · usado',
ddrTotal: 'DDR · total',
ddrBandwidth: 'DDR · utilização da largura de banda',
hbmUsage: 'HBM · capacidade',
hbmUsed: 'HBM · usado',
hbmTotal: 'HBM · total',
hbmBandwidth: 'HBM · utilização da largura de banda',
hugepagesUsage: 'Páginas grandes · quantidade',
hugepagesUsed: 'Páginas grandes · usado',
hugepagesTotal: 'Páginas grandes · total',
historyAverage: 'Média',
historyPeak: 'Pico',
historyAggregated:
'{count} amostras agregadas em intervalos de {seconds} segundos; processos exibem apenas contagens.',
historyRaw: '{count} amostras originais.',
historyMetricEmpty: 'Sem dados desta métrica no período selecionado',
historyLoadFailed: 'Falha ao carregar o histórico. Tente novamente.',
collectedAt: 'Data da coleta',
processUnavailable: 'Informações de processos indisponíveis',
architecture: 'Arquitetura',
memoryFrequency: 'Frequência da memória',
memoryTemperature: 'Temperatura da memória',
memoryReserved: 'Memória reservada',
powerDefaultLimit: 'Limite de potência padrão',
powerMaxLimit: 'Potência máxima configurável',
clockEvents: 'Motivos de limitação de frequência',
memoryBandwidth: 'Uso da largura de banda da memória',
legacyDevice: 'Registro antigo',
legacyHistory:
'Estes registros não têm um ID de dispositivo estável. Não é possível verificar a identidade nem os valores ausentes.',
pcieGeneration: 'Geração PCIe (atual / máxima)',
pcieWidth: 'Largura PCIe (atual / máxima)',
gpu: 'Monitoramento de GPU',
gpuHelper:
'Nenhum comando de gerenciamento de GPU/XPU/NPU compatível foi detectado. Verifique e tente novamente!',
@@ -1533,7 +1586,7 @@ const message = {
temperature: 'Temperatura',
performanceState: 'Estado de Desempenho',
powerUsage: 'Consumo de Energia',
memoryUsage: 'Utilização de Memória',
memoryUsage: 'Uso da capacidade de memória',
fanSpeed: 'Velocidade do Ventilador',
power: 'Energia',
powerCurrent: 'Energia Atual',
@@ -6867,6 +6920,15 @@ const message = {
snapshot: 'Instantâneo do Sistema',
clamsRulesHelper: 'Tarefas de varredura de vírus que exigem alertas por',
cronJobRulesHelper: 'Este tipo de tarefa cron precisa ser configurado',
alertTriggerMode: 'Notificar em caso de',
alertTriggerFailed: 'Apenas falha',
alertTriggerSuccess: 'Apenas sucesso',
alertTriggerBoth: 'Sucesso ou falha',
alertNotificationHelper:
'Notificar após o resultado final, não durante as novas tentativas. As notificações de sucesso e falha compartilham o limite diário e o horário de envio.',
notificationTitle: 'Tarefa agendada - {0} 「{1}」 notificação de execução',
notificationSuccessTitle: 'Tarefa agendada - {0} 「{1}」 concluída com sucesso',
notificationRule: '{0}: {1}, até {2} notificações por dia',
clamsTitle: 'Tarefa de Varredura de Vírus 「 {0} 」 detectou arquivo infectado',
cronJobAppTitle: 'Tarefa Cron - Falha no Backup do Aplicativo 「 {0} 」',
cronJobWebsiteTitle: 'Tarefa Cron - Falha no Backup do Site 「 {0} 」',
+63 -1
View File
@@ -1511,6 +1511,59 @@ const message = {
extraHeaders: 'Дополнительные заголовки',
},
gpu: {
memoryActivity: 'Активность чтения/записи памяти',
hotspotTemperature: 'Температура горячей точки',
fanRPM: 'Скорость вентилятора (об/мин)',
eccCorrectable: 'Исправимые ошибки',
eccUncorrectable: 'Неисправимые ошибки',
eccPending: 'Ожидающий режим ECC',
engineUtil: 'Загрузка движков',
cpuUtil: 'CPU · загрузка',
aiCore: 'AICore · загрузка',
aiCPUUtil: 'AICPU · загрузка',
ctrlCPUUtil: 'CtrlCPU · загрузка',
mediaFrequency: 'Медиа · частота',
encoderUtil: 'Кодировщик · загрузка',
decoderUtil: 'Декодировщик · загрузка',
jpegUtil: 'JPEG · загрузка',
ofaUtil: 'OFA · загрузка',
mediaUtil: 'Медиа · загрузка',
computeUtil: 'Вычисления · загрузка',
copyUtil: 'Копирование · загрузка',
ddrUsage: 'DDR · объём',
ddrUsed: 'DDR · использовано',
ddrTotal: 'DDR · всего',
ddrBandwidth: 'DDR · использование пропускной способности',
hbmUsage: 'HBM · объём',
hbmUsed: 'HBM · использовано',
hbmTotal: 'HBM · всего',
hbmBandwidth: 'HBM · использование пропускной способности',
hugepagesUsage: 'Большие страницы · количество',
hugepagesUsed: 'Большие страницы · использовано',
hugepagesTotal: 'Большие страницы · всего',
historyAverage: 'Среднее',
historyPeak: 'Максимум',
historyAggregated:
'{count} измерений сгруппированы по {seconds} с; для процессов показано только количество.',
historyRaw: 'Исходных измерений: {count}.',
historyMetricEmpty: 'Нет данных этой метрики за выбранный период',
historyLoadFailed: 'Не удалось загрузить историю. Повторите попытку.',
collectedAt: 'Время сбора',
processUnavailable: 'Информация о процессах недоступна',
architecture: 'Архитектура',
memoryFrequency: 'Частота памяти',
memoryTemperature: 'Температура памяти',
memoryReserved: 'Зарезервированная память',
powerDefaultLimit: 'Лимит мощности по умолчанию',
powerMaxLimit: 'Максимальная настраиваемая мощность',
clockEvents: 'Причины ограничения частоты',
memoryBandwidth: 'Использование пропускной способности памяти',
legacyDevice: 'Старая запись',
legacyHistory:
'У этих записей нет постоянного ID устройства. Нельзя проверить принадлежность устройству и отсутствующие значения.',
pcieGeneration: 'Поколение PCIe (текущее / максимальное)',
pcieWidth: 'Ширина PCIe (текущая / максимальная)',
gpu: 'Мониторинг GPU',
gpuHelper: 'Поддерживаемая команда управления GPU/XPU/NPU не обнаружена. Проверьте и повторите попытку!',
process: 'Информация о Процессе',
@@ -1525,7 +1578,7 @@ const message = {
temperature: 'Температура',
performanceState: 'Состояние Производительности',
powerUsage: 'Потребление Мощности',
memoryUsage: 'Использование Памяти',
memoryUsage: 'Использование объёма памяти',
fanSpeed: 'Скорость Вентилятора',
power: 'Мощность',
powerCurrent: 'Текущая Мощность',
@@ -6835,6 +6888,15 @@ const message = {
snapshot: 'Системный снимок',
clamsRulesHelper: 'Задачи сканирования на вирусы, требующие уведомлений',
cronJobRulesHelper: 'Этот тип планируемой задачи требует конфигурации',
alertTriggerMode: 'Условие уведомления',
alertTriggerFailed: 'Только при ошибке',
alertTriggerSuccess: 'Только при успехе',
alertTriggerBoth: 'При успехе или ошибке',
alertNotificationHelper:
'Уведомление отправляется по окончательному результату, а не во время повторных попыток. Уведомления об успехе и ошибке используют общий дневной лимит и часы отправки.',
notificationTitle: 'Плановая задача - {0} «{1}»: уведомление о выполнении',
notificationSuccessTitle: 'Плановая задача - {0} «{1}» успешно завершена',
notificationRule: '{0}: {1}, до {2} уведомлений в день',
clamsTitle: 'Задача сканирования на вирусы 「 {0} 」 обнаружила заражённый файл',
cronJobAppTitle: 'Задача Cron - ошибка задачи резервного копирования приложения 「 {0} 」',
cronJobWebsiteTitle: 'Задача Cron - ошибка задачи резервного копирования сайта 「 {0} 」',
+62 -1
View File
@@ -1518,6 +1518,58 @@ const message = {
extraHeaders: 'Ek Başlıklar',
},
gpu: {
memoryActivity: 'Bellek okuma/yazma etkinliği',
hotspotTemperature: 'Sıcak nokta sıcaklığı',
fanRPM: 'Fan hızı (RPM)',
eccCorrectable: 'Düzeltilebilir hatalar',
eccUncorrectable: 'Düzeltilemeyen hatalar',
eccPending: 'Bekleyen ECC modu',
engineUtil: 'Motor kullanımı',
cpuUtil: 'CPU · kullanım',
aiCore: 'AICore · kullanım',
aiCPUUtil: 'AICPU · kullanım',
ctrlCPUUtil: 'CtrlCPU · kullanım',
mediaFrequency: 'Medya · frekans',
encoderUtil: 'Kodlayıcı · kullanım',
decoderUtil: 'Kod çözücü · kullanım',
jpegUtil: 'JPEG · kullanım',
ofaUtil: 'OFA · kullanım',
mediaUtil: 'Medya · kullanım',
computeUtil: 'Hesaplama · kullanım',
copyUtil: 'Kopyalama · kullanım',
ddrUsage: 'DDR · kapasite',
ddrUsed: 'DDR · kullanılan',
ddrTotal: 'DDR · toplam',
ddrBandwidth: 'DDR · bant genişliği kullanımı',
hbmUsage: 'HBM · kapasite',
hbmUsed: 'HBM · kullanılan',
hbmTotal: 'HBM · toplam',
hbmBandwidth: 'HBM · bant genişliği kullanımı',
hugepagesUsage: 'Büyük sayfalar · sayı',
hugepagesUsed: 'Büyük sayfalar · kullanılan',
hugepagesTotal: 'Büyük sayfalar · toplam',
historyAverage: 'Ortalama',
historyPeak: 'En yüksek',
historyAggregated:
'{count} örnek {seconds} saniyelik aralıklarla toplandı; işlemler için yalnızca sayılar gösterilir.',
historyRaw: '{count} ham örnek.',
historyMetricEmpty: 'Seçilen zaman aralığında bu metriğe ait veri yok',
historyLoadFailed: 'Geçmiş yüklenemedi. Lütfen tekrar deneyin.',
collectedAt: 'Toplanma zamanı',
processUnavailable: 'İşlem bilgileri kullanılamıyor',
architecture: 'Mimari',
memoryFrequency: 'Bellek frekansı',
memoryTemperature: 'Bellek sıcaklığı',
memoryReserved: 'Ayrılmış bellek',
powerDefaultLimit: 'Varsayılan güç sınırı',
powerMaxLimit: 'Yapılandırılabilir en yüksek güç',
clockEvents: 'Saat frekansı sınırlama nedenleri',
memoryBandwidth: 'Bellek bant genişliği kullanımı',
legacyDevice: 'Eski kayıt',
legacyHistory: 'Bu kayıtların sabit cihaz kimliği yoktur. Cihaz kimliği ve eksik değerler doğrulanamaz.',
pcieGeneration: 'PCIe nesli (geçerli / en yüksek)',
pcieWidth: 'PCIe genişliği (geçerli / en yüksek)',
gpu: 'GPU İzleme',
gpuHelper: 'Desteklenen bir GPU/XPU/NPU yönetim komutu algılanmadı. Lütfen kontrol edip tekrar deneyin!',
process: 'İşlem Bilgisi',
@@ -1532,7 +1584,7 @@ const message = {
temperature: 'Sıcaklık',
performanceState: 'Performans Durumu',
powerUsage: 'Güç Tüketimi',
memoryUsage: 'Bellek Kullanımı',
memoryUsage: 'Bellek kapasitesi kullanımı',
fanSpeed: 'Fan Hızı',
power: 'Güç',
powerCurrent: 'Mevcut Güç',
@@ -6841,6 +6893,15 @@ const message = {
snapshot: 'Sistem Anlık Görüntüsü',
clamsRulesHelper: 'Uyarısı gerektiren virüs tarama görevleri',
cronJobRulesHelper: 'Bu tür zamanlanmış görevlerin yapılandırılması gerekir',
alertTriggerMode: 'Bildirim koşulu',
alertTriggerFailed: 'Yalnızca başarısızlık',
alertTriggerSuccess: 'Yalnızca başarı',
alertTriggerBoth: 'Başarı veya başarısızlık',
alertNotificationHelper:
'Yeniden denemeler sırasında değil, son yürütme sonucuna göre bildirim gönderilir. Başarı ve başarısızlık bildirimleri günlük sınırı ve gönderim saatlerini paylaşır.',
notificationTitle: 'Zamanlanmış görev - {0} 「{1}」 yürütme bildirimi',
notificationSuccessTitle: 'Zamanlanmış görev - {0} 「{1}」 başarıyla tamamlandı',
notificationRule: '{0}: {1}, günde en fazla {2} bildirim',
clamsTitle: 'Virüs tarama görevi 「 {0} 」 enfekte dosya algıladı uyarısı',
cronJobAppTitle: 'Zamanlanmış Görev - Yedekleme Uygulaması 「 {0} 」 Görev Başarısızlık Uyarısı',
cronJobWebsiteTitle: 'Zamanlanmış Görev - Yedekleme Web Sitesi 「 {0} 」 Görev Başarısızlık Uyarısı',
+70 -1
View File
@@ -709,6 +709,11 @@ const message = {
keyspaceMisses: '尋找資料庫鍵失敗的次數',
hit: '尋找資料庫鍵命中率',
latestForkUsec: '最近一次 fork() 操作耗費的微秒數',
redisCliEnableTask: '啟用 Redis 遠端終端',
redisCliEnabling: '正在啟用 Redis 遠端終端',
redisCliEnabled: 'Redis 遠端終端已啟用',
redisCliEnableFailed: 'Redis 遠端終端啟用失敗,請查看任務日誌後重試',
redisCliTaskSubmitted: '啟用任務已提交,可在任務中心查看進度',
redisCliHelper: '未偵測到 redis-cli 服務,請先啟用服務',
redisQuickCmd: 'Redis 快速指令',
recoverHelper: '即將使用 [{0}] 對資料進行覆蓋,是否繼續?',
@@ -1429,6 +1434,57 @@ const message = {
extraHeaders: '額外請求標頭',
},
gpu: {
memoryActivity: '顯示記憶體讀寫活躍率',
hotspotTemperature: '熱點溫度',
fanRPM: '風扇轉速(RPM)',
eccCorrectable: '可修正錯誤數',
eccUncorrectable: '不可修正錯誤數',
eccPending: '待生效 ECC 模式',
engineUtil: '引擎使用率',
cpuUtil: 'CPU · 使用率',
aiCore: 'AICore · 使用率',
aiCPUUtil: 'AICPU · 使用率',
ctrlCPUUtil: 'CtrlCPU · 使用率',
mediaFrequency: '媒體引擎 · 頻率',
encoderUtil: '編碼器 · 使用率',
decoderUtil: '解碼器 · 使用率',
jpegUtil: 'JPEG · 使用率',
ofaUtil: 'OFA · 使用率',
mediaUtil: '媒體引擎 · 使用率',
computeUtil: '計算引擎 · 使用率',
copyUtil: '複製引擎 · 使用率',
ddrUsage: 'DDR · 容量',
ddrUsed: 'DDR · 已用',
ddrTotal: 'DDR · 總量',
ddrBandwidth: 'DDR · 頻寬使用率',
hbmUsage: 'HBM · 容量',
hbmUsed: 'HBM · 已用',
hbmTotal: 'HBM · 總量',
hbmBandwidth: 'HBM · 頻寬使用率',
hugepagesUsage: '大頁 · 數量',
hugepagesUsed: '大頁 · 已用',
hugepagesTotal: '大頁 · 總量',
historyAverage: '平均值',
historyPeak: '峰值',
historyAggregated: '共 {count} 筆取樣,按 {seconds} 秒彙整;程序僅顯示統計數量。',
historyRaw: '共 {count} 筆原始取樣。',
historyMetricEmpty: '所選時間範圍內沒有此指標的資料',
historyLoadFailed: '歷史記錄載入失敗,請重試',
collectedAt: '採集時間',
processUnavailable: '程序資訊不可用',
architecture: '架構',
memoryFrequency: '顯存頻率',
memoryTemperature: '顯存溫度',
memoryReserved: '保留顯存',
powerDefaultLimit: '預設功率限制',
powerMaxLimit: '最大可設定功率',
clockEvents: '頻率限制原因',
memoryBandwidth: '顯存頻寬使用率',
legacyDevice: '舊記錄',
legacyHistory: '這些記錄沒有穩定裝置識別碼,無法確認其裝置身分和缺失值。',
pcieGeneration: 'PCIe 世代(目前 / 最大)',
pcieWidth: 'PCIe 寬度(目前 / 最大)',
gpu: 'GPU 監控',
gpuHelper: '目前系統未偵測到支援的 GPU/XPU/NPU 管理指令,請檢查後重試',
process: '行程資訊',
@@ -1443,7 +1499,7 @@ const message = {
temperature: '溫度',
performanceState: '效能狀態',
powerUsage: '功耗',
memoryUsage: '顯存使用率',
memoryUsage: '顯示記憶體容量占用率',
fanSpeed: '風扇轉速',
power: '功率',
powerCurrent: '目前功率',
@@ -1695,6 +1751,10 @@ const message = {
delInsecureHelper: '刪除授信需要重新啟動 docker 服務,是否刪除?',
pull: '拉取',
path: '路徑',
imageImportSource: '匯入方式',
imageUploadDrop: '拖曳映像檔案至此處,或點擊上傳',
imageUploadHelper: '不限制檔案格式,由 Docker 驗證內容。上傳的檔案會保留,可重試匯入或手動刪除。',
imageImportSubmitted: '映像匯入任務已提交,請查看任務日誌',
importImage: '匯入映像',
imageBuild: '構建映像',
buildArgs: '構建參數',
@@ -6238,6 +6298,15 @@ const message = {
snapshot: '系統快照',
clamsRulesHelper: '需要開啟告警的病毒掃描任務',
cronJobRulesHelper: '需要設定此類型的計劃任務',
alertTriggerMode: '通知條件',
alertTriggerFailed: '僅失敗',
alertTriggerSuccess: '僅成功',
alertTriggerBoth: '成功或失敗',
alertNotificationHelper:
'任務最終執行完成後按結果通知,重試過程中不發送;成功與失敗共用每日次數及發送時段。',
notificationTitle: '計劃任務-{0}「{1}」執行告警',
notificationSuccessTitle: '計劃任務-{0}「{1}」執行成功通知',
notificationRule: '{0}:{1},每天最多發送 {2} 次',
clamsTitle: '病毒掃描「{0}」任務偵測到感染檔案告警',
cronJobAppTitle: '計劃任務-備份應用「{0}」任務失敗告警',
cronJobWebsiteTitle: '計劃任務-備份網站「{0}」任務失敗告警',
+70 -1
View File
@@ -722,6 +722,11 @@ const message = {
keyspaceMisses: '查找数据库键失败的次数',
hit: '查找数据库键命中率',
latestForkUsec: '最近一次 fork() 操作耗费的微秒数',
redisCliEnableTask: '启用 Redis 远程终端',
redisCliEnabling: '正在启用 Redis 远程终端',
redisCliEnabled: 'Redis 远程终端已启用',
redisCliEnableFailed: 'Redis 远程终端启用失败,请查看任务日志后重试',
redisCliTaskSubmitted: '启用任务已提交,可在任务中心查看进度',
redisCliHelper: '未检测到 redis-cli 服务,请先启用服务!',
redisQuickCmd: 'Redis 快速命令',
@@ -1442,6 +1447,57 @@ const message = {
extraHeaders: '额外请求头',
},
gpu: {
memoryActivity: '显存读写活跃率',
hotspotTemperature: '热点温度',
fanRPM: '风扇转速(RPM)',
eccCorrectable: '可纠正错误数',
eccUncorrectable: '不可纠正错误数',
eccPending: '待生效 ECC 模式',
engineUtil: '引擎使用率',
cpuUtil: 'CPU · 使用率',
aiCore: 'AICore · 使用率',
aiCPUUtil: 'AICPU · 使用率',
ctrlCPUUtil: 'CtrlCPU · 使用率',
mediaFrequency: '媒体引擎 · 频率',
encoderUtil: '编码器 · 使用率',
decoderUtil: '解码器 · 使用率',
jpegUtil: 'JPEG · 使用率',
ofaUtil: 'OFA · 使用率',
mediaUtil: '媒体引擎 · 使用率',
computeUtil: '计算引擎 · 使用率',
copyUtil: '复制引擎 · 使用率',
ddrUsage: 'DDR · 容量',
ddrUsed: 'DDR · 已用',
ddrTotal: 'DDR · 总量',
ddrBandwidth: 'DDR · 带宽利用率',
hbmUsage: 'HBM · 容量',
hbmUsed: 'HBM · 已用',
hbmTotal: 'HBM · 总量',
hbmBandwidth: 'HBM · 带宽利用率',
hugepagesUsage: '大页 · 数量',
hugepagesUsed: '大页 · 已用',
hugepagesTotal: '大页 · 总量',
historyAverage: '平均值',
historyPeak: '峰值',
historyAggregated: '共 {count} 条采样,按 {seconds} 秒聚合;进程仅展示统计数量。',
historyRaw: '共 {count} 条原始采样。',
historyMetricEmpty: '所选时间范围内没有该指标的数据',
historyLoadFailed: '历史记录加载失败,请重试',
collectedAt: '采集时间',
processUnavailable: '进程信息不可用',
architecture: '架构',
memoryFrequency: '显存频率',
memoryTemperature: '显存温度',
memoryReserved: '保留显存',
powerDefaultLimit: '默认功率限制',
powerMaxLimit: '最大可设置功率',
clockEvents: '频率限制原因',
memoryBandwidth: '显存带宽利用率',
legacyDevice: '旧记录',
legacyHistory: '这些记录没有稳定设备标识,无法确认其设备身份和缺失值。',
pcieGeneration: 'PCIe 代际(当前 / 最大)',
pcieWidth: 'PCIe 宽度(当前 / 最大)',
gpu: 'GPU 监控',
gpuHelper: '当前系统未检测到支持的 GPU/XPU/NPU 管理指令,请检查后重试!',
process: '进程信息',
@@ -1457,7 +1513,7 @@ const message = {
temperature: '温度',
performanceState: '性能状态',
powerUsage: '功耗',
memoryUsage: '显存使用率',
memoryUsage: '显存容量占用率',
fanSpeed: '风扇转速',
power: '功率',
powerCurrent: '当前功率',
@@ -1715,6 +1771,10 @@ const message = {
delInsecureHelper: '删除授信需要重启 docker 服务,是否删除?',
pull: '拉取',
path: '路径',
imageImportSource: '导入方式',
imageUploadDrop: '拖拽镜像文件到此处,或点击上传',
imageUploadHelper: '不限制文件格式,由 Docker 校验内容。上传的文件会保留,可重试导入或手动删除。',
imageImportSubmitted: '镜像导入任务已提交,请查看任务日志',
importImage: '导入镜像',
imageBuild: '构建镜像',
buildArgs: '构建参数',
@@ -6294,6 +6354,15 @@ const message = {
snapshot: '系统快照',
clamsRulesHelper: '需要开启告警通知的病毒扫描任务',
cronJobRulesHelper: '需要配置此类型的计划任务',
alertTriggerMode: '通知条件',
alertTriggerFailed: '仅失败',
alertTriggerSuccess: '仅成功',
alertTriggerBoth: '成功或失败',
alertNotificationHelper:
'任务最终执行完成后按结果通知,重试过程中不发送;成功与失败共用每日次数及发送时段。',
notificationTitle: '计划任务-{0}「{1}」执行告警',
notificationSuccessTitle: '计划任务-{0}「{1}」执行成功通知',
notificationRule: '{0}:{1},每天最多发送 {2} 次',
clamsTitle: '病毒扫描「 {0} 」任务检测到感染文件告警',
cronJobAppTitle: '计划任务-备份应用「 {0} 」任务失败告警',
cronJobWebsiteTitle: '计划任务-备份网站「 {0} 」任务失败告警',
+13 -1
View File
@@ -1,5 +1,6 @@
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { defineStore } from 'pinia';
import { ref, reactive } from 'vue';
import { ref, reactive, toRef } from 'vue';
import { MsgError } from '@/utils/message';
import { checkStreamAuth } from '@/utils/stream-auth';
@@ -46,6 +47,17 @@ export const ProcessStore = defineStore('ProcessStore', () => {
port: undefined,
});
useSearchPersistence('host/process/process', {
pid: toRef(psSearch, 'pid'),
name: toRef(psSearch, 'name'),
username: toRef(psSearch, 'username'),
});
useSearchPersistence('host/process/network', {
processID: toRef(netSearch, 'processID'),
processName: toRef(netSearch, 'processName'),
port: toRef(netSearch, 'port'),
});
let pendingRequestType: 'ps' | 'net' | null = null;
let queuedRequestType: 'ps' | 'net' | null = null;
+74
View File
@@ -0,0 +1,74 @@
export type CronjobAlertTriggerMode = 'failed' | 'success' | 'both';
export const cronjobAlertTypes = [
'shell',
'app',
'website',
'database',
'directory',
'log',
'snapshot',
'curl',
'cutWebsiteLog',
'clean',
'ntp',
'syncIpGroup',
'cleanLog',
];
export const cronjobAlertModes = [
{ value: 'failed', label: 'xpack.alert.alertTriggerFailed' },
{ value: 'success', label: 'xpack.alert.alertTriggerSuccess' },
{ value: 'both', label: 'xpack.alert.alertTriggerBoth' },
] as const;
export const normalizeCronjobAlertMode = (value: unknown): CronjobAlertTriggerMode => {
return value === 'success' || value === 'both' ? value : 'failed';
};
const parseAdvancedParams = (value?: string): Record<string, unknown> => {
try {
const params = JSON.parse(value || '{}');
return params && typeof params === 'object' && !Array.isArray(params) ? params : {};
} catch {
return {};
}
};
export const getCronjobAlertMode = (advancedParams?: string): CronjobAlertTriggerMode => {
return normalizeCronjobAlertMode(parseAdvancedParams(advancedParams).alertTriggerMode);
};
export const setCronjobAlertMode = (advancedParams: string | undefined, mode: CronjobAlertTriggerMode): string => {
return JSON.stringify({ ...parseAdvancedParams(advancedParams), alertTriggerMode: mode });
};
export const getCronjobAlertModeLabel = (mode: unknown): string => {
return cronjobAlertModes.find((item) => item.value === normalizeCronjobAlertMode(mode))!.label;
};
export const getCronjobAlertTitle = (
row: { type: string; subType?: string; taskName?: string; title: string },
renderTitle: (type: string, taskName: string) => string,
): string => {
const type = row.type === 'cronJob' ? row.subType : row.type;
if (type && cronjobAlertTypes.includes(type) && typeof row.taskName === 'string' && row.taskName.length > 0) {
return renderTitle(type, row.taskName);
}
return row.title;
};
export const getCronjobAlertResult = (params: unknown): 'success' | 'failed' => {
if (typeof params === 'string') {
try {
params = JSON.parse(params);
} catch {
return 'failed';
}
}
if (!Array.isArray(params)) return 'failed';
const result = params.find((item) => item?.index === 'result');
if (result) return result.value === 'success' ? 'success' : 'failed';
const numberedResult = params.find((item) => item?.index === '2' && item?.key === 'result');
return numberedResult?.value === '成功' ? 'success' : 'failed';
};
+63
View File
@@ -0,0 +1,63 @@
import { watch, type Ref } from 'vue';
type SearchValue = string | number | undefined;
type SearchFields = Record<string, Ref<SearchValue>>;
export const bindSearchPersistence = (key: () => string | undefined, fields: SearchFields) => {
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
let initialized = false;
let restoring = false;
let activeKey: string | undefined;
const stopRestore = watch(
key,
(value) => {
activeKey = value ? `1panel:search:v1:${value}` : undefined;
let saved: Record<string, unknown> = {};
try {
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
} catch {
// Invalid or unavailable storage must not prevent a page from loading.
}
restoring = true;
for (const [name, field] of Object.entries(fields)) {
const value = saved[name];
const initial = defaults[name];
const fallback = initialized ? (typeof initial === 'string' ? '' : undefined) : initial;
const valid =
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
(fallback === undefined || typeof value === typeof fallback);
field.value =
value === null
? typeof initial === 'string'
? ''
: undefined
: valid
? (value as SearchValue)
: fallback;
}
restoring = false;
initialized = true;
},
{ immediate: true, flush: 'sync' },
);
const stopSave = watch(
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
(value) => {
if (restoring || !activeKey) return;
try {
localStorage.setItem(activeKey, JSON.stringify(value));
} catch {
// Search remains usable when browser storage is disabled or full.
}
},
{ flush: 'sync' },
);
return () => {
stopRestore();
stopSave();
};
};
+17
View File
@@ -0,0 +1,17 @@
import { watch, type Ref } from 'vue';
type SearchFields = Record<string, Ref<string | number | undefined>>;
/** Search terms belong to the current tab visit, not browser storage or cached pages. */
export const bindSearchReset = (context: () => unknown, fields: SearchFields) => {
const emptyValues = Object.fromEntries(
Object.entries(fields).map(([name, field]) => [name, typeof field.value === 'string' ? '' : undefined]),
);
return watch(
() => JSON.stringify(context()),
() => {
for (const [name, field] of Object.entries(fields)) field.value = emptyValues[name];
},
{ immediate: true, flush: 'sync' },
);
};
+1
View File
@@ -24,6 +24,7 @@ const formatSkillInstallTask = () => {
};
const taskTextMap: Record<string, () => string> = {
RedisCliEnable: () => i18n.global.t('database.redisCliEnableTask'),
BatchInstallAgent: () => formatBatchAgentTask('Install'),
DispatchAgentInstallTasks: () => formatDispatchAgentTask('Install'),
BatchUpgradeAgent: () => formatBatchAgentTask('Upgrade'),
@@ -172,6 +172,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
@@ -192,6 +193,11 @@ const searching = ref(false);
const operating = ref('');
const keyword = ref('');
const marketKeyword = ref('');
useSearchPersistence(
'ai/agents/agent/config/tabs/plugins',
{ keyword, marketKeyword },
() => agentId.value || undefined,
);
const origin = ref('');
const status = ref('');
const page = ref(1);
@@ -299,8 +305,6 @@ async function operate(plugin: AI.AgentPluginItem, action: PluginOperate) {
async function load(id: number) {
agentId.value = id;
mode.value = 'installed';
keyword.value = '';
marketKeyword.value = '';
origin.value = '';
status.value = '';
page.value = 1;
@@ -138,6 +138,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -168,6 +169,11 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchPersistence(
'ai/agents/agent/config/tabs/skills/hermes',
{ installedKeyword, marketKeyword },
() => agentId.value || undefined,
);
const installedSkills = ref<AI.AgentSkillItem[]>([]);
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
const installingSkill = ref('');
@@ -315,8 +321,6 @@ const handleModeChange = async (value: SkillViewMode) => {
const load = async (id: number) => {
agentId.value = id;
mode.value = 'market';
installedKeyword.value = '';
marketKeyword.value = '';
marketSource.value = getDefaultMarketSource();
marketSearched.value = false;
installedSkills.value = [];
@@ -142,6 +142,7 @@
</template>
<script setup lang="ts">
import { useSearchPersistence } from '@/composables/useSearchPersistence';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -180,6 +181,11 @@ const getDefaultMarketSource = (): SkillMarketSource => {
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchPersistence(
'ai/agents/agent/config/tabs/skills/openclaw',
{ installedKeyword, marketKeyword },
() => agentId.value || undefined,
);
const skills = ref<AI.AgentSkillItem[]>([]);
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
const updatingSkill = ref('');

Some files were not shown because too many files have changed in this diff Show More