Compare commits

...
Author SHA1 Message Date
guoyuqi 262618a4a9 fix: fix container list 2026-10-08 17:45:09 +08:00
guoyuqi b374b70e71 fix: fix container list 2026-10-08 17:44:29 +08:00
guoyuqi 4ea3f7e4b8 fix: fix clean network 2026-10-08 17:44:29 +08:00
guoyuqi a917880cac fix: fix clean network 2026-10-08 17:44:29 +08:00
guoyuqi d41dd382ab fix: fix container list 2026-10-08 17:44:29 +08:00
Yuki Guoandssongliu 2701af9054 refactor: select bind (#13993)
* refactor: select bind

* refactor: simplify table selection state handling

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-08 17:11:28 +08:00
Yuki Guoandssongliu 4954067736 refactor: search bind (#13992)
* refactor: search bind

* refactor: simplify search persistence

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-08 16:19:29 +08:00
ssongliu b962144e7d fix(ssh): validate imported key pairs and cancel stale file reads (#13991) 2026-10-08 14:06:40 +08:00
Yuki Guo 8f1be42b6b fix: fix log text (#13990) 2026-10-08 11:32:09 +08:00
Yuki Guo c6969f231b fix: fix operation log (#13989) 2026-10-08 11:08:04 +08:00
4kerccand4kercc 3571fb8d86 fix: speed up SFTP backup upload with concurrent writes (#13976)
The SFTP uploader used io.Copy(dstFile, srcFile). Since the source is an
*os.File it implements io.WriterTo, so io.Copy took the os.File.WriteTo
branch and issued one synchronous SFTP WRITE per packet, waiting for the
server reply every time. Throughput was therefore bounded by packetSize/RTT
instead of the link capacity: on a ~68ms RTT link a 440MB backup reached
only ~0.25 MB/s, while rclone / OpenSSH sftp reach 16-19 MB/s on the very
same link and target.

pkg/sftp already provides a pipelined write path (File.ReadFrom, enabled by
UseConcurrentWrites and bounded by MaxConcurrentRequestsPerFile, default 64).
Use it for uploads, and truncate the remote file to the number of bytes
actually written when the concurrent write fails, to avoid leaving holes.

Measured against the same 68ms RTT target: a 96MiB upload went from 6m40s
(~0.25 MB/s) to ~6s (~16 MB/s); a 950MB three-site backup job from ~2h40m to
~2m04s.

Co-authored-by: 4kercc <13767585+4kercc@users.noreply.github.com>
2026-10-08 09:45:51 +08:00
ssongliu d21288788e fix: harden WebSocket origin validation (#13953) 2026-09-30 15:21:21 +08:00
ssongliu f65e4b06ab feat(vm): add VNC keyboard control translations (#13951)
* feat(vm): add VNC keyboard control translations

* feat(vm): add Windows VirtIO driver guidance translations

* feat(vm): add disk expansion translations and operation logs
2026-09-30 15:20:19 +08:00
ssongliu d26bc9a3a4 chore(deps): update dependencies and address security advisories (#13945)
Update pending Go and frontend dependencies, including gRPC, OpenTelemetry
and SVGO security fixes. Raise the minimum Go version to 1.26.6 and update
x/image and x/mod to patched versions.

Use moby/go-archive directly for image build contexts so its vulnerable
v0.1.0 replacement can be removed and v0.3.0 used. Regenerate Go module
checksums with go mod tidy and refresh the npm lockfile.

Validation: Linux amd64 builds, existing Go tests and go mod verify pass
for core and agent; frontend production build passes; npm audit reports
zero vulnerabilities. Frontend type-check reports the same nine errors
with the original and updated lockfiles.

Go scanning still flags two advisories in the legacy Docker SDK, which
has no patched release on its current module path. A separate SDK
migration and host Docker daemon updates remain necessary.
2026-09-29 21:36:19 +08:00
ssongliu 7088a903fe fix(vllm): sample realtime metrics independently of history (#13944) 2026-09-29 17:29:29 +08:00
ssongliu 864ebeed82 fix: load domestic release notes from new docs (#13943) 2026-09-29 17:29:16 +08:00
CN_CoreStebandssongliu c1b2b92708 feat(monitor): add monitor data export with csv, json and xlsx formats (#13875)
- new export tab between monitor and settings with time range, metric,
  device/interface/gpu selection and format choice
- files are generated in the browser from existing monitor apis; no new
  backend endpoints
- csv with utf-8 bom, escaping and formula sanitization; multiple metrics
  are zipped; xlsx uses one worksheet per metric with real date cells and
  row-limit warnings; json emits flat row objects
- share downloadBlob in utils/file with delayed url revocation
- use the 60s monitor timeout and node header for gpu queries

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-09-29 15:50:00 +08:00
CN_CoreStebandssongliu 2189d9229a perf(monitor): improve monitor query and chart performance for large ranges (#13874)
* perf(monitor): improve monitor query and chart performance for large ranges

- enable LTTB sampling in the shared line chart to bound rendered points
- empty io/network in monitor search now means no name filter, returning
  all device rows in one query instead of fanning out per device
- merge device names recorded in the monitor db into io/network/gpu
  options so removed devices stay selectable; move logic to service layer
- add composite indexes (name, created_at) on monitor_ios/monitor_networks
  and (product_name, created_at) on monitor_gpus
- stop migrating the unused MonitorGPU model into MonitorDB
- fix the memory param which was compared against "mem" and never matched

* chore(monitor): remove monitor test fixtures

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-09-29 15:34:16 +08:00
ssongliu d0183f460e feat: add vLLM monitoring (#13942) 2026-09-29 15:19:30 +08:00
149 changed files with 4859 additions and 850 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)
}
+10 -23
View File
@@ -1,13 +1,9 @@
package v2
import (
"sort"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/net"
)
// @Tags Monitor
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
}
// @Tags Monitor
// @Summary Clean monitor data
// @Summary Clean host or GPU monitor data
// @Param request body dto.MonitorClean true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
func (b *BaseApi) CleanMonitor(c *gin.Context) {
if err := monitorService.CleanData(); err != nil {
var req dto.MonitorClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanData(req.Type); err != nil {
helper.InternalServer(c, err)
return
}
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/netoptions [get]
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
netStat, _ := net.IOCounters(true)
var options []string
options = append(options, "all")
for _, net := range netStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
}
// @Tags Monitor
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/iooptions [get]
func (b *BaseApi) GetIOOptions(c *gin.Context) {
diskStat, _ := disk.IOCounters()
var options []string
options = append(options, "all")
for _, net := range diskStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadIOOptions())
}
+45
View File
@@ -0,0 +1,45 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
var req dto.MonitorVLLMCurrent
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanVLLMMonitor(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+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
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.CreateRootCert(req); err != nil {
helper.InternalServer(c, err)
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.EditRootCert(req); err != nil {
helper.InternalServer(c, err)
@@ -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"`
}
+43 -7
View File
@@ -1,6 +1,10 @@
package dto
import "time"
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type MonitorSearch struct {
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
@@ -26,19 +30,26 @@ type Process struct {
}
type MonitorSetting struct {
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
GPUMonitorStatus string `json:"gpuMonitorStatus"`
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
GPUMonitorInterval string `json:"gpuMonitorInterval"`
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
}
type MonitorSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
Value string `json:"value"`
}
type MonitorGPUOptions struct {
Supported bool `json:"supported"`
GPUType string `json:"gpuType"`
ChartHide []GPUChartHide `json:"chartHide"`
Options []string `json:"options"`
@@ -114,3 +125,28 @@ type GPUProcess struct {
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}
type MonitorVLLMSearch struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"`
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
}
type MonitorVLLMData struct {
SampleCount int64 `json:"sampleCount"`
BucketSeconds int64 `json:"bucketSeconds"`
Points []model.MonitorVLLM `json:"points"`
}
type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorClean struct {
Type string `json:"type" validate:"required,oneof=host gpu"`
}
+1 -1
View File
@@ -25,7 +25,7 @@ type RootCertOperate struct {
ID uint `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
EncryptionMode string `json:"encryptionMode"`
PassPhrase string `json:"passPhrase"`
PublicKey string `json:"publicKey"`
PrivateKey string `json:"privateKey"`
+35
View File
@@ -1,5 +1,7 @@
package model
import "time"
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
@@ -75,3 +77,36 @@ type MonitorGPU struct {
FanSpeed *float64 `json:"fanSpeed"`
Processes string `json:"processes"`
}
type MonitorVLLM struct {
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
CreatedAt time.Time `json:"createdAt"`
AppInstallID uint `json:"appInstallID"`
Status string `json:"status"`
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"`
CacheUsage *float64 `json:"cacheUsage"`
PromptThroughput *float64 `json:"promptThroughput"`
GenerationThroughput *float64 `json:"generationThroughput"`
RequestThroughput *float64 `json:"requestThroughput"`
TimeToFirstToken *float64 `json:"timeToFirstToken"`
TimePerOutputToken *float64 `json:"timePerOutputToken"`
RequestLatency *float64 `json:"requestLatency"`
PrefillTime *float64 `json:"prefillTime"`
DecodeTime *float64 `json:"decodeTime"`
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
}
+27 -16
View File
@@ -21,13 +21,16 @@ type GPUHistoryPoint struct {
}
type IMonitorRepo interface {
CleanHost() error
CleanGPU() error
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)
GetIONames() ([]string, error)
GetNetworkNames() ([]string, error)
CreateMonitorBase(model model.MonitorBase) error
BatchCreateMonitorGPU(list []model.MonitorGPU) error
@@ -38,7 +41,6 @@ type IMonitorRepo interface {
DelMonitorIO(timeForDelete time.Time) error
DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
}
@@ -46,6 +48,19 @@ func NewIMonitorRepo() IMonitorRepo {
return &MonitorRepo{}
}
func (s *MonitorRepo) CleanHost() error {
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
return err
}
}
return nil
}
func (s *MonitorRepo) CleanGPU() error {
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
}
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
var data []model.MonitorBase
db := global.MonitorDB
@@ -73,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
err := db.Find(&data).Error
return data, err
}
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
db := global.GPUMonitorDB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&data).Error
return data, err
func (u *MonitorRepo) GetIONames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
@@ -111,12 +128,6 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
}
func (s *MonitorRepo) WithByProductName(name string) DBOption {
return func(g *gorm.DB) *gorm.DB {
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
+78
View File
@@ -0,0 +1,78 @@
package repo
import (
"errors"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
HistogramSamples string
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
return global.VLLMMonitorDB.Create(point).Error
}
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
var point model.MonitorVLLM
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
err := db.Order("created_at DESC, id DESC").First(&point).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return point, nil
}
return point, err
}
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
var count int64
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
err := db.Count(&count).Error
return count, err
}
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
var columns []string
if seconds > 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(), seconds)}
for _, column := range metrics {
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
continue
}
columns = append(columns, operation+"("+column+") AS "+column)
}
if aggregation != "max" {
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
db = db.Order("created_at ASC, id ASC")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
}
+76 -34
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,7 @@ 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) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
@@ -123,7 +124,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 +207,30 @@ 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})
if !containersOnly {
images, _ := client.ImageList(ctx, 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(ctx, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(ctx, client)
}
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers)
for _, item := range containers {
switch item.State {
@@ -292,27 +296,65 @@ 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})
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) {
@@ -1715,11 +1757,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 +1984,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)
}
})
}
}
+5 -4
View File
@@ -32,10 +32,11 @@ var (
scriptRepo = repo.NewIScriptRepo()
cronjobRepo = repo.NewICronjobRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
settingRepo = repo.NewISettingRepo()
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive"
"github.com/moby/go-archive"
)
type ImageService struct{}
+242 -55
View File
@@ -33,11 +33,14 @@ import (
type MonitorService struct {
DiskIO chan ([]disk.IOCountersStat)
NetIO chan ([]net.IOCountersStat)
ctx context.Context
}
var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
monitorSettingMutex sync.Mutex
gpuMonitorMutex sync.Mutex
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
@@ -47,8 +50,13 @@ type IMonitorService interface {
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
LoadSetting() (*dto.MonitorSetting, error)
UpdateSetting(key, value string) error
CleanData() error
CleanData(monitorType string) error
LoadIOOptions() []string
LoadNetworkOptions() []string
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
@@ -60,6 +68,7 @@ func NewIMonitorService() IMonitorService {
return &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: context.Background(),
}
}
@@ -85,7 +94,7 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
base.TopCPUItems = processes
base.TopCPU = ""
}
if req.Param == "all" || req.Param == "mem" {
if req.Param == "all" || req.Param == "memory" {
var processes []dto.Process
_ = json.Unmarshal([]byte(base.TopMem), &processes)
base.TopMemItems = processes
@@ -96,7 +105,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "io" {
bases, err := monitorRepo.GetIO(repo.WithByName(req.IO), repo.WithByCreatedAt(req.StartTime, req.EndTime))
ioOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.IO) != 0 {
ioOpts = append(ioOpts, repo.WithByName(req.IO))
}
bases, err := monitorRepo.GetIO(ioOpts...)
if err != nil {
return nil, err
}
@@ -110,7 +123,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "network" {
bases, err := monitorRepo.GetNetwork(repo.WithByName(req.Network), repo.WithByCreatedAt(req.StartTime, req.EndTime))
netOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.Network) != 0 {
netOpts = append(netOpts, repo.WithByName(req.Network))
}
bases, err := monitorRepo.GetNetwork(netOpts...)
if err != nil {
return nil, err
}
@@ -126,20 +143,54 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
return data, nil
}
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
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 {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
func (m *MonitorService) LoadIOOptions() []string {
optionSet := make(map[string]struct{})
if diskStat, err := disk.IOCounters(); err == nil {
for _, item := range diskStat {
optionSet[item.Name] = struct{}{}
}
}
// union with names recorded in the monitor db so removed devices stay selectable
if names, err := monitorRepo.GetIONames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadNetworkOptions() []string {
optionSet := make(map[string]struct{})
if netStat, err := net.IOCounters(true); err == nil {
for _, item := range netStat {
optionSet[item.Name] = struct{}{}
}
}
if names, err := monitorRepo.GetNetworkNames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
exist, client := accelerator.New()
if !exist {
return data
}
seen := make(map[string]bool)
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
}
data.Supported = true
devices, err := monitorRepo.GetGPUDevices()
if err != nil {
global.LOG.Warnf("Load accelerator history options failed: %v", err)
@@ -282,9 +333,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
}
func (m *MonitorService) UpdateSetting(key, value string) error {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
switch key {
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
if value != constant.StatusEnable && value != constant.StatusDisable {
return fmt.Errorf("invalid monitoring status")
}
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
interval, err := strconv.Atoi(value)
if err != nil || interval < 10 || interval > 43200 {
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
}
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
days, err := strconv.Atoi(value)
if err != nil || days < 1 {
return fmt.Errorf("monitoring retention must be a positive integer")
}
}
switch key {
case "MonitorStatus":
if value == constant.StatusEnable && global.MonitorCronID == 0 {
if value == constant.StatusEnable && monitorCancel == nil {
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
if err != nil {
return err
@@ -293,8 +362,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
return err
}
}
if value == constant.StatusDisable && global.MonitorCronID != 0 {
if value == constant.StatusDisable && monitorCancel != nil {
monitorCancel()
monitorCancel = nil
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
global.MonitorCronID = 0
}
@@ -303,30 +373,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
if err != nil {
return err
}
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
if status.Value == constant.StatusEnable && monitorCancel != nil {
if err := StartMonitor(true, value); err != nil {
return err
}
}
case "GPUMonitorStatus":
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
if err != nil {
return err
}
if err := StartGPUMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
global.GPUMonitorCronID = 0
}
case "GPUMonitorInterval":
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartGPUMonitor(value); err != nil {
return err
}
}
case "VLLMMonitorStatus":
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return err
}
if err := StartVLLMMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
global.VLLMMonitorCronID = 0
}
case "VLLMMonitorInterval":
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartVLLMMonitor(value); err != nil {
return err
}
}
}
return settingRepo.Update(key, value)
}
func (m *MonitorService) CleanData() error {
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
return err
func (m *MonitorService) CleanData(monitorType string) error {
switch monitorType {
case "host":
return monitorRepo.CleanHost()
case "gpu":
gpuMonitorMutex.Lock()
defer gpuMonitorMutex.Unlock()
return monitorRepo.CleanGPU()
default:
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
return err
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil {
return err
}
_ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error
return nil
}
func (m *MonitorService) Run() {
if m.ctx.Err() != nil {
return
}
var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 {
@@ -362,7 +482,6 @@ func (m *MonitorService) Run() {
m.loadDiskIO()
m.loadNetIO()
m.saveGPUData()
MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays"))
if err != nil {
@@ -373,30 +492,34 @@ func (m *MonitorService) Run() {
_ = monitorRepo.DelMonitorBase(timeForDelete)
_ = monitorRepo.DelMonitorIO(timeForDelete)
_ = monitorRepo.DelMonitorNet(timeForDelete)
_ = monitorRepo.DelMonitorGPU(timeForDelete)
}
func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters()
ioStat, _ := disk.IOCountersWithContext(m.ctx)
var diskIOList []disk.IOCountersStat
for _, io := range ioStat {
diskIOList = append(diskIOList, io)
}
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
m.DiskIO <- diskIOList
select {
case <-m.ctx.Done():
case m.DiskIO <- diskIOList:
}
}
func (m *MonitorService) loadNetIO() {
netStat, _ := net.IOCounters(true)
netStatAll, _ := net.IOCounters(false)
netStat, _ := net.IOCountersWithContext(m.ctx, true)
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
var netList []net.IOCountersStat
netList = append(netList, netStat...)
netList = append(netList, netStatAll...)
m.NetIO <- netList
select {
case <-m.ctx.Done():
case m.NetIO <- netList:
}
}
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
defer close(m.DiskIO)
for {
select {
case <-ctx.Done():
@@ -446,14 +569,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
}
}
_ = monitorRepo.BatchCreateMonitorIO(ioList)
m.DiskIO <- ioStat2
select {
case <-ctx.Done():
return
case m.DiskIO <- ioStat2:
}
}
}
}
}
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
defer close(m.NetIO)
for {
select {
case <-ctx.Done():
@@ -483,7 +609,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
}
_ = monitorRepo.BatchCreateMonitorNet(netList)
m.NetIO <- netStat2
select {
case <-ctx.Done():
return
case m.NetIO <- netStat2:
}
}
}
}
@@ -592,27 +722,38 @@ func loadTopMem() []dto.Process {
}
func StartMonitor(removeBefore bool, interval string) error {
intervalItem, err := strconv.Atoi(interval)
if err != nil || intervalItem < 10 || intervalItem > 43200 {
return fmt.Errorf("invalid host monitoring interval: %s", interval)
}
if removeBefore {
monitorCancel()
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
}
intervalItem, err := strconv.Atoi(interval)
if err != nil {
return err
}
service := NewIMonitorService()
ctx, cancel := context.WithCancel(context.Background())
monitorCancel = cancel
service := &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: ctx,
}
now := time.Now()
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
time.AfterFunc(time.Until(nextMinute), func() {
timer := time.AfterFunc(time.Until(nextMinute), func() {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
if ctx.Err() != nil {
return
}
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
if err != nil {
return
}
global.MonitorCronID = monitorID
})
monitorCancel = func() {
cancel()
timer.Stop()
}
service.Run()
@@ -669,15 +810,50 @@ func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
return data
}
func (m *MonitorService) saveGPUData() {
status, err := settingRepo.GetValueByKey("MonitorStatus")
func StartGPUMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
global.LOG.Errorf("load monitor status failed: %v", err)
return err
}
if global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
}
global.GPUMonitorCronID = id
return nil
}
func (m *MonitorService) saveGPUData() {
if !gpuMonitorMutex.TryLock() {
return
}
defer gpuMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
return
}
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
}
exist, client := accelerator.New()
if !exist {
return
@@ -691,7 +867,7 @@ func (m *MonitorService) saveGPUData() {
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
}
intervalSeconds := 0
if setting, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval")); err == nil {
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
intervalSeconds, _ = strconv.Atoi(setting.Value)
}
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
@@ -807,3 +983,14 @@ func loadHostSysPath() string {
}
return hostSys
}
func sortedMonitorOptions(optionSet map[string]struct{}) []string {
options := make([]string, 0, len(optionSet))
for name := range optionSet {
if len(name) != 0 && name != "all" {
options = append(options, name)
}
}
sort.Strings(options)
return append([]string{"all"}, options...)
}
+287
View File
@@ -0,0 +1,287 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/robfig/cron/v3"
"golang.org/x/sync/errgroup"
)
var vllmMonitorMutex sync.Mutex
var vllmMetricsClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request")
}
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return data, err
}
if install.App.Key != "vllm" {
return data, fmt.Errorf("not a vLLM installation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
}
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
if err != nil {
return data, err
}
if data.BucketSeconds > 0 {
if req.Aggregation != "max" {
for i := range points {
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
return data, err
}
}
}
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
var point model.MonitorVLLM
if next < len(points) && points[next].Bucket == bucket {
point = points[next].MonitorVLLM
next++
}
point.AppInstallID = req.AppInstallID
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
data.Points = append(data.Points, point.MonitorVLLM)
}
}
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
}
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return previous, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return model.MonitorVLLM{}, ctx.Err()
case <-timer.C:
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
}
return point, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "vllm" {
return fmt.Errorf("not a vLLM installation")
}
vllmMonitorMutex.Lock()
defer vllmMonitorMutex.Unlock()
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
}
func StartVLLMMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
}
global.VLLMMonitorCronID = id
return nil
}
func (m *MonitorService) saveVLLMData() {
if !vllmMonitorMutex.TryLock() {
return
}
defer vllmMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
return
}
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
}
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
if err != nil {
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
return
}
if len(apps) == 0 {
return
}
ids := make([]uint, 0, len(apps))
for _, app := range apps {
ids = append(ids, app.ID)
}
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
if err != nil {
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
return
}
var group errgroup.Group
group.SetLimit(4)
for _, install := range installs {
group.Go(func() error {
previous, err := vllmMonitorRepo.Latest(install.ID)
if err != nil {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
return vllmMonitorRepo.Create(&point)
})
}
if err := group.Wait(); err != nil {
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
}
}
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
if install.HttpPort <= 0 || install.HttpPort > 65535 {
return point, fmt.Errorf("invalid vLLM port")
}
var env map[string]interface{}
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
return point, err
}
host, _ := env[constant.HostIP].(string)
host = strings.Trim(host, "[]")
switch host {
case "", "0.0.0.0":
host = "127.0.0.1"
case "::":
host = "::1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
if err != nil {
return point, err
}
response, err := vllmMetricsClient.Do(req)
if err != nil {
return point, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
if err != nil {
return point, err
}
if len(body) > 8*1024*1024 {
return point, fmt.Errorf("vLLM metrics response too large")
}
metrics, err := vllm.Parse(bytes.NewReader(body))
if err != nil {
return point, err
}
point.CreatedAt = time.Now()
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
}
var before vllm.Metrics
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
if previous.Status == "ok" && elapsed > 0 {
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
return point, err
}
}
calculated, err := vllm.Calculate(metrics, before, elapsed)
if err != nil {
return point, err
}
calculated.AppInstallID = install.ID
calculated.CreatedAt = point.CreatedAt
calculated.RawMetrics = string(raw)
calculated.Status = "ok"
return calculated, nil
}
+72 -11
View File
@@ -7,6 +7,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
@@ -37,6 +38,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
const sshPath = "/etc/ssh/sshd_config"
@@ -438,15 +440,33 @@ func (u *SSHService) SyncRootCert() error {
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
continue
}
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
continue
}
cert.EncryptionMode = loadSSHKeyType(publicKey)
if cert.EncryptionMode == "" {
continue
}
rootCerts = append(rootCerts, cert)
}
return hostRepo.SyncCert(rootCerts)
}
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
return buserr.New("ErrCmdIllegal")
switch req.Mode {
case "generate":
switch req.EncryptionMode {
case "rsa", "ed25519", "ecdsa", "dsa":
default:
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
}
case "input", "import":
if err := validateSSHKeyPair(&req); err != nil {
return err
}
default:
return buserr.WithName("ErrNotSupportType", req.Mode)
}
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
if certItem.ID != 0 {
@@ -543,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
}
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
if err := validateSSHKeyPair(&req); err != nil {
return err
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("load current user failed, err: %v", err)
@@ -1681,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
return itemDate
}
func loadEncryptioMode(content string) string {
if strings.HasPrefix(content, "ssh-rsa") {
func loadSSHKeyType(publicKey ssh.PublicKey) string {
switch publicKey.Type() {
case ssh.KeyAlgoRSA:
return "rsa"
}
if strings.HasPrefix(content, "ssh-ed25519") {
case ssh.KeyAlgoED25519:
return "ed25519"
}
if strings.HasPrefix(content, "ssh-ecdsa") {
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
return "ecdsa"
}
if strings.HasPrefix(content, "ssh-dsa") {
case ssh.KeyAlgoDSA:
return "dsa"
}
return ""
}
func validateSSHKeyPair(req *dto.RootCertOperate) error {
req.PublicKey = strings.TrimSpace(req.PublicKey)
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
return buserr.New("ErrSSHPublicKey")
}
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPublicKey")
}
req.EncryptionMode = loadSSHKeyType(publicKey)
if req.EncryptionMode == "" {
return buserr.WithName("ErrNotSupportType", publicKey.Type())
}
block, rest := pem.Decode([]byte(req.PrivateKey))
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPrivateKey")
}
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
var missingPassphrase *ssh.PassphraseMissingError
if errors.As(err, &missingPassphrase) {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
} else if err == nil {
req.PassPhrase = ""
}
if err != nil {
return buserr.New("ErrSSHPrivateKey")
}
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
return buserr.New("ErrSSHKeyMismatch")
}
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
if comment != "" {
req.PublicKey += " " + comment
}
req.PublicKey += "\n"
req.PrivateKey += "\n"
return nil
}
func updateLocalConn(newPort uint) error {
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
if len(conn) == 0 {
+1 -1
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)
}
+31 -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"
@@ -2486,6 +2493,17 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
@@ -4028,12 +4046,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": [
+22
View File
@@ -38,6 +38,28 @@ func Run() {
}
}
var gpuStatus, gpuInterval model.Setting
if err := global.DB.Where("key = ?", "GPUMonitorStatus").First(&gpuStatus).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
} else if gpuStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "GPUMonitorInterval").First(&gpuInterval).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring interval failed: %v", err)
} else if err := service.StartGPUMonitor(gpuInterval.Value); err != nil {
global.LOG.Errorf("Start GPU monitoring failed: %v", err)
}
}
var vllmStatus, vllmInterval model.Setting
if err := global.DB.Where("key = ?", "VLLMMonitorStatus").First(&vllmStatus).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
} else if vllmStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "VLLMMonitorInterval").First(&vllmInterval).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring interval failed: %v", err)
} else if err := service.StartVLLMMonitor(vllmInterval.Value); err != nil {
global.LOG.Errorf("Start vLLM monitoring failed: %v", err)
}
}
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
global.LOG.Errorf("can not add website corn job: %s", err.Error())
}
+11 -8
View File
@@ -14,12 +14,13 @@ import (
)
var (
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
VLLMMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
LOG *logrus.Logger
CONF ServerConfig
@@ -29,8 +30,10 @@ var (
Dir SystemDir
Cron *cron.Cron
MonitorCronID cron.EntryID
Cron *cron.Cron
MonitorCronID cron.EntryID
VLLMMonitorCronID cron.EntryID
GPUMonitorCronID cron.EntryID
IsMaster bool
+17 -20
View File
@@ -1,14 +1,12 @@
module github.com/1Panel-dev/1Panel/agent
go 1.26.1
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
go 1.26.6
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.14.0
github.com/compose-spec/compose-go/v2 v2.15.0
github.com/creack/pty v1.1.24
github.com/docker/cli v29.7.2+incompatible
github.com/docker/docker v28.5.2+incompatible
@@ -33,6 +31,7 @@ require (
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.73
github.com/minio/minio-go/v7 v7.3.0
github.com/moby/go-archive v0.3.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1
@@ -52,13 +51,13 @@ require (
github.com/tklauser/go-sysconf v0.4.0
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.8.2
golang.org/x/crypto v0.55.0
go.mongodb.org/mongo-driver/v2 v2.9.0
golang.org/x/crypto v0.56.0
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
golang.org/x/time v0.15.0
golang.org/x/time v0.16.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3
gopkg.in/yaml.v3 v3.0.1
@@ -121,7 +120,7 @@ require (
github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -132,7 +131,6 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -155,11 +153,10 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -209,18 +206,18 @@ require (
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/grpc v1.83.1 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
+41 -40
View File
@@ -205,8 +205,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00=
github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -307,8 +307,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -438,6 +438,7 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
@@ -597,16 +598,16 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -877,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0=
go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -889,23 +890,23 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -951,8 +952,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1076,8 +1077,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1193,8 +1194,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1315,10 +1316,10 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1336,8 +1337,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+14
View File
@@ -731,3 +731,17 @@ 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 }}"
ErrSSHPublicKey: "Enter a valid SSH public key"
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
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"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
ErrSSHPublicKey: "Introduce una clave pública SSH válida"
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
+4
View File
@@ -732,3 +732,7 @@ FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند"
ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
+4
View File
@@ -722,3 +722,7 @@ FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: 'Nama fail sasaran: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker."
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: 'Nome do arquivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker."
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу"
+4
View File
@@ -731,3 +731,7 @@ FileTaskRename: 'Hedef dosya adı: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin."
ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın."
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
+14
View File
@@ -731,3 +731,17 @@ FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
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: "部分網路清理失敗,請查看任務日誌"
+14
View File
@@ -731,3 +731,17 @@ FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
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
@@ -13,6 +13,7 @@ func Init() {
global.TaskDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "task.db"), "task")
global.MonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "monitor.db"), "monitor")
global.GPUMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "gpu_monitor.db"), "gpu_monitor")
global.VLLMMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "vllm_monitor.db"), "vllm_monitor")
global.AlertDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "alert.db"), "alert")
if _, err := os.Stat(path.Join(global.Dir.DbDir, "core.db")); err == nil {
+33 -1
View File
@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -13,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/alert_push"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
func Init() {
@@ -174,6 +176,36 @@ func initAlertTask() {
}
func initMonitorDB() {
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorGPU{}, &model.MonitorIO{})
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorIO{})
_ = global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
_ = global.TaskDB.AutoMigrate(&model.Task{})
// building indexes on large monitor tables can take seconds, keep it off the startup path;
// WAL mode leaves readers unblocked and busy_timeout covers the collector's inserts meanwhile
go ensureMonitorIndexes()
}
func ensureMonitorIndexes() {
indexes := []struct {
db *gorm.DB
stmt string
}{
// created_at alone serves unfiltered range queries and retention cleanup
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_bases_created ON monitor_bases(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_created ON monitor_ios(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_created ON monitor_networks(created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_created ON monitor_gpus(created_at)"},
// (name, created_at) serves per-device range queries and distinct name lookups
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_name_created ON monitor_ios(name, created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_name_created ON monitor_networks(name, created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_product_created ON monitor_gpus(product_name, created_at)"},
}
start := time.Now()
for _, index := range indexes {
if err := index.db.Exec(index.stmt).Error; err != nil {
global.LOG.Warnf("create monitor index failed, stmt: %s, err: %v", index.stmt, err)
}
}
if elapsed := time.Since(start); elapsed > time.Second {
global.LOG.Infof("monitor indexes ready, took %s", elapsed)
}
}
+2
View File
@@ -112,6 +112,8 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
}
}
+25
View File
@@ -1915,3 +1915,28 @@ var AddAcceleratorMetrics = &gormigrate.Migration{
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
var AddVLLMMonitor = &gormigrate.Migration{
ID: "20260928-vllm-monitor",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddMonitorSettings = &gormigrate.Migration{
ID: "20260929-monitor-settings",
Migrate: func(tx *gorm.DB) error {
for _, key := range []string{"MonitorStatus", "MonitorInterval", "MonitorStoreDays"} {
var setting model.Setting
if err := tx.Where("key = ?", key).First(&setting).Error; err != nil {
return err
}
for _, prefix := range []string{"GPU", "VLLM"} {
if err := tx.Where("key = ?", prefix+key).FirstOrCreate(&model.Setting{Key: prefix + key, Value: setting.Value}).Error; err != nil {
return err
}
}
}
return nil
},
}
+3
View File
@@ -27,6 +27,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor)
aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions)
aiToolsRouter.POST("/vllm/monitor/search", baseApi.LoadVLLMMonitor)
aiToolsRouter.POST("/vllm/monitor/current", baseApi.LoadVLLMCurrent)
aiToolsRouter.POST("/vllm/monitor/clean", baseApi.CleanVLLMMonitor)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server/detail", baseApi.LoadMcpServerDetail)
+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)
@@ -23,14 +23,6 @@ type providerResult struct {
}
func New() (bool, Client) {
return newClient()
}
func NewAll() (bool, Client) {
return New()
}
func newClient() (bool, Client) {
client := Client{}
if available, gpuClient := gpu.New(); available {
client.providers = append(client.providers, gpuProvider{client: gpuClient})
+335
View File
@@ -0,0 +1,335 @@
package vllm
import (
"bufio"
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type Metrics map[string]map[string]float64
type histogram struct {
Sum float64 `json:"sum"`
Count float64 `json:"count"`
Buckets map[string]float64 `json:"buckets,omitempty"`
}
func Parse(reader io.Reader) (Metrics, error) {
metrics := Metrics{}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "vllm:") {
continue
}
match := re.GetRegex(re.VLLMMetricSamplePattern).FindStringSubmatch(line)
if match == nil {
return nil, fmt.Errorf("invalid vLLM metric sample")
}
name := strings.TrimPrefix(match[1], "vllm:")
switch name {
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total":
default:
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
if base == name {
continue
}
switch base {
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds":
default:
continue
}
}
value, err := strconv.ParseFloat(match[3], 64)
if err != nil {
return nil, fmt.Errorf("invalid vLLM metric value: %w", err)
}
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
continue
}
if metrics[name] == nil {
metrics[name] = map[string]float64{}
}
labels := re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(match[2], -1)
parts := make([]string, 0, len(labels))
for _, label := range labels {
parts = append(parts, label[1]+"="+label[2])
}
sort.Strings(parts)
metrics[name]["{"+strings.Join(parts, ",")+"}"] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(metrics) == 0 {
return nil, fmt.Errorf("no supported vLLM metrics")
}
return metrics, nil
}
func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, error) {
result := model.MonitorVLLM{}
for name, target := range map[string]**float64{
"num_requests_running": &result.Running,
"num_requests_waiting": &result.Waiting,
} {
if len(current[name]) == 0 {
continue
}
value := 0.0
for _, sample := range current[name] {
value += sample
}
*target = &value
}
cache := current["kv_cache_usage_perc"]
if len(cache) == 0 {
cache = current["gpu_cache_usage_perc"]
}
if len(cache) > 0 {
value := 0.0
for _, sample := range cache {
value += sample
}
value = value / float64(len(cache)) * 100
result.CacheUsage = &value
}
if elapsed <= 0 {
return result, nil
}
for name, target := range map[string]**float64{
"prompt_tokens_total": &result.PromptThroughput,
"generation_tokens_total": &result.GenerationThroughput,
"request_success_total": &result.RequestThroughput,
} {
if value := counterDelta(current[name], previous[name]); value != nil {
rate := *value / elapsed
*target = &rate
}
}
histograms := map[string]histogram{}
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} {
source := name
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
source = "time_per_output_token_seconds"
}
sum := counterDelta(current[source+"_sum"], previous[source+"_sum"])
count := counterDelta(current[source+"_count"], previous[source+"_count"])
if sum == nil || count == nil {
continue
}
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)}
}
applyHistograms(&result, histograms)
encoded, err := json.Marshal(histograms)
if err != nil {
return model.MonitorVLLM{}, err
}
result.HistogramDeltas = string(encoded)
return result, nil
}
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error {
var samples []map[string]histogram
if err := json.Unmarshal([]byte(encoded), &samples); err != nil {
return err
}
merged := map[string]histogram{}
for _, sample := range samples {
for name, item := range sample {
if item.Count == 0 {
continue
}
total, exists := merged[name]
if !exists {
merged[name] = item
continue
}
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
merged[name] = total
}
}
applyHistograms(result, merged)
return nil
}
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
for _, item := range []struct {
name string
mean **float64
quantiles []**float64
}{
{"time_to_first_token_seconds", &result.TimeToFirstToken, []**float64{&result.TimeToFirstTokenP50, &result.TimeToFirstTokenP90, &result.TimeToFirstTokenP95, &result.TimeToFirstTokenP99}},
{"inter_token_latency_seconds", &result.TimePerOutputToken, []**float64{&result.TimePerOutputTokenP50, &result.TimePerOutputTokenP90, &result.TimePerOutputTokenP95, &result.TimePerOutputTokenP99}},
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
{"request_prefill_time_seconds", &result.PrefillTime, nil},
{"request_decode_time_seconds", &result.DecodeTime, nil},
} {
histogram, ok := histograms[item.name]
if !ok || histogram.Count <= 0 {
continue
}
mean := histogram.Sum / histogram.Count
*item.mean = &mean
if len(item.quantiles) == 0 {
continue
}
quantiles := histogramQuantiles(histogram.Buckets)
for i, target := range item.quantiles {
*target = quantiles[i]
}
}
}
func counterDelta(current, previous map[string]float64) *float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
value := 0.0
for label, sample := range current {
before, ok := previous[label]
if !ok || sample < before {
return nil
}
value += sample - before
}
if math.IsInf(value, 0) || math.IsNaN(value) {
return nil
}
return &value
}
func bucketDeltas(current, previous map[string]float64, count float64) map[string]float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
groups := map[string]map[string]float64{}
for labels, sample := range current {
before, ok := previous[labels]
if !ok || sample < before {
return nil
}
bound := ""
var identity []string
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
if label[1] == "le" {
raw, err := strconv.Unquote(label[2])
if err != nil {
return nil
}
upper, err := strconv.ParseFloat(raw, 64)
if err != nil || math.IsNaN(upper) || upper < 0 {
return nil
}
bound = strconv.FormatFloat(upper, 'g', -1, 64)
} else {
identity = append(identity, label[1]+"="+label[2])
}
}
if bound == "" {
return nil
}
group := strings.Join(identity, ",")
if groups[group] == nil {
groups[group] = map[string]float64{}
}
if _, exists := groups[group][bound]; exists {
return nil
}
groups[group][bound] = sample - before
}
result := map[string]float64{}
for _, buckets := range groups {
if _, ok := buckets["+Inf"]; !ok || len(buckets) < 2 {
return nil
}
if len(result) > 0 && len(result) != len(buckets) {
return nil
}
for bound := range result {
if _, ok := buckets[bound]; !ok {
return nil
}
}
bounds := make([]float64, 0, len(buckets))
for bound := range buckets {
value, _ := strconv.ParseFloat(bound, 64)
bounds = append(bounds, value)
}
sort.Float64s(bounds)
previousCount := 0.0
for _, bound := range bounds {
key := strconv.FormatFloat(bound, 'g', -1, 64)
value := buckets[key]
if value < previousCount {
if previousCount-value > 1e-12*(previousCount+value) {
return nil
}
value = previousCount
}
result[key] += value
previousCount = value
}
}
if math.Abs(result["+Inf"]-count) > 1e-12*(result["+Inf"]+count) {
return nil
}
return result
}
func histogramQuantiles(buckets map[string]float64) [4]*float64 {
var quantiles [4]*float64
if len(buckets) < 2 || buckets["+Inf"] <= 0 {
return quantiles
}
bounds := make([]float64, 0, len(buckets)-1)
for bound := range buckets {
value, err := strconv.ParseFloat(bound, 64)
if err != nil || math.IsNaN(value) || value < 0 {
return quantiles
}
if !math.IsInf(value, 1) {
bounds = append(bounds, value)
}
}
if len(bounds) == 0 {
return quantiles
}
sort.Float64s(bounds)
for i, q := range [...]float64{0.5, 0.9, 0.95, 0.99} {
rank := q * buckets["+Inf"]
lower, before := 0.0, 0.0
value := bounds[len(bounds)-1]
for _, upper := range bounds {
count := buckets[strconv.FormatFloat(upper, 'g', -1, 64)]
if count >= rank && count > before {
value = lower + (upper-lower)*(rank-before)/(count-before)
break
}
lower, before = upper, count
}
quantiles[i] = &value
}
return quantiles
}
+10 -2
View File
@@ -97,7 +97,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}()
defer close(done)
client, err := sftp.NewClient(sshClient)
client, err := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true))
if err != nil {
return false, err
}
@@ -127,7 +127,15 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
// ReadFrom uses the pipelined write path of pkg/sftp. A plain io.Copy would
// pick os.File.WriteTo (the source is an *os.File), which issues one
// synchronous SFTP WRITE per packet and waits for the reply every time, so
// the throughput is bounded by packetSize/RTT.
written, err := dstFile.ReadFrom(srcFile)
if err != nil {
// A failed concurrent write may leave holes in the remote file, so cut
// it back to the number of bytes actually written.
_ = dstFile.Truncate(written)
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
+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)
}
}
+4
View File
@@ -6,6 +6,8 @@ import (
)
const (
VLLMMetricSamplePattern = `^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{(?:[^"\\]|"(?:\\.|[^"\\])*")*\})?\s+(\S+)(?:\s+.*)?$`
VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")`
NumberAlphaPattern = `(\d+)([A-Za-z]+)`
ComposeDisallowedCharsPattern = `[^a-z0-9_-]+`
ComposeNamePattern = `^[a-z0-9][a-z0-9_-]{0,255}$`
@@ -61,6 +63,8 @@ var regexMap = make(map[string]*regexp.Regexp)
func Init() {
patterns := []string{
VLLMMetricSamplePattern,
VLLMMetricLabelPattern,
NumberAlphaPattern,
ComposeDisallowedCharsPattern,
ComposeNamePattern,
+92 -1
View File
@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/model"
@@ -25,6 +26,7 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/req_helper"
upgradeUtil "github.com/1Panel-dev/1Panel/core/utils/upgrade"
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"golang.org/x/net/html"
)
type serviceInfo struct {
@@ -299,7 +301,7 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
docSource, _ := settingRepo.GetValueByKey("DocSource")
lang, _ := settingRepo.GetValueByKey("Language")
var notes []dto.ReleasesNotes
url := "https://1panel.cn/docs/v2/search/search_index.json"
url := "https://docs.fit2cloud.com/1panel/changelog/"
useIntlDocs := false
lang = strings.ToLower(strings.TrimSpace(lang))
if docSource == "withByRegion" {
@@ -315,6 +317,12 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
return notes, err
}
defer resp.Body.Close()
if !useIntlDocs {
if resp.StatusCode != http.StatusOK {
return notes, fmt.Errorf("load release notes failed: HTTP %d", resp.StatusCode)
}
return parseReleaseHTML(resp.Body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return notes, err
@@ -336,6 +344,89 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
return notes, nil
}
func parseReleaseHTML(reader io.Reader) ([]dto.ReleasesNotes, error) {
doc, err := html.Parse(reader)
if err != nil {
return nil, err
}
var article *html.Node
for node := range doc.Descendants() {
if node.Type == html.ElementNode && node.Data == "article" {
article = node
break
}
}
if article == nil {
return nil, fmt.Errorf("release notes article not found")
}
textContent := func(node *html.Node) string {
var text strings.Builder
for child := range node.Descendants() {
if child.Type == html.TextNode {
text.WriteString(child.Data)
}
}
return strings.TrimSpace(strings.ReplaceAll(text.String(), "\u200b", ""))
}
var notes []dto.ReleasesNotes
for heading := range article.Descendants() {
if heading.Type != html.ElementNode || heading.Data != "h3" {
continue
}
version := textContent(heading)
if !strings.HasPrefix(version, "v") {
continue
}
item := dto.ReleasesNotes{Version: version}
var content strings.Builder
section := ""
for node := heading.NextSibling; node != nil; node = node.NextSibling {
if node.Type != html.ElementNode {
continue
}
if node.Data == "h1" || node.Data == "h2" || node.Data == "h3" {
break
}
if item.CreatedAt == "" {
date := textContent(node)
if _, err := time.Parse("2006年1月2日", date); node.Data != "p" || err != nil {
return nil, fmt.Errorf("release date not found for %s", version)
}
item.CreatedAt = date
continue
}
if node.Data == "p" {
section = textContent(node)
}
for child := range node.Descendants() {
if child.Type != html.ElementNode || child.Data != "li" {
continue
}
switch section {
case "新增功能":
item.NewCount++
case "功能优化":
item.OptimizationCount++
case "问题修复":
item.FixCount++
}
}
if err := html.Render(&content, node); err != nil {
return nil, err
}
}
if item.CreatedAt == "" || content.Len() == 0 {
return nil, fmt.Errorf("release notes content not found for %s", version)
}
item.Content = content.String()
notes = append(notes, item)
}
if len(notes) == 0 {
return nil, fmt.Errorf("release notes versions not found")
}
return notes, nil
}
func analyzeDoc(version, content string) dto.ReleasesNotes {
var item dto.ReleasesNotes
parts := strings.Split(content, "<p>")
+31 -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"
@@ -2486,6 +2493,17 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
@@ -4028,12 +4046,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 -5
View File
@@ -1,6 +1,6 @@
module github.com/1Panel-dev/1Panel/core
go 1.26.1
go 1.26.6
require (
github.com/beevik/etree v1.7.0
@@ -39,8 +39,8 @@ require (
github.com/swaggo/files/v2 v2.0.2
github.com/swaggo/swag v1.16.6
github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.55.0
golang.org/x/image v0.41.0
golang.org/x/crypto v0.56.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0
@@ -129,9 +129,9 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
+8 -10
View File
@@ -124,8 +124,6 @@ github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -304,13 +302,13 @@ golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
@@ -335,8 +333,8 @@ golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
VMCreateTask: "Create virtual machine [{{ .name }}]"
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
VMUpdateTask: "Update virtual machine [{{ .name }}]"
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "شبکه ماشین مجازی وجود ندارد: {{ .d
ErrVMInvalidPath: "مسیر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMInvalidInput: "پارامتر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMCommandFailed: "اجرای دستور ماشین مجازی ناموفق بود: {{ .err }}"
ErrVMDiskResizeBlocked: "دیسک شرایط افزایش ظرفیت را ندارد یا دستگاه دیگری به آن ارجاع می‌دهد. وضعیت دیسک را بررسی کنید."
ErrVMDiskReferenced: "این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود."
ErrVMDiskReferenceCheck: "بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید."
ErrVMSnapshotMetadataSync: "اسنپ‌شات بازیابی شد، اما همگام‌سازی اطلاعات دیسک ناموفق بود. تازه‌سازی و بررسی کنید؛ نیازی به بازیابی دوباره اسنپ‌شات نیست."
VMCreateTask: "ایجاد ماشین مجازی [{{ .name }}]"
VMCreateFromTemplateTask: "ایجاد ماشین مجازی [{{ .name }}] از قالب"
VMUpdateTask: "به‌روزرسانی ماشین مجازی [{{ .name }}]"
VMDiskResizeTask: "افزایش ظرفیت دیسک [{{ .disk }}] ماشین مجازی [{{ .name }}]"
VMRenameTask: "تغییر نام ماشین مجازی [{{ .name }}] به [{{ .newName }}]"
VMDeleteTask: "حذف ماشین مجازی [{{ .name }}]"
VMTemplateCreateTask: "ذخیره ماشین مجازی [{{ .name }}] به عنوان قالب [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "仮想マシンネットワークが存在しません: {
ErrVMInvalidPath: "仮想マシンのパスが無効です: {{ .detail }}"
ErrVMInvalidInput: "仮想マシンのパラメータが無効です: {{ .detail }}"
ErrVMCommandFailed: "仮想マシンコマンドの実行に失敗しました: {{ .err }}"
ErrVMDiskResizeBlocked: "ディスクが拡張条件を満たしていないか、他のデバイスから参照されています。ディスクの状態を確認してください。"
ErrVMDiskReferenced: "この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。"
ErrVMDiskReferenceCheck: "仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。"
ErrVMSnapshotMetadataSync: "スナップショットは復元されましたが、ディスク情報の同期に失敗しました。更新して確認してください。再度復元する必要はありません。"
VMCreateTask: "仮想マシン [{{ .name }}] を作成"
VMCreateFromTemplateTask: "テンプレートから仮想マシン [{{ .name }}] を作成"
VMUpdateTask: "仮想マシン [{{ .name }}] を更新"
VMDiskResizeTask: "仮想マシン [{{ .name }}] のディスク [{{ .disk }}] を拡張"
VMRenameTask: "仮想マシン [{{ .name }}] を [{{ .newName }}] にリネーム"
VMDeleteTask: "仮想マシン [{{ .name }}] を削除"
VMTemplateCreateTask: "仮想マシン [{{ .name }}] をテンプレート [{{ .template }}] として保存"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "가상 머신 네트워크가 존재하지 않습니다:
ErrVMInvalidPath: "가상 머신 경로가 유효하지 않습니다: {{ .detail }}"
ErrVMInvalidInput: "가상 머신 매개변수가 유효하지 않습니다: {{ .detail }}"
ErrVMCommandFailed: "가상 머신 명령 실행 실패: {{ .err }}"
ErrVMDiskResizeBlocked: "디스크가 확장 조건을 충족하지 않거나 다른 장치에서 참조하고 있습니다. 디스크 상태를 확인하세요."
ErrVMDiskReferenced: "다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다."
ErrVMDiskReferenceCheck: "가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요."
ErrVMSnapshotMetadataSync: "스냅샷은 복원되었지만 디스크 정보 동기화에 실패했습니다. 새로 고쳐 확인하세요. 스냅샷을 다시 복원할 필요는 없습니다."
VMCreateTask: "가상 머신 [{{ .name }}] 생성"
VMCreateFromTemplateTask: "템플릿에서 가상 머신 [{{ .name }}] 생성"
VMUpdateTask: "가상 머신 [{{ .name }}] 업데이트"
VMDiskResizeTask: "가상 머신 [{{ .name }}]의 디스크 [{{ .disk }}] 확장"
VMRenameTask: "가상 머신 [{{ .name }}]을(를) [{{ .newName }}](으)로 이름 변경"
VMDeleteTask: "가상 머신 [{{ .name }}] 삭제"
VMTemplateCreateTask: "가상 머신 [{{ .name }}]을(를) 템플릿 [{{ .template }}](으)로 저장"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "ບໍ່ມີເຄືອຂ່າຍເຄື່ອງ
ErrVMInvalidPath: "ເສັ້ນທາງເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMInvalidInput: "ພາລາມິເຕີເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMCommandFailed: "ຄຳສັ່ງເຄື່ອງສະເໝືອນລົ້ມເຫຼວ: {{ .err }}"
ErrVMDiskResizeBlocked: "ດິສກ໌ບໍ່ຕອບສະໜອງເງື່ອນໄຂການຂະຫຍາຍ ຫຼື ຖືກອ້າງອີງໂດຍອຸປະກອນອື່ນ. ກະລຸນາກວດສອບສະຖານະດິສກ໌."
ErrVMDiskReferenced: "ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ."
ErrVMDiskReferenceCheck: "ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ."
ErrVMSnapshotMetadataSync: "ກູ້ຄືນສະແນັບຊັອດແລ້ວ ແຕ່ຊິງຂໍ້ມູນດິສກ໌ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໃໝ່ ແລະ ກວດສອບ; ບໍ່ຈຳເປັນຕ້ອງກູ້ຄືນສະແນັບຊັອດອີກ."
VMCreateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMCreateFromTemplateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກແມ່ແບບ"
VMUpdateTask: "ອັບເດດເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMDiskResizeTask: "ຂະຫຍາຍດິສກ໌ [{{ .disk }}] ຂອງເຄື່ອງເສມືອນ [{{ .name }}]"
VMRenameTask: "ປ່ຽນຊື່ເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນ [{{ .newName }}]"
VMDeleteTask: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMTemplateCreateTask: "ບັນທຶກເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນແມ່ແບບ [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Rangkaian mesin maya tidak wujud: {{ .detail }}"
ErrVMInvalidPath: "Laluan mesin maya tidak sah: {{ .detail }}"
ErrVMInvalidInput: "Parameter mesin maya tidak sah: {{ .detail }}"
ErrVMCommandFailed: "Arahan mesin maya gagal: {{ .err }}"
ErrVMDiskResizeBlocked: "Cakera tidak memenuhi syarat pembesaran atau dirujuk oleh peranti lain. Semak status cakera."
ErrVMDiskReferenced: "Cakera maya ini digunakan oleh mesin maya atau peranti lain; pembesaran belum disokong."
ErrVMDiskReferenceCheck: "Rujukan cakera maya tidak dapat disahkan. Pembesaran tidak tersedia; semak cakera mesin maya lain."
ErrVMSnapshotMetadataSync: "Syot kilat telah dipulihkan, tetapi maklumat cakera gagal disegerakkan. Muat semula dan semak; tidak perlu memulihkan syot kilat sekali lagi."
VMCreateTask: "Cipta mesin maya [{{ .name }}]"
VMCreateFromTemplateTask: "Cipta mesin maya [{{ .name }}] daripada templat"
VMUpdateTask: "Kemas kini mesin maya [{{ .name }}]"
VMDiskResizeTask: "Besarkan cakera [{{ .disk }}] mesin maya [{{ .name }}]"
VMRenameTask: "Namakan semula mesin maya [{{ .name }}] kepada [{{ .newName }}]"
VMDeleteTask: "Padam mesin maya [{{ .name }}]"
VMTemplateCreateTask: "Simpan mesin maya [{{ .name }}] sebagai templat [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Rede da máquina virtual não existe: {{ .detail }}"
ErrVMInvalidPath: "Caminho da máquina virtual inválido: {{ .detail }}"
ErrVMInvalidInput: "Parâmetro da máquina virtual inválido: {{ .detail }}"
ErrVMCommandFailed: "Falha no comando da máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "O disco não atende aos requisitos de expansão ou é referenciado por outro dispositivo. Verifique seu estado."
ErrVMDiskReferenced: "Este disco virtual é usado por outra máquina virtual ou dispositivo e ainda não pode ser expandido."
ErrVMDiskReferenceCheck: "Não foi possível verificar as referências ao disco virtual. A expansão está indisponível; verifique os discos das outras máquinas virtuais."
ErrVMSnapshotMetadataSync: "O snapshot foi restaurado, mas a sincronização dos dados do disco falhou. Atualize e verifique; não restaure o snapshot novamente."
VMCreateTask: "Criar máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Criar máquina virtual [{{ .name }}] a partir do modelo"
VMUpdateTask: "Atualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Expandir disco [{{ .disk }}] da máquina virtual [{{ .name }}]"
VMRenameTask: "Renomear máquina virtual [{{ .name }}] para [{{ .newName }}]"
VMDeleteTask: "Excluir máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Salvar máquina virtual [{{ .name }}] como modelo [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Сеть виртуальной машины не сущ
ErrVMInvalidPath: "Недопустимый путь виртуальной машины: {{ .detail }}"
ErrVMInvalidInput: "Недопустимые параметры виртуальной машины: {{ .detail }}"
ErrVMCommandFailed: "Ошибка выполнения команды виртуальной машины: {{ .err }}"
ErrVMDiskResizeBlocked: "Диск не соответствует условиям расширения или используется другим устройством. Проверьте состояние диска."
ErrVMDiskReferenced: "Этот виртуальный диск используется другой виртуальной машиной или устройством; расширение пока недоступно."
ErrVMDiskReferenceCheck: "Не удалось проверить ссылки на виртуальный диск. Расширение недоступно; проверьте диски других виртуальных машин."
ErrVMSnapshotMetadataSync: "Снимок восстановлен, но синхронизация данных диска не удалась. Обновите данные и проверьте их; повторно восстанавливать снимок не нужно."
VMCreateTask: "Создать виртуальную машину [{{ .name }}]"
VMCreateFromTemplateTask: "Создать виртуальную машину [{{ .name }}] из шаблона"
VMUpdateTask: "Обновить виртуальную машину [{{ .name }}]"
VMDiskResizeTask: "Расширить диск [{{ .disk }}] виртуальной машины [{{ .name }}]"
VMRenameTask: "Переименовать виртуальную машину [{{ .name }}] в [{{ .newName }}]"
VMDeleteTask: "Удалить виртуальную машину [{{ .name }}]"
VMTemplateCreateTask: "Сохранить виртуальную машину [{{ .name }}] как шаблон [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Sanal makine ağı mevcut değil: {{ .detail }}"
ErrVMInvalidPath: "Geçersiz sanal makine yolu: {{ .detail }}"
ErrVMInvalidInput: "Geçersiz sanal makine parametresi: {{ .detail }}"
ErrVMCommandFailed: "Sanal makine komutu başarısız: {{ .err }}"
ErrVMDiskResizeBlocked: "Disk genişletme koşullarını karşılamıyor veya başka bir aygıt tarafından kullanılıyor. Disk durumunu kontrol edin."
ErrVMDiskReferenced: "Bu sanal disk başka bir sanal makine veya aygıt tarafından kullanılıyor; genişletme henüz desteklenmiyor."
ErrVMDiskReferenceCheck: "Sanal disk başvuruları doğrulanamadı. Genişletme kullanılamıyor; diğer sanal makinelerin disklerini kontrol edin."
ErrVMSnapshotMetadataSync: "Anlık görüntü geri yüklendi ancak disk bilgileri eşitlenemedi. Yenileyip kontrol edin; anlık görüntüyü tekrar geri yüklemeyin."
VMCreateTask: "Sanal makine [{{ .name }}] oluştur"
VMCreateFromTemplateTask: "Şablondan sanal makine [{{ .name }}] oluştur"
VMUpdateTask: "Sanal makine [{{ .name }}] güncelle"
VMDiskResizeTask: "[{{ .name }}] sanal makinesinin [{{ .disk }}] diskini genişlet"
VMRenameTask: "Sanal makine [{{ .name }}] adını [{{ .newName }}] olarak değiştir"
VMDeleteTask: "Sanal makine [{{ .name }}] sil"
VMTemplateCreateTask: "Sanal makine [{{ .name }}] öğesini şablon [{{ .template }}] olarak kaydet"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "虛擬機網路不存在: {{ .detail }}"
ErrVMInvalidPath: "虛擬機路徑非法: {{ .detail }}"
ErrVMInvalidInput: "虛擬機參數非法: {{ .detail }}"
ErrVMCommandFailed: "虛擬機指令執行失敗: {{ .err }}"
ErrVMDiskResizeBlocked: "磁碟不符合擴容條件,或被其他裝置引用,請檢查磁碟狀態"
ErrVMDiskReferenced: "此虛擬硬碟被其他虛擬機或裝置使用,暫不支援擴容"
ErrVMDiskReferenceCheck: "無法完成虛擬硬碟引用檢查,暫不支援擴容,請檢查其他虛擬機的磁碟狀態"
ErrVMSnapshotMetadataSync: "快照已還原,但磁碟資訊同步失敗,請重新整理檢查,無需重複還原快照"
VMCreateTask: "建立虛擬機 [{{ .name }}]"
VMCreateFromTemplateTask: "從範本建立虛擬機 [{{ .name }}]"
VMUpdateTask: "更新虛擬機 [{{ .name }}]"
VMDiskResizeTask: "擴容虛擬機 [{{ .name }}] 磁碟 [{{ .disk }}]"
VMRenameTask: "重新命名虛擬機 [{{ .name }}] 為 [{{ .newName }}]"
VMDeleteTask: "刪除虛擬機 [{{ .name }}]"
VMTemplateCreateTask: "轉存虛擬機 [{{ .name }}] 為範本 [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "虚拟机网络不存在: {{ .detail }}"
ErrVMInvalidPath: "虚拟机路径非法: {{ .detail }}"
ErrVMInvalidInput: "虚拟机参数非法: {{ .detail }}"
ErrVMCommandFailed: "虚拟机命令执行失败: {{ .err }}"
ErrVMDiskResizeBlocked: "磁盘不符合扩容条件,或被其他设备引用,请检查磁盘状态"
ErrVMDiskReferenced: "该虚拟硬盘被其他虚拟机或设备使用,暂不支持扩容"
ErrVMDiskReferenceCheck: "无法完成虚拟硬盘引用检查,暂不支持扩容,请检查其他虚拟机的磁盘状态"
ErrVMSnapshotMetadataSync: "快照已恢复,但磁盘信息同步失败,请刷新检查,无需重复恢复快照"
VMCreateTask: "创建虚拟机 [{{ .name }}]"
VMCreateFromTemplateTask: "从模板创建虚拟机 [{{ .name }}]"
VMUpdateTask: "更新虚拟机 [{{ .name }}]"
VMDiskResizeTask: "扩容虚拟机 [{{ .name }}] 磁盘 [{{ .disk }}]"
VMRenameTask: "重命名虚拟机 [{{ .name }}] 为 [{{ .newName }}]"
VMDeleteTask: "删除虚拟机 [{{ .name }}]"
VMTemplateCreateTask: "转存虚拟机 [{{ .name }}] 为模板 [{{ .template }}]"
+1
View File
@@ -87,6 +87,7 @@ func Routers() *gin.Engine {
}
Router.Use(middleware.FrontendFallback())
Router.Use(middleware.WebSocketOriginGuard())
Router.Use(middleware.OperationLog())
Router.Use(middleware.GlobalLoading())
Router.Use(xpack.AuthProvider.CoreAPIAuthMiddleware())
+41
View File
@@ -0,0 +1,41 @@
package middleware
import (
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
func WebSocketOriginGuard() gin.HandlerFunc {
return func(c *gin.Context) {
if !websocket.IsWebSocketUpgrade(c.Request) {
c.Next()
return
}
if site := c.GetHeader("Sec-Fetch-Site"); site != "" && site != "same-origin" {
c.AbortWithStatus(http.StatusForbidden)
return
}
origins := c.Request.Header.Values("Origin")
if len(origins) == 0 {
c.Next()
return
}
if len(origins) != 1 {
c.AbortWithStatus(http.StatusForbidden)
return
}
origin, err := url.Parse(origins[0])
if err != nil || (origin.Scheme != "http" && origin.Scheme != "https") ||
origin.Host == "" || !strings.EqualFold(origin.Host, c.Request.Host) ||
origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.ForceQuery || origin.Fragment != "" {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
+2 -2
View File
@@ -53,11 +53,11 @@
$ bash -c "$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"
如果是用于离线环境,推荐使用 [安装包方式](https://1panel.cn/docs/v2/installation/enterprise_installation/) 进行安装部署。
如果是用于离线环境,推荐使用 [安装包方式](https://docs.fit2cloud.com/1panel/installation/enterprise-installation/) 进行安装部署。
**学习资料**
- [在线文档](https://1panel.cn/docs/)
- [在线文档](https://docs.fit2cloud.com/1panel/)
- [社区论坛](https://bbs.fit2cloud.com/c/1p/7)
## 版本对比
+2 -2
View File
@@ -55,11 +55,11 @@
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
```
如果是用於離線環境,推薦使用 [安裝包方式](https://1panel.cn/docs/installation/package_installation/) 進行安裝部署。
如果是用於離線環境,推薦使用 [安裝包方式](https://docs.fit2cloud.com/1panel/installation/enterprise-installation/) 進行安裝部署。
**學習資料**
- [線上文件](https://1panel.cn/docs/)
- [線上文件](https://docs.fit2cloud.com/1panel/)
- [社區論壇](https://bbs.fit2cloud.com/c/1p/7)
- [如何加入微信交流群?](https://bbs.fit2cloud.com/t/topic/2147)
+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 客户端和临时任务数据库,不执行真实网络删除。
+83 -309
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -57,18 +57,18 @@
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.6",
"vue-i18n": "^11.4.4",
"vue-router": "^5.2.0"
"vue-router": "^5.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^26.4.0",
"@types/node": "^26.6.3",
"@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.68.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.6",
"@vue/compiler-sfc": "^3.5.42",
"autoprefixer": "^10.5.4",
"autoprefixer": "^10.6.1",
"esbuild": "^0.28.2",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
@@ -79,7 +79,7 @@
"postcss-html": "^2.0.0",
"prettier": "^3.9.6",
"rollup-plugin-visualizer": "^7.0.1",
"sass": "^1.103.1",
"sass": "^1.105.0",
"standard-version": "^9.5.0",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
@@ -88,7 +88,7 @@
"vite": "^8.2.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint2": "^5.3.0",
"vite-svg-loader": "^5.1.2",
"vite-svg-loader": "^5.1.3",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^3.3.9"
},
+1
View File
@@ -117,6 +117,7 @@ export namespace AI {
endTime: Date;
}
export interface MonitorGPUOptions {
supported: boolean;
gpuType: string;
options: Array<string>;
chartHide: Array<ChartHide>;
+6
View File
@@ -65,6 +65,12 @@ export namespace Host {
}
export interface MonitorSetting {
gpuMonitorStatus: string;
gpuMonitorStoreDays: string;
gpuMonitorInterval: string;
vllmMonitorStatus: string;
vllmMonitorStoreDays: string;
vllmMonitorInterval: string;
defaultNetwork: string;
defaultIO: string;
monitorStatus: string;
+13 -4
View File
@@ -41,11 +41,20 @@ export const updateBindDomain = (req: AI.BindDomain) => {
export const loadGPUInfo = () => {
return http.get<AI.Info>(`/ai/gpu/load`);
};
export const getGPUOptions = () => {
return http.get<AI.MonitorGPUOptions>(`/ai/gpu/options`);
export const getGPUOptions = (currentNode?: string) => {
return http.get<AI.MonitorGPUOptions>(
`/ai/gpu/options`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
};
export const loadGPUMonitor = (param: AI.MonitorGPUSearch) => {
return http.post<AI.MonitorGPUData>(`/ai/gpu/search`, param);
export const loadGPUMonitor = (param: AI.MonitorGPUSearch, currentNode?: string) => {
return http.post<AI.MonitorGPUData>(
`/ai/gpu/search`,
param,
TimeoutEnum.T_60S,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const pageMcpServer = (req: AI.McpServerSearch) => {
+19 -19
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}`);
@@ -164,6 +161,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);
};
+2 -2
View File
@@ -28,8 +28,8 @@ export const getIOOptions = (currentNode?: string) => {
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
};
export const cleanMonitors = () => {
return http.post(`/hosts/monitor/clean`, {});
export const cleanMonitors = (type: 'host' | 'gpu') => {
return http.post(`/hosts/monitor/clean`, { type });
};
export const loadMonitorSetting = (currentNode?: string) => {
return http.get<Host.MonitorSetting>(
@@ -11,6 +11,7 @@
<fu-table
v-if="currentViewMode === 'table'"
v-bind="$attrs"
:data="props.data"
ref="tableRef"
@select="handleSelect"
@selection-change="handleSelectionChange"
@@ -175,7 +176,7 @@ import { useResponsivePagination } from './useResponsivePagination';
import { useTableSelection } from './useTableSelection';
const slots = useSlots();
const attrs = useAttrs();
const { isMobile, openMenuTabs } = useGlobalStore();
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
defineOptions({ name: 'ComplexTable' });
export type DropdownProps = FuTableOperationButton;
@@ -184,6 +185,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 +242,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 +298,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 +307,6 @@ const {
selectedRows,
isRowSelected,
clearSelects,
pruneSelection,
toggleSelection,
selectRow,
syncTableSelection,
@@ -313,7 +315,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);
};
@@ -446,7 +459,26 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
flush: 'post',
});
watch(tableData, pruneSelection);
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
flush: 'sync',
});
watch(
[
currentNode,
() =>
JSON.stringify(
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
),
],
() => {
clearSelects();
if (props.paginationConfig) {
props.paginationConfig.currentPage = 1;
}
},
{ flush: 'sync' },
);
onBeforeUnmount(() => {
window.removeEventListener('resize', calcHeight);
@@ -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);
@@ -155,7 +184,6 @@ export const useTableSelection = (
selectedRows,
isRowSelected,
clearSelects,
pruneSelection,
toggleSelection,
selectRow,
syncTableSelection,
@@ -139,6 +139,7 @@ function initChart() {
data: item?.data,
showSymbol: item.showSymbol ?? false,
symbolSize: item.symbolSize,
sampling: 'lttb',
yAxisIndex: item.yAxisIndex ? 1 : null,
});
});
@@ -227,9 +228,10 @@ function initChart() {
};
const tooltip = props.option.tooltip || defaultTooltip;
const option = {
animation: (props.option.xData?.length ?? 0) <= 3000,
title: [
{
left: 'center',
left: props.option.titleLeft ?? 'center',
text: props.option.title,
show: props.option.title,
textStyle: {
@@ -257,6 +259,7 @@ function initChart() {
itemWidth: 8,
icon: 'circle',
...props.option.legend,
selected: { ...props.option.legend?.selected, ...itemSelect?.selected },
textStyle: {
...itemSelect?.textStyle,
...props.option.legend?.textStyle,
@@ -0,0 +1,63 @@
import { storeToRefs } from 'pinia';
import { watch, type Ref } from 'vue';
import GlobalStore from '@/store/modules/global';
type SearchValue = string | number | undefined;
type SearchFields = Record<string, Ref<SearchValue>>;
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
const { currentNode } = storeToRefs(GlobalStore());
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
let initialized = false;
let restoring = false;
let activeKey: string | undefined;
watch(
() => {
const context = scope?.();
if (scope && context === undefined) return undefined;
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
},
(key) => {
activeKey = key;
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 {}
restoring = true;
for (const [name, field] of Object.entries(fields)) {
const value = saved[name];
const initial = defaults[name];
let fallback = initial;
if (initialized) {
fallback = typeof initial === 'string' ? '' : undefined;
}
const valid =
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
(fallback === undefined || typeof value === typeof fallback);
if (value === null) {
field.value = typeof initial === 'string' ? '' : undefined;
} else if (valid) {
field.value = value as SearchValue;
} else {
field.value = fallback;
}
}
restoring = false;
initialized = true;
},
{ immediate: true, flush: 'sync' },
);
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 {}
},
{ flush: 'sync' },
);
};
+80 -1
View File
@@ -1507,6 +1507,34 @@ const message = {
ignoreEos: 'Ignore EOS',
extraHeaders: 'Extra Headers',
},
vllmMonitor: {
cleanHelper:
'Clear monitoring history only for this vLLM instance? Other instances, host and GPU monitoring are unaffected.',
overview: 'Overview',
latencyAnalysis: 'Latency analysis',
requests: 'Requests',
tokenThroughput: 'Token throughput',
running: 'Running requests',
waiting: 'Waiting requests',
cacheUsage: 'KV cache usage',
promptThroughput: 'Input token throughput',
generationThroughput: 'Output token throughput',
requestThroughput: 'Completed requests',
timeToFirstToken: 'Mean time to first token',
timePerOutputToken: 'Mean inter-token latency',
requestLatency: 'Mean request latency',
unavailable: 'Metrics are unavailable. Check that the instance is running and /metrics is accessible.',
loadFailed: 'Failed to load monitoring data. Please retry.',
helper: 'Manage collection, interval and retention in monitoring settings.',
cache: 'KV cache',
throughput: 'Request throughput',
phaseTime: 'Prefill and decode time',
prefillTime: 'Mean prefill time',
decodeTime: 'Mean decode time',
requestLatencyChart: 'End-to-end request latency',
timeToFirstTokenChart: 'Time to first token',
timePerOutputTokenChart: 'Inter-token latency',
},
gpu: {
memoryActivity: 'Memory read/write activity',
hotspotTemperature: 'Hotspot temperature',
@@ -2119,6 +2147,9 @@ const message = {
cleanLogscope: 'Log retention',
},
monitor: {
hostMonitor: 'Host Monitoring',
gpuMonitor: 'GPU Monitoring',
vllmMonitor: 'vLLM Monitoring',
globalFilter: 'Global Filter',
enableMonitor: 'Monitoring Status',
storeDays: 'Retention Days',
@@ -2127,7 +2158,7 @@ const message = {
defaultIO: 'Default Disk',
defaultIOHelper: 'Default disk option displayed in monitoring and overview interfaces',
cleanMonitor: 'Clear Monitoring Records',
cleanHelper: 'This operation will clear all monitoring records including GPU. Continue?',
cleanHelper: 'This operation will clear all history for {0}. Continue?',
avgLoad: 'Load average',
loadDetail: 'Load detail',
resourceUsage: 'Utilization',
@@ -2185,6 +2216,22 @@ const message = {
profileGoroutineDesc: 'Saves the state and call stack of every goroutine and groups identical call paths.',
profileMutexDesc: 'Measures wait time caused by mutex contention to locate lock contention hotspots.',
profileBlockDesc: 'Measures blocking on channels, select statements, and synchronization primitives.',
exportRange: 'Time range',
exportMetrics: 'Metrics',
exportMetricsHelper: 'CPU, memory and load metrics are merged into one table.',
exportAllDevices: 'Each device (per-device detail)',
exportAllInterfaces: 'Each interface (per-interface detail)',
exportFormat: 'Format',
exportCsvHelper: 'Multiple metrics are exported as separate CSV files packed into a ZIP archive.',
exportJsonHelper: 'Export as a single structured JSON file.',
exportXlsxHelper: 'Each metric is exported as a separate worksheet in one workbook.',
exportTruncated: 'Excel row limit reached; these datasets were truncated: {0}',
exportEmptyTables: 'No data in the selected time range for: {0}',
exportAggregate: 'Combined total (all devices)',
exportAllGPUs: 'Each model (per-model detail)',
exportOptionsFailed: 'Failed to load the device list for: {0}. Refresh the page and retry.',
selectMetrics: 'Select at least one metric to export.',
selectTimeRange: 'Select the time range to export.',
},
terminal: {
showTerminalButton: 'Terminal Shortcut',
@@ -2841,6 +2888,9 @@ const message = {
privateKey: 'Private Key',
publicKey: 'Public Key',
password: 'Password',
existingPassPhrase: 'Existing private key passphrase',
existingPassPhraseHelper:
'Enter the original passphrase if the private key is encrypted; otherwise leave blank. This does not change the passphrase.',
createMode: 'Creation Method',
generate: 'Auto-generate',
unSyncPass: 'Key password cannot be synchronized',
@@ -7146,9 +7196,34 @@ const message = {
orphan_snapshot: 'Orphaned Snapshot',
partial_file: 'Incomplete File',
},
disks: 'Disks',
resizeDisk: 'Expand',
diskTarget: 'Device',
diskCapacityAfterResize: 'Total capacity after expansion',
diskResizeHelper:
'Expansion only increases the virtual disk capacity. Afterwards, extend the partitions and filesystems inside the virtual machine.',
diskResizeLimit:
'Expansion requires a shut-off VM and an independent, writable qcow2 disk without snapshots or a backing chain. Shrinking is not supported.',
diskResizeReason: {
referenceCheck:
'Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines.',
referenced:
'This virtual disk is used by another virtual machine or device and cannot be expanded yet.',
running: 'Shut down the virtual machine before expanding the disk.',
saved: 'Resume the virtual machine, then shut it down normally before expanding.',
snapshots: 'Expansion is unavailable while snapshots exist to avoid affecting snapshot recovery.',
backing: 'The disk depends on another image; expansion is not supported yet.',
shared: 'The disk is marked as shared; expansion is unavailable to avoid affecting other users.',
unsupported: 'The disk format, location or access permissions do not support expansion.',
unavailable: 'Cannot verify disk status. Check it and refresh.',
},
diskSize: 'Disk Size',
isoHelper:
'Mount multiple ISOs. Select the operating system installer first, then driver images such as VirtIO. Each image uses a separate CD-ROM drive.',
windowsDriverHelper:
'The Windows image may not include the VirtIO network driver (NetKVM). Mount a VirtIO driver ISO and install the driver in Windows. Ignore this if the image already includes the driver.',
windowsDiskDriverHelper:
'VirtIO disks may also require loading the disk driver (viostor); otherwise, Windows Setup may not detect the disk.',
osType: 'Operating System Type',
osOther: 'Other',
diskBus: 'Disk Bus',
@@ -7200,6 +7275,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Console',
consoleShortcutKeys: 'Shortcut keys',
consoleCustomKeys: 'Custom key combination',
consoleMainKey: 'Select main key',
consoleSendKeys: 'Send',
consoleNotReady: 'The VNC console is unavailable. Start the VM and check its VNC graphics configuration.',
consoleDisconnected: 'The VNC console has been disconnected.',
consoleConnectFailed: 'Failed to connect to the VNC console. Check the console proxy service.',
+81 -1
View File
@@ -1516,6 +1516,35 @@ const message = {
ignoreEos: 'Ignorar EOS',
extraHeaders: 'Encabezados adicionales',
},
vllmMonitor: {
cleanHelper:
'¿Borrar solo el historial de monitoreo de esta instancia de vLLM? No afecta a otras instancias ni al monitoreo del host o de GPU.',
overview: 'Resumen',
latencyAnalysis: 'Análisis de latencia',
requests: 'Solicitudes',
tokenThroughput: 'Rendimiento de tokens',
running: 'Solicitudes en ejecución',
waiting: 'Solicitudes en espera',
cacheUsage: 'Uso de caché KV',
promptThroughput: 'Rendimiento de tokens de entrada',
generationThroughput: 'Rendimiento de tokens de salida',
requestThroughput: 'Solicitudes completadas',
timeToFirstToken: 'Latencia media del primer token',
timePerOutputToken: 'Latencia media entre tokens',
requestLatency: 'Latencia media de solicitud',
unavailable:
'Métricas no disponibles. Compruebe que la instancia esté en ejecución y que /metrics sea accesible.',
loadFailed: 'No se pudieron cargar los datos. Inténtelo de nuevo.',
helper: 'Configura la recopilación, el intervalo y la retención en los ajustes de monitorización.',
cache: 'Caché KV',
throughput: 'Solicitudes/s',
phaseTime: 'Tiempo de precarga y decodificación',
prefillTime: 'Tiempo medio de precarga',
decodeTime: 'Tiempo medio de decodificación',
requestLatencyChart: 'Latencia de solicitud de extremo a extremo',
timeToFirstTokenChart: 'Tiempo hasta el primer token',
timePerOutputTokenChart: 'Latencia entre tokens',
},
gpu: {
memoryActivity: 'Actividad de lectura/escritura de memoria',
hotspotTemperature: 'Temperatura del punto caliente',
@@ -2155,6 +2184,9 @@ const message = {
cleanLogscope: 'Retención de logs',
},
monitor: {
hostMonitor: 'Monitoreo del host',
gpuMonitor: 'Monitoreo de GPU',
vllmMonitor: 'Monitoreo de vLLM',
globalFilter: 'Filtro Global',
enableMonitor: 'Estado de Monitoreo',
storeDays: 'Días de Retención',
@@ -2164,7 +2196,7 @@ const message = {
defaultIO: 'Disco Predeterminado',
defaultIOHelper: 'Opción de disco predeterminada mostrada en las interfaces de monitoreo y resumen',
cleanMonitor: 'Limpiar Registros de Monitoreo',
cleanHelper: 'Esta operación borrará todos los registros de monitoreo, incluidos los de GPU. ¿Continuar?',
cleanHelper: 'Esta operación borrará todo el historial de {0}. ¿Continuar?',
avgLoad: 'Carga promedio',
loadDetail: 'Detalle de carga',
resourceUsage: 'Utilización',
@@ -2223,6 +2255,22 @@ const message = {
profileMutexDesc:
'Mide el tiempo de espera causado por la contención de mutex para localizar bloqueos disputados.',
profileBlockDesc: 'Mide los bloqueos en canales, select y primitivas de sincronización.',
exportRange: 'Intervalo de tiempo',
exportMetrics: 'Métricas',
exportMetricsHelper: 'Las métricas de CPU, memoria y carga se combinan en una sola tabla.',
exportAllDevices: 'Cada dispositivo (detalle por dispositivo)',
exportAllInterfaces: 'Cada interfaz (detalle por interfaz)',
exportFormat: 'Formato',
exportCsvHelper: 'Al seleccionar varias métricas se generan CSV separados empaquetados en un ZIP.',
exportJsonHelper: 'Exportar como un único archivo JSON estructurado.',
exportXlsxHelper: 'Cada métrica se exporta en una hoja independiente.',
exportTruncated: 'Se superó el límite de filas de Excel; estos conjuntos se truncaron: {0}',
exportEmptyTables: 'Sin datos en el rango seleccionado para: {0}',
exportAggregate: 'Total combinado (todos los dispositivos)',
exportAllGPUs: 'Cada modelo (detalle por modelo)',
exportOptionsFailed: 'No se pudo cargar la lista de dispositivos para: {0}. Actualiza la página y reintenta.',
selectMetrics: 'Selecciona al menos una métrica para exportar',
selectTimeRange: 'Selecciona el intervalo de tiempo a exportar',
},
terminal: {
showTerminalButton: 'Acceso rápido al terminal',
@@ -2882,6 +2930,9 @@ const message = {
privateKey: 'Clave privada',
publicKey: 'Clave pública',
password: 'Contraseña',
existingPassPhrase: 'Contraseña actual de la clave privada',
existingPassPhraseHelper:
'Introduce la contraseña original si la clave privada está cifrada; en caso contrario, deja el campo vacío. Esto no cambia la contraseña.',
createMode: 'Método de creación',
generate: 'Autogenerar',
unSyncPass: 'La contraseña de la clave no se puede sincronizar',
@@ -7259,9 +7310,34 @@ const message = {
orphan_snapshot: 'Instantánea huérfana',
partial_file: 'Archivo incompleto',
},
disks: 'Discos',
resizeDisk: 'Ampliar',
diskTarget: 'Dispositivo',
diskCapacityAfterResize: 'Capacidad total tras la ampliación',
diskResizeHelper:
'La ampliación solo aumenta la capacidad del disco virtual. Después, amplíe las particiones y los sistemas de archivos dentro de la máquina virtual.',
diskResizeLimit:
'La máquina virtual debe estar apagada y el disco qcow2 debe ser independiente y escribible, sin instantáneas ni cadena de imágenes de respaldo. No se admite reducir el tamaño.',
diskResizeReason: {
referenceCheck:
'No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales.',
referenced:
'Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite.',
running: 'Apague la máquina virtual antes de ampliar el disco.',
saved: 'Reanude la máquina virtual y apáguela normalmente antes de ampliar.',
snapshots: 'No se admite la ampliación con instantáneas para no afectar a su restauración.',
backing: 'El disco depende de otra imagen; la ampliación aún no se admite.',
shared: 'El disco está marcado como compartido; no se permite ampliarlo para no afectar a otros usuarios.',
unsupported: 'El formato, la ubicación o los permisos del disco no permiten ampliarlo.',
unavailable: 'No se puede verificar el estado del disco. Revise y actualice.',
},
diskSize: 'Tamaño del disco',
isoHelper:
'Puede montar varios ISO. Seleccione primero el instalador del sistema operativo y después las imágenes de controladores, como VirtIO. Cada imagen usa una unidad de CD-ROM independiente.',
windowsDriverHelper:
'Es posible que la imagen de Windows no incluya el controlador de red VirtIO (NetKVM). Monte una ISO de controladores VirtIO e instale el controlador en Windows. Omita este aviso si la imagen ya incluye el controlador.',
windowsDiskDriverHelper:
'Los discos VirtIO también pueden requerir que se cargue el controlador de disco (viostor); de lo contrario, el instalador de Windows podría no detectar el disco.',
osType: 'Tipo de sistema operativo',
osOther: 'Otro',
diskBus: 'Bus de disco',
@@ -7314,6 +7390,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Consola',
consoleShortcutKeys: 'Atajos de teclado',
consoleCustomKeys: 'Combinación personalizada',
consoleMainKey: 'Seleccionar tecla',
consoleSendKeys: 'Enviar',
consoleNotReady:
'La consola VNC no está disponible. Inicie la VM y compruebe su configuración gráfica VNC.',
consoleDisconnected: 'La consola VNC se ha desconectado.',
+81 -1
View File
@@ -1492,6 +1492,34 @@ const message = {
ignoreEos: 'نادیده گرفتن EOS',
extraHeaders: 'هدرهای اضافی',
},
vllmMonitor: {
cleanHelper:
'فقط سابقه نظارت این نمونه vLLM پاک شود؟ سایر نمونه\u200cها و نظارت میزبان و GPU تغییری نمی\u200cکنند.',
overview: 'نمای کلی',
latencyAnalysis: 'تحلیل تأخیر',
requests: 'تعداد درخواست‌ها',
tokenThroughput: 'توان عملیاتی توکن',
running: 'درخواست‌های در حال اجرا',
waiting: 'درخواست‌های در انتظار',
cacheUsage: 'مصرف حافظه نهان KV',
promptThroughput: 'گذردهی توکن ورودی',
generationThroughput: 'گذردهی توکن خروجی',
requestThroughput: 'نرخ درخواست‌های تکمیل‌شده',
timeToFirstToken: 'میانگین تأخیر توکن اول',
timePerOutputToken: 'میانگین تأخیر بین توکن‌ها',
requestLatency: 'میانگین تأخیر درخواست',
unavailable: 'معیارها در دسترس نیستند. از اجرای نمونه و دسترسی به /metrics مطمئن شوید.',
loadFailed: 'بارگیری داده‌های پایش ناموفق بود. دوباره تلاش کنید.',
helper: 'فعال‌سازی جمع‌آوری، فاصله زمانی و مدت نگهداری را در تنظیمات پایش تغییر دهید.',
cache: 'حافظه نهان KV',
throughput: 'گذردهی درخواست',
phaseTime: 'زمان پیش‌پرکردن و رمزگشایی',
prefillTime: 'میانگین زمان پیش‌پرکردن',
decodeTime: 'میانگین زمان رمزگشایی',
requestLatencyChart: 'تأخیر سرتاسری درخواست',
timeToFirstTokenChart: 'زمان تا توکن اول',
timePerOutputTokenChart: 'تأخیر بین توکن‌ها',
},
gpu: {
memoryActivity: 'فعالیت خواندن/نوشتن حافظه',
hotspotTemperature: 'دمای نقطه داغ',
@@ -2103,6 +2131,9 @@ const message = {
cleanLogscope: 'نگهداری لاگ',
},
monitor: {
hostMonitor: 'نظارت میزبان',
gpuMonitor: 'نظارت GPU',
vllmMonitor: 'نظارت vLLM',
globalFilter: 'فیلتر جهانی',
enableMonitor: 'وضعیت نظارت',
storeDays: 'روزهای نگهداری',
@@ -2111,7 +2142,7 @@ const message = {
defaultIO: 'دیسک پیش‌فرض',
defaultIOHelper: 'گزینه دیسک پیش‌فرض که در رابط‌های نظارت و نمای کلی نمایش داده می‌شود',
cleanMonitor: 'پاک‌سازی رکوردهای نظارت',
cleanHelper: 'این عملیات همه رکوردهای نظارت از جمله GPU را پاک می‌کند. ادامه می‌دهید؟',
cleanHelper: 'این عملیات تمام سوابق {0} را پاک می‌کند. ادامه می‌دهید؟',
avgLoad: 'میانگین بار',
loadDetail: 'جزئیات بار',
resourceUsage: 'استفاده از منابع',
@@ -2164,6 +2195,23 @@ const message = {
profileGoroutineDesc: 'وضعیت و پشته فراخوانی هر Goroutine را ذخیره و مسیرهای یکسان را گروه‌بندی می‌کند.',
profileMutexDesc: 'زمان انتظار ناشی از رقابت mutex را برای یافتن نقاط رقابت قفل اندازه‌گیری می‌کند.',
profileBlockDesc: 'زمان انسداد روی Channel، Select و ابزارهای همگام‌سازی را اندازه‌گیری می‌کند.',
exportRange: 'بازه زمانی',
exportMetrics: 'معیارها',
exportMetricsHelper: 'معیارهای CPU، حافظه و بار در یک جدول ادغام می‌شوند.',
exportAllDevices: 'هر دستگاه (جزئیات به تفکیک دستگاه)',
exportAllInterfaces: 'هر رابط (جزئیات به تفکیک رابط)',
exportFormat: 'قالب',
exportCsvHelper: 'هنگام انتخاب چند معیار، فایل‌های CSV جداگانه در یک بایگانی ZIP بسته‌بندی می‌شوند.',
exportJsonHelper: 'به‌صورت یک فایل JSON ساخت‌یافته واحد خروجی بگیرید.',
exportXlsxHelper: 'هر معیار به‌صورت یک کاربرگ جداگانه خروجی گرفته می‌شود.',
exportTruncated: 'از حد ردیف اکسل فراتر رفت؛ این مجموعه‌داده‌ها بریده شدند: {0}',
exportEmptyTables: 'در بازه انتخاب‌شده داده‌ای برای این موارد نیست: {0}',
exportAggregate: 'مجموع ترکیبی (همه دستگاه‌ها)',
exportAllGPUs: 'هر مدل (جزئیات به تفکیک مدل)',
exportOptionsFailed:
'بارگیری فهرست دستگاه برای موارد زیر ناموفق بود: {0}. صفحه را تازه کرده و دوباره تلاش کنید.',
selectMetrics: 'حداقل یک معیار برای خروجی انتخاب کنید',
selectTimeRange: 'بازه زمانی خروجی را انتخاب کنید',
},
terminal: {
showTerminalButton: 'میانبر ترمینال',
@@ -2813,6 +2861,9 @@ const message = {
privateKey: 'کلید خصوصی',
publicKey: 'کلید عمومی',
password: 'رمز عبور',
existingPassPhrase: 'عبارت عبور فعلی کلید خصوصی',
existingPassPhraseHelper:
'اگر کلید خصوصی رمزگذاری شده است، عبارت عبور اصلی را وارد کنید؛ در غیر این صورت خالی بگذارید. این کار عبارت عبور را تغییر نمی‌دهد.',
createMode: 'روش ایجاد',
generate: 'تولید خودکار',
unSyncPass: 'رمز عبور کلید قابل همگام‌سازی نیست',
@@ -7099,9 +7150,34 @@ const message = {
orphan_snapshot: 'اسنپ‌شات بدون مرجع',
partial_file: 'فایل ناتمام',
},
disks: 'مدیریت دیسک‌ها',
resizeDisk: 'افزایش ظرفیت',
diskTarget: 'دستگاه',
diskCapacityAfterResize: 'ظرفیت کل پس از افزایش',
diskResizeHelper:
'این عملیات فقط ظرفیت دیسک مجازی را افزایش می‌دهد. سپس باید پارتیشن‌ها و سیستم‌های فایل را درون ماشین مجازی گسترش دهید.',
diskResizeLimit:
'ماشین مجازی باید خاموش باشد و دیسک qcow2 مستقل و قابل‌نوشتن، بدون اسنپ‌شات یا زنجیره تصاویر پایه باشد. کاهش ظرفیت پشتیبانی نمی‌شود.',
diskResizeReason: {
referenceCheck:
'بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید.',
referenced:
'این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود.',
running: 'پیش از افزایش ظرفیت دیسک، ماشین مجازی را خاموش کنید.',
saved: 'ماشین مجازی را از سر بگیرید، سپس پیش از افزایش ظرفیت به‌طور عادی خاموش کنید.',
snapshots: 'برای جلوگیری از اختلال در بازیابی، افزایش ظرفیت با وجود اسنپ‌شات پشتیبانی نمی‌شود.',
backing: 'دیسک به تصویر دیگری وابسته است؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود.',
shared: 'دیسک اشتراکی است؛ برای جلوگیری از تأثیر بر دیگر کاربران، افزایش ظرفیت پشتیبانی نمی‌شود.',
unsupported: 'قالب، محل یا مجوزهای دسترسی دیسک از افزایش ظرفیت پشتیبانی نمی‌کنند.',
unavailable: 'وضعیت دیسک قابل تأیید نیست. بررسی و تازه‌سازی کنید.',
},
diskSize: 'ظرفیت دیسک',
isoHelper:
'می‌توانید چند ISO متصل کنید. ابتدا تصویر نصب سیستم‌عامل و سپس تصاویر درایور مانند VirtIO را انتخاب کنید. هر تصویر از یک درایو CD-ROM جداگانه استفاده می‌کند.',
windowsDriverHelper:
'ممکن است تصویر Windows شامل درایور شبکه VirtIO (NetKVM) نباشد. یک ISO درایور VirtIO را نیز متصل کرده و درایور را در Windows نصب کنید. اگر تصویر از قبل شامل درایور است، این پیام را نادیده بگیرید.',
windowsDiskDriverHelper:
'دیسک‌های VirtIO ممکن است به بارگذاری درایور دیسک (viostor) نیز نیاز داشته باشند؛ در غیر این صورت، برنامه نصب Windows ممکن است دیسک را شناسایی نکند.',
osType: 'نوع سیستم‌عامل',
osOther: 'سایر',
diskBus: 'گذرگاه دیسک',
@@ -7153,6 +7229,10 @@ const message = {
rx: 'دریافت',
tx: 'ارسال',
console: 'کنسول',
consoleShortcutKeys: 'کلیدهای میانبر',
consoleCustomKeys: 'ترکیب کلید سفارشی',
consoleMainKey: 'انتخاب کلید اصلی',
consoleSendKeys: 'ارسال',
consoleNotReady: 'کنسول VNC در دسترس نیست. ماشین مجازی را شروع کنید و پیکربندی گرافیکی VNC را بررسی کنید.',
consoleDisconnected: 'اتصال کنسول VNC قطع شد.',
consoleConnectFailed: 'اتصال به کنسول VNC ناموفق بود. سرویس پراکسی کنسول را بررسی کنید.',
+80 -1
View File
@@ -1495,6 +1495,34 @@ const message = {
ignoreEos: 'EOS を無視',
extraHeaders: '追加ヘッダー',
},
vllmMonitor: {
cleanHelper:
'この vLLM インスタンスの監視履歴のみを削除しますか?他のインスタンス、ホスト、GPU の監視には影響しません。',
overview: '概要',
latencyAnalysis: 'レイテンシ分析',
requests: 'リクエスト数',
tokenThroughput: 'トークンスループット',
running: '実行中のリクエスト',
waiting: '待機中のリクエスト',
cacheUsage: 'KVキャッシュ使用率',
promptThroughput: '入力トークンスループット',
generationThroughput: '出力トークンスループット',
requestThroughput: '完了リクエスト数/秒',
timeToFirstToken: '平均初回トークン遅延',
timePerOutputToken: '平均トークン間遅延',
requestLatency: '平均リクエスト遅延',
unavailable: '指標を取得できません。インスタンスが稼働し、/metrics にアクセスできるか確認してください。',
loadFailed: '監視データの読み込みに失敗しました。再試行してください。',
helper: '収集の有効化、間隔、保存期間は監視設定で変更できます。',
cache: 'KVキャッシュ',
throughput: 'リクエストスループット',
phaseTime: 'プリフィルとデコードの所要時間',
prefillTime: '平均プリフィル時間',
decodeTime: '平均デコード時間',
requestLatencyChart: 'エンドツーエンドのリクエスト遅延',
timeToFirstTokenChart: '初回トークン遅延',
timePerOutputTokenChart: 'トークン間遅延',
},
gpu: {
memoryActivity: 'メモリ読み書き稼働率',
hotspotTemperature: 'ホットスポット温度',
@@ -2108,6 +2136,9 @@ const message = {
cleanLogscope: 'ログ保持',
},
monitor: {
hostMonitor: 'ホスト監視',
gpuMonitor: 'GPU 監視',
vllmMonitor: 'vLLM 監視',
globalFilter: 'グローバルフィルター',
enableMonitor: '監視ステータス',
storeDays: '保存日数',
@@ -2116,7 +2147,7 @@ const message = {
defaultIO: 'デフォルトディスク',
defaultIOHelper: '監視および概要インターフェースに表示されるデフォルトのディスクオプション',
cleanMonitor: '監視記録をクリア',
cleanHelper: 'この操作により、GPUを含むすべての監視記録がクリアされます。続行しますか?',
cleanHelper: 'この操作は {0} の履歴をすべて削除します。続行しますか?',
avgLoad: 'ロード平均',
loadDetail: '詳細を読み込みます',
resourceUsage: '利用',
@@ -2170,6 +2201,22 @@ const message = {
profileGoroutineDesc: 'すべての Goroutine の状態と呼び出しスタックを保存し、同じ呼び出し経路を集約します。',
profileMutexDesc: 'Mutex 競合による待機時間を測定し、ロック競合のホットスポットを特定します。',
profileBlockDesc: 'Channel、Select、同期プリミティブでのブロック待機時間を測定します。',
exportRange: '期間',
exportMetrics: 'エクスポート指標',
exportMetricsHelper: 'CPU、メモリ、負荷の指標は 1 つのテーブルにまとめられます。',
exportAllDevices: 'デバイスごと(デバイス別明細)',
exportAllInterfaces: 'インターフェースごと(インターフェース別明細)',
exportFormat: 'エクスポート形式',
exportCsvHelper: '複数の指標を選択した場合、指標ごとに CSV ファイルを生成して ZIP にまとめます。',
exportJsonHelper: '構造化された単一の JSON ファイルとしてエクスポートします。',
exportXlsxHelper: '指標ごとに独立したワークシートを生成します。',
exportTruncated: 'Excel の行数上限を超えたため、以下のデータセットは切り詰められました: {0}',
exportEmptyTables: '選択した期間にデータがない指標: {0}',
exportAggregate: '合計(全デバイス合算)',
exportAllGPUs: 'モデルごと(モデル別明細)',
exportOptionsFailed: '次の指標のデバイス一覧を読み込めませんでした。再読み込みして再試行してください: {0}',
selectMetrics: 'エクスポートする指標を 1 つ以上選択してください',
selectTimeRange: 'エクスポートする期間を選択してください',
},
terminal: {
showTerminalButton: 'ターミナルへのショートカット',
@@ -2818,6 +2865,9 @@ const message = {
privateKey: '秘密鍵',
publicKey: '公開鍵',
password: 'パスワード',
existingPassPhrase: '既存の秘密鍵のパスフレーズ',
existingPassPhraseHelper:
'秘密鍵が暗号化されている場合は元のパスフレーズを入力し、それ以外は空欄にしてください。パスフレーズは変更されません。',
createMode: '作成方法',
generate: '自動生成',
unSyncPass: '鍵パスワードは同期できません',
@@ -7132,9 +7182,34 @@ const message = {
orphan_snapshot: '孤立スナップショット',
partial_file: '未完了ファイル',
},
disks: 'ディスク管理',
resizeDisk: '拡張',
diskTarget: 'デバイス',
diskCapacityAfterResize: '拡張後の合計容量',
diskResizeHelper:
'拡張されるのは仮想ディスクの容量のみです。その後、仮想マシン内でパーティションとファイルシステムを拡張してください。',
diskResizeLimit:
'停止中の仮想マシンに接続された、スナップショットやバッキングチェーンのない独立した書き込み可能な qcow2 ディスクのみ拡張できます。縮小はできません。',
diskResizeReason: {
referenceCheck:
'仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。',
referenced:
'この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。',
running: '仮想マシンを停止してからディスクを拡張してください。',
saved: '仮想マシンを再開し、正常にシャットダウンしてから拡張してください。',
snapshots: 'スナップショットの復元に影響するため、スナップショットがある場合は拡張できません。',
backing: '他のイメージに依存するディスクの拡張には未対応です。',
shared: '共有設定のディスクは他の利用者に影響するため拡張できません。',
unsupported: 'ディスクの形式、保存場所、またはアクセス権限が拡張に対応していません。',
unavailable: 'ディスクの状態を確認できません。確認後に更新してください。',
},
diskSize: 'ディスク容量',
isoHelper:
'複数の ISO をマウントできます。OS のインストールイメージを先に選択し、次に VirtIO などのドライバーイメージを選択してください。各イメージは個別の CD-ROM ドライブを使用します。',
windowsDriverHelper:
'Windows イメージに VirtIO ネットワークドライバー(NetKVM)が含まれていない場合があります。VirtIO ドライバー ISO もマウントし、Windows 内でドライバーをインストールしてください。すでに含まれている場合は不要です。',
windowsDiskDriverHelper:
'VirtIO ディスクを使用する場合、ディスクドライバー(viostor)の読み込みも必要になることがあります。読み込まないと、Windows セットアップでディスクが認識されない場合があります。',
osType: 'OS タイプ',
osOther: 'その他',
diskBus: 'ディスクバス',
@@ -7186,6 +7261,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'コンソール',
consoleShortcutKeys: 'ショートカットキー',
consoleCustomKeys: 'カスタムキーの組み合わせ',
consoleMainKey: 'メインキーを選択',
consoleSendKeys: '送信',
consoleNotReady: 'VNC コンソールを使用できません。VM を起動し、VNC グラフィック設定を確認してください。',
consoleDisconnected: 'VNC コンソールの接続が切断されました。',
consoleConnectFailed:
+79 -1
View File
@@ -1481,6 +1481,34 @@ const message = {
ignoreEos: 'EOS 무시',
extraHeaders: '추가 헤더',
},
vllmMonitor: {
cleanHelper:
'현재 vLLM 인스턴스의 모니터링 기록만 삭제하시겠습니까? 다른 인스턴스, 호스트 및 GPU 모니터링에는 영향을 주지 않습니다.',
overview: '개요',
latencyAnalysis: '지연 시간 분석',
requests: '요청 수',
tokenThroughput: '토큰 처리량',
running: '실행 중인 요청',
waiting: '대기 중인 요청',
cacheUsage: 'KV 캐시 사용률',
promptThroughput: '입력 토큰 처리량',
generationThroughput: '출력 토큰 처리량',
requestThroughput: '완료 요청 속도',
timeToFirstToken: '평균 첫 토큰 지연',
timePerOutputToken: '평균 토큰 간 지연',
requestLatency: '평균 요청 지연',
unavailable: '지표를 수집할 수 없습니다. 인스턴스 실행 상태와 /metrics 접근 가능 여부를 확인하세요.',
loadFailed: '모니터링 데이터를 불러오지 못했습니다. 다시 시도하세요.',
helper: '수집 활성화, 간격 및 보존 기간은 모니터링 설정에서 조정합니다.',
cache: 'KV 캐시',
throughput: '요청 처리량',
phaseTime: '프리필 및 디코딩 시간',
prefillTime: '평균 프리필 시간',
decodeTime: '평균 디코딩 시간',
requestLatencyChart: '종단 간 요청 지연',
timeToFirstTokenChart: '첫 토큰 지연',
timePerOutputTokenChart: '토큰 간 지연',
},
gpu: {
memoryActivity: '메모리 읽기/쓰기 활성률',
hotspotTemperature: '핫스폿 온도',
@@ -2082,6 +2110,9 @@ const message = {
cleanLogscope: '로그 보관 기간',
},
monitor: {
hostMonitor: '호스트 모니터링',
gpuMonitor: 'GPU 모니터링',
vllmMonitor: 'vLLM 모니터링',
globalFilter: '전역 필터',
enableMonitor: '모니터링 상태',
storeDays: '보관 일수',
@@ -2090,7 +2121,7 @@ const message = {
defaultIO: '기본 디스크',
defaultIOHelper: '모니터링 및 개요 인터페이스에 표시되는 기본 디스크 옵션',
cleanMonitor: '모니터링 기록 지우기',
cleanHelper: '이 작업은 GPU를 포함한 모든 모니터링 기록을 지웁니다. 계속하시겠습니까?',
cleanHelper: '이 작업은 {0}의 모든 기록을 삭제합니다. 계속하시겠습니까?',
avgLoad: '평균 부하',
loadDetail: '부하 세부사항',
resourceUsage: '자원 사용률',
@@ -2144,6 +2175,22 @@ const message = {
profileGoroutineDesc: '모든 Goroutine의 상태와 호출 스택을 저장하고 동일한 호출 경로를 그룹화합니다.',
profileMutexDesc: 'Mutex 경합으로 인한 대기 시간을 측정하여 잠금 경합 지점을 찾습니다.',
profileBlockDesc: 'Channel, Select 및 동기화 프리미티브의 차단 대기 시간을 측정합니다.',
exportRange: '기간',
exportMetrics: '보낼 지표',
exportMetricsHelper: 'CPU, 메모리, 로드 지표는 하나의 테이블로 병합됩니다.',
exportAllDevices: '장치별 (장치별 상세)',
exportAllInterfaces: '인터페이스별 (인터페이스별 상세)',
exportFormat: '보내기 형식',
exportCsvHelper: '여러 지표를 선택하면 지표별 CSV 파일을 생성해 ZIP으로 묶습니다.',
exportJsonHelper: '하나의 구조화된 JSON 파일로보냅니다.',
exportXlsxHelper: '지표별로 개별 워크시트를 생성합니다.',
exportTruncated: 'Excel 행 제한을 초과하여 다음 데이터셋이 잘렸습니다: {0}',
exportEmptyTables: '선택한 기간에 데이터가 없는 지표: {0}',
exportAggregate: '합계 (모든 장치 합산)',
exportAllGPUs: '모델별 (모델별 상세)',
exportOptionsFailed: '다음 지표의 장치 목록을 불러오지 못했습니다. 새로고침 후 다시 시도하세요: {0}',
selectMetrics: '보낼 지표를 하나 이상 선택하세요',
selectTimeRange: '보낼 기간을 선택하세요',
},
terminal: {
showTerminalButton: '터미널 바로가기',
@@ -2780,6 +2827,9 @@ const message = {
privateKey: '개인 키',
publicKey: '공개 키',
password: '비밀번호',
existingPassPhrase: '기존 개인 키 암호',
existingPassPhraseHelper:
'개인 키가 암호화되어 있으면 원래 암호를 입력하고, 그렇지 않으면 비워 두세요. 이 작업은 암호를 변경하지 않습니다.',
createMode: '생성 방식',
generate: '자동 생성',
unSyncPass: '키 비밀번호 동기화 불가',
@@ -6994,9 +7044,33 @@ const message = {
orphan_snapshot: '고아 스냅샷',
partial_file: '미완료 파일',
},
disks: '디스크 관리',
resizeDisk: '확장',
diskTarget: '장치',
diskCapacityAfterResize: '확장 후 전체 용량',
diskResizeHelper:
'확장은 가상 디스크 용량만 늘립니다. 완료 후 가상 머신 내에서 파티션과 파일 시스템을 확장해야 합니다.',
diskResizeLimit:
'가상 머신이 종료된 상태에서 스냅샷이나 백킹 체인이 없는 독립적인 쓰기 가능한 qcow2 디스크만 확장할 수 있습니다. 축소는 지원하지 않습니다.',
diskResizeReason: {
referenceCheck:
'가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요.',
referenced: '다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다.',
running: '가상 머신을 종료한 후 디스크를 확장하세요.',
saved: '가상 머신을 재개하고 정상 종료한 후 확장하세요.',
snapshots: '스냅샷 복원에 영향을 줄 수 있어 스냅샷이 있으면 확장할 수 없습니다.',
backing: '다른 이미지에 의존하는 디스크는 아직 확장을 지원하지 않습니다.',
shared: '공유로 설정된 디스크는 다른 사용자에게 영향을 줄 수 있어 확장할 수 없습니다.',
unsupported: '디스크 형식, 위치 또는 접근 권한이 확장을 지원하지 않습니다.',
unavailable: '디스크 상태를 확인할 수 없습니다. 확인 후 새로 고치세요.',
},
diskSize: '디스크 용량',
isoHelper:
'여러 ISO를 연결할 수 있습니다. 운영 체제 설치 이미지를 먼저 선택한 다음 VirtIO 등의 드라이버 이미지를 선택하세요. 각 이미지는 별도의 CD-ROM 드라이브를 사용합니다.',
windowsDriverHelper:
'Windows 이미지에 VirtIO 네트워크 드라이버(NetKVM)가 포함되어 있지 않을 수 있습니다. VirtIO 드라이버 ISO도 마운트하고 Windows에서 드라이버를 설치하세요. 이미 포함된 경우에는 무시해도 됩니다.',
windowsDiskDriverHelper:
'VirtIO 디스크를 사용하면 디스크 드라이버(viostor)도 로드해야 할 수 있습니다. 그렇지 않으면 Windows 설치 프로그램이 디스크를 인식하지 못할 수 있습니다.',
osType: '운영 체제 유형',
osOther: '기타',
diskBus: '디스크 버스',
@@ -7048,6 +7122,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: '콘솔',
consoleShortcutKeys: '단축키',
consoleCustomKeys: '사용자 지정 키 조합',
consoleMainKey: '기본 키 선택',
consoleSendKeys: '전송',
consoleNotReady: 'VNC 콘솔을 사용할 수 없습니다. VM을 시작하고 VNC 그래픽 구성을 확인하세요.',
consoleDisconnected: 'VNC 콘솔 연결이 끊어졌습니다.',
consoleConnectFailed: 'VNC 콘솔 연결에 실패했습니다. 콘솔 프록시 서비스를 확인하세요.',
+78 -1
View File
@@ -1474,6 +1474,33 @@ const message = {
ignoreEos: 'ລະເວັ້ນ EOS',
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
vllmMonitor: {
cleanHelper: 'ລຶບສະເພາະປະຫວັດການຕິດຕາມຂອງ vLLM ນີ້ບໍ? ບໍ່ກະທົບຕໍ່ອິນສະແຕນອື່ນ, ໂຮສ ຫຼື GPU.',
overview: 'ພາບລວມ',
latencyAnalysis: 'ການວິເຄາະຄວາມລ່າຊ້າ',
requests: 'ຈຳນວນຄຳຮ້ອງ',
tokenThroughput: 'ອັດຕາປະມວນຜົນໂທເຄັນ',
running: 'ຄຳຮ້ອງຂໍທີ່ກຳລັງເຮັດວຽກ',
waiting: 'ຄຳຮ້ອງຂໍທີ່ກຳລັງລໍຖ້າ',
cacheUsage: 'ການໃຊ້ງານແຄດ KV',
promptThroughput: 'ອັດຕາ Token ຂາເຂົ້າ',
generationThroughput: 'ອັດຕາ Token ຂາອອກ',
requestThroughput: 'ອັດຕາຄຳຮ້ອງຂໍສຳເລັດ',
timeToFirstToken: 'ເວລາລໍຖ້າສະເລ່ຍຂອງ Token ທຳອິດ',
timePerOutputToken: 'ເວລາລໍຖ້າສະເລ່ຍລະຫວ່າງ Token',
requestLatency: 'ເວລາລໍຖ້າສະເລ່ຍຂອງຄຳຮ້ອງຂໍ',
unavailable: 'ບໍ່ສາມາດເກັບຕົວຊີ້ວັດໄດ້. ກວດສອບວ່າອິນສະແຕນເຮັດວຽກ ແລະ ເຂົ້າເຖິງ /metrics ໄດ້.',
loadFailed: 'ໂຫຼດຂໍ້ມູນຕິດຕາມບໍ່ສຳເລັດ. ກະລຸນາລອງອີກຄັ້ງ.',
helper: 'ປັບການເປີດໃຊ້ການເກັບຂໍ້ມູນ, ໄລຍະຫ່າງ ແລະ ຈຳນວນມື້ເກັບຮັກສາໃນການຕັ້ງຄ່າການຕິດຕາມ.',
cache: 'ແຄດ KV',
throughput: 'ຄຳຮ້ອງຂໍ/ວິນາທີ',
phaseTime: 'ເວລາເຕີມລ່ວງໜ້າ ແລະ ຖອດລະຫັດ',
prefillTime: 'ເວລາເຕີມລ່ວງໜ້າສະເລ່ຍ',
decodeTime: 'ເວລາຖອດລະຫັດສະເລ່ຍ',
requestLatencyChart: 'ເວລາລໍຖ້າຄຳຮ້ອງຂໍຕັ້ງແຕ່ຕົ້ນຈົນຈົບ',
timeToFirstTokenChart: 'ເວລາລໍຖ້າ Token ທຳອິດ',
timePerOutputTokenChart: 'ເວລາລໍຖ້າລະຫວ່າງ Token',
},
gpu: {
memoryActivity: 'ກິດຈະກຳອ່ານ/ຂຽນໜ່ວຍຄວາມຈຳ',
hotspotTemperature: 'ອຸນຫະພູມຈຸດຮ້ອນ',
@@ -2062,6 +2089,9 @@ const message = {
cleanLogscope: 'ການເກັບຮັກສາບັນທຶກ',
},
monitor: {
hostMonitor: 'ການຕິດຕາມໂຮສ',
gpuMonitor: 'ການຕິດຕາມ GPU',
vllmMonitor: 'ການຕິດຕາມ vLLM',
globalFilter: 'ຕົວກອງທົ່ວໂລກ',
enableMonitor: 'ສະຖານະການຕິດຕາມ',
storeDays: 'ຈຳນວນມື້ທີ່ເກັບໄວ້',
@@ -2070,7 +2100,7 @@ const message = {
defaultIO: 'Disk ເລີ່ມຕົ້ນ',
defaultIOHelper: 'Disk ທີ່ຈະສະແດງໃນໜ້າ monitor ແລະ ພາບລວມ',
cleanMonitor: 'ລ້າງບັນທຶກການຕິດຕາມ',
cleanHelper: 'ການດຳເນີນການນີ້ຈະລ້າງບັນທຶກການຕິດຕາມທັງໝົດລວມເຖິງ GPU. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
cleanHelper: 'ການດຳເນີນການນີ້ຈະລຶບປະຫວັດທັງໝົດຂອງ {0}. ສືບຕໍ່ບໍ?',
avgLoad: 'ຄ່າ Load ສະເລ່ຍ',
loadDetail: 'ລາຍລະອຽດ Load',
resourceUsage: 'ການນຳໃຊ້',
@@ -2125,6 +2155,22 @@ const message = {
profileGoroutineDesc: 'ບັນທຶກສະຖານະ ແລະ Call Stack ຂອງທຸກ Goroutine ແລະ ຈັດກຸ່ມເສັ້ນທາງການເອີ້ນທີ່ຄືກັນ.',
profileMutexDesc: 'ວັດແທກເວລາລໍຖ້າທີ່ເກີດຈາກ Mutex Contention ເພື່ອຊອກຫາຈຸດທີ່ມີການແຂ່ງຂັນ Lock ສູງ.',
profileBlockDesc: 'ວັດແທກການບລັອກໃນ Channel, Select Statement ແລະ ກົນໄກ Synchronization.',
exportRange: 'ຊ່ວງເວລາ',
exportMetrics: 'ຕົວຊີ້ວັດ',
exportMetricsHelper: 'ຕົວຊີ້ວັດ CPU, ຫນ່ວຍຄວາມຈຳ ແລະ ໂຫຼດ ຈະຖືກຮວມເຂົ້າເປັນຕາຕະລາງດຽວ.',
exportAllDevices: 'ແຕ່ລະອຸປະກອນ (ລາຍລະອຽດຕໍ່ອຸປະກອນ)',
exportAllInterfaces: 'ແຕ່ລະອິນເຕີເຟສ (ລາຍລະອຽດຕໍ່ອິນເຕີເຟສ)',
exportFormat: 'ຮູບແບບ',
exportCsvHelper: 'ເມື່ອເລືອກຫຼາຍຕົວຊີ້ວັດ ຈະສ້າງໄຟລ໌ CSV ແຍກກັນແລ້ວຮັດຮວມເປັນ ZIP.',
exportJsonHelper: 'ສົ່ງອອກເປັນໄຟລ໌ JSON ໂຄງສ້າງດຽວ.',
exportXlsxHelper: 'ແຕ່ລະຕົວຊີ້ວັດຈະຖືກສົ່ງອອກເປັນເວີກຊີດແຍກກັນ.',
exportTruncated: 'ເກີນຂີດຈຳກັດແຖວຂອງ Excel, ຊຸດຂໍ້ມູນຕໍ່ໄປນີ້ຖືກຕັດ: {0}',
exportEmptyTables: 'ບໍ່ມີຂໍ້ມູນໃນຊ່ວງເວລາທີ່ເລືອກສຳລັບ: {0}',
exportAggregate: 'ລວມທັງໝົດ (ທຸກອຸປະກອນ)',
exportAllGPUs: 'ແຕ່ລະໂມເດລ (ລາຍລະອຽດຕໍ່ໂມເດລ)',
exportOptionsFailed: 'ໂຫຼດລາຍຊື່ອຸປະກອນສຳລັບລາຍການຕໍ່ໄປນີ້ບໍ່ສຳເລັດ: {0}. ໂຫຼດໜ້າໃໝ່ແລ້ວລອງອີກຄັ້ງ.',
selectMetrics: 'ກະລຸນາເລືອກຢ່າງໜ້ອຍໜຶ່ງຕົວຊີ້ວັດທີ່ຈະສົ່ງອອກ',
selectTimeRange: 'ກະລຸນາເລືອກຊ່ວງເວລາທີ່ຈະສົ່ງອອກ',
},
terminal: {
showTerminalButton: 'ທາງລັດ Terminal',
@@ -2761,6 +2807,9 @@ const message = {
privateKey: 'ຄີສ່ວນຕົວ',
publicKey: 'ຄີສາທາລະນະ',
password: 'ລະຫັດຜ່ານ',
existingPassPhrase: 'ລະຫັດຜ່ານເດີມຂອງຄີສ່ວນຕົວ',
existingPassPhraseHelper:
'ຖ້າຄີສ່ວນຕົວຖືກເຂົ້າລະຫັດ ໃຫ້ປ້ອນລະຫັດຜ່ານເດີມ; ຖ້າບໍ່ ໃຫ້ປະໄວ້ຫວ່າງ. ການດຳເນີນການນີ້ບໍ່ປ່ຽນລະຫັດຜ່ານ.',
createMode: 'ວິທີການສ້າງ',
generate: 'ສ້າງອັດຕະໂນມັດ',
unSyncPass: 'ບໍ່ສາມາດຊິງຄ໌ລະຫັດຜ່ານຄີໄດ້',
@@ -6904,9 +6953,33 @@ const message = {
orphan_snapshot: 'ສະແນັບຊັອດທີ່ບໍ່ຖືກອ້າງອີງ',
partial_file: 'ໄຟລ໌ບໍ່ສົມບູນ',
},
disks: 'ຈັດການດິສກ໌',
resizeDisk: 'ຂະຫຍາຍ',
diskTarget: 'ອຸປະກອນ',
diskCapacityAfterResize: 'ຄວາມຈຸລວມຫຼັງຂະຫຍາຍ',
diskResizeHelper:
'ການຂະຫຍາຍເພີ່ມພຽງຄວາມຈຸດິສກ໌ເສມືອນ. ຫຼັງຈາກນັ້ນ ຕ້ອງຂະຫຍາຍພາທິຊັນ ແລະ ລະບົບໄຟລ໌ພາຍໃນເຄື່ອງເສມືອນ.',
diskResizeLimit:
'ຕ້ອງປິດເຄື່ອງເສມືອນ ແລະ ໃຊ້ດິສກ໌ qcow2 ອິດສະຫຼະທີ່ຂຽນໄດ້ ໂດຍບໍ່ມີສະແນັບຊັອດ ຫຼື ຕ່ອງໂສ້ອິມເມຈພື້ນຖານ. ບໍ່ຮອງຮັບການຫຼຸດຂະໜາດ.',
diskResizeReason: {
referenceCheck:
'ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ.',
referenced: 'ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ.',
running: 'ກະລຸນາປິດເຄື່ອງເສມືອນກ່ອນຂະຫຍາຍດິສກ໌.',
saved: 'ກະລຸນາເຮັດວຽກເຄື່ອງເສມືອນຕໍ່ ແລ້ວປິດຕາມປົກກະຕິກ່ອນຂະຫຍາຍ.',
snapshots: 'ມີສະແນັບຊັອດ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ ເພື່ອບໍ່ໃຫ້ກະທົບການກູ້ຄືນ.',
backing: 'ດິສກ໌ຂຶ້ນກັບອິມເມຈອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ.',
shared: 'ດິສກ໌ຖືກຕັ້ງໃຫ້ໃຊ້ຮ່ວມກັນ; ບໍ່ຮອງຮັບການຂະຫຍາຍ ເພື່ອບໍ່ໃຫ້ກະທົບຜູ້ໃຊ້ອື່ນ.',
unsupported: 'ຮູບແບບ, ຕຳແໜ່ງ ຫຼື ສິດເຂົ້າເຖິງດິສກ໌ບໍ່ຮອງຮັບການຂະຫຍາຍ.',
unavailable: 'ບໍ່ສາມາດຢືນຢັນສະຖານະດິສກ໌ໄດ້. ກະລຸນາກວດສອບ ແລະ ໂຫຼດໃໝ່.',
},
diskSize: 'ຂະໜາດດິສກ໌',
isoHelper:
'ສາມາດເຊື່ອມຕໍ່ ISO ຫຼາຍໄຟລ໌ໄດ້. ເລືອກໄຟລ໌ຕິດຕັ້ງລະບົບປະຕິບັດການກ່ອນ ແລ້ວຈຶ່ງເລືອກໄຟລ໌ໄດຣເວີ ເຊັ່ນ VirtIO. ແຕ່ລະໄຟລ໌ໃຊ້ໄດຣ CD-ROM ແຍກຕ່າງຫາກ.',
windowsDriverHelper:
'ອິມເມຈ Windows ອາດບໍ່ມີໄດຣເວີເຄືອຂ່າຍ VirtIO (NetKVM). ແນະນຳໃຫ້ແນບ ISO ໄດຣເວີ VirtIO ແລະ ຕິດຕັ້ງໄດຣເວີໃນ Windows. ຖ້າອິມເມຈມີໄດຣເວີແລ້ວ ສາມາດຂ້າມໄດ້.',
windowsDiskDriverHelper:
'ເມື່ອໃຊ້ດິສ VirtIO ອາດຕ້ອງໂຫຼດໄດຣເວີດິສ (viostor) ເພີ່ມເຕີມ; ຖ້າບໍ່ໂຫຼດ ຕົວຕິດຕັ້ງ Windows ອາດບໍ່ພົບດິສ.',
osType: 'ປະເພດລະບົບປະຕິບັດການ',
osOther: 'ອື່ນໆ',
diskBus: 'ບັສດິສກ໌',
@@ -6957,6 +7030,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'ຄອນໂຊນ',
consoleShortcutKeys: 'ປຸ່ມລັດ',
consoleCustomKeys: 'ປຸ່ມປະສົມແບບກຳນົດເອງ',
consoleMainKey: 'ເລືອກປຸ່ມຫຼັກ',
consoleSendKeys: 'ສົ່ງ',
consoleNotReady: 'ຄອນໂຊນ VNC ໃຊ້ງານບໍ່ໄດ້. ກະລຸນາເລີ່ມ VM ແລະ ກວດສອບການຕັ້ງຄ່າກຣາຟິກ VNC.',
consoleDisconnected: 'ຄອນໂຊນ VNC ຖືກຕັດການເຊື່ອມຕໍ່ແລ້ວ.',
consoleConnectFailed: 'ເຊື່ອມຕໍ່ຄອນໂຊນ VNC ລົ້ມເຫຼວ. ກວດສອບບໍລິການພຣັອກຊີຄອນໂຊນ.',
+79 -1
View File
@@ -1521,6 +1521,34 @@ const message = {
ignoreEos: 'Abaikan EOS',
extraHeaders: 'Pengepala Tambahan',
},
vllmMonitor: {
cleanHelper:
'Padamkan sejarah pemantauan untuk instans vLLM ini sahaja? Instans lain serta pemantauan hos dan GPU tidak terjejas.',
overview: 'Gambaran keseluruhan',
latencyAnalysis: 'Analisis kependaman',
requests: 'Permintaan',
tokenThroughput: 'Daya pemprosesan token',
running: 'Permintaan berjalan',
waiting: 'Permintaan menunggu',
cacheUsage: 'Penggunaan cache KV',
promptThroughput: 'Daya pemprosesan token input',
generationThroughput: 'Daya pemprosesan token output',
requestThroughput: 'Kadar permintaan selesai',
timeToFirstToken: 'Purata kependaman token pertama',
timePerOutputToken: 'Purata kependaman antara token',
requestLatency: 'Purata kependaman permintaan',
unavailable: 'Metrik tidak tersedia. Pastikan tika sedang berjalan dan /metrics boleh dicapai.',
loadFailed: 'Gagal memuatkan data pemantauan. Sila cuba lagi.',
helper: 'Urus pengumpulan, sela dan tempoh penyimpanan dalam tetapan pemantauan.',
cache: 'Cache KV',
throughput: 'Permintaan/s',
phaseTime: 'Masa praisi dan penyahkodan',
prefillTime: 'Purata masa praisi',
decodeTime: 'Purata masa penyahkodan',
requestLatencyChart: 'Kependaman permintaan hujung ke hujung',
timeToFirstTokenChart: 'Masa ke token pertama',
timePerOutputTokenChart: 'Kependaman antara token',
},
gpu: {
memoryActivity: 'Aktiviti baca/tulis memori',
hotspotTemperature: 'Suhu titik panas',
@@ -2145,6 +2173,9 @@ const message = {
cleanLogscope: 'Tempoh simpan log',
},
monitor: {
hostMonitor: 'Pemantauan Hos',
gpuMonitor: 'Pemantauan GPU',
vllmMonitor: 'Pemantauan vLLM',
globalFilter: 'Penapis Global',
enableMonitor: 'Status Pemantauan',
storeDays: 'Hari Penyimpanan',
@@ -2154,7 +2185,7 @@ const message = {
defaultIO: 'Cakera Lalai',
defaultIOHelper: 'Pilihan cakera lalai yang dipaparkan dalam antara muka pemantauan dan gambaran keseluruhan',
cleanMonitor: 'Kosongkan Rekod Pemantauan',
cleanHelper: 'Operasi ini akan membersihkan semua rekod pemantauan termasuk GPU. Teruskan?',
cleanHelper: 'Operasi ini akan memadamkan semua sejarah untuk {0}. Teruskan?',
avgLoad: 'Purata beban',
loadDetail: 'Butiran beban',
resourceUsage: 'Penggunaan sumber',
@@ -2212,6 +2243,22 @@ const message = {
'Menyimpan keadaan dan tindanan panggilan setiap Goroutine serta mengumpulkan laluan yang sama.',
profileMutexDesc: 'Mengukur masa menunggu akibat persaingan mutex untuk mencari titik persaingan kunci.',
profileBlockDesc: 'Mengukur sekatan pada Channel, Select dan primitif penyegerakan.',
exportRange: 'Julat masa',
exportMetrics: 'Metrik',
exportMetricsHelper: 'Metrik CPU, memori dan beban digabungkan dalam satu jadual.',
exportAllDevices: 'Setiap peranti (perincian per peranti)',
exportAllInterfaces: 'Setiap antara muka (perincian per antara muka)',
exportFormat: 'Format',
exportCsvHelper: 'Apabila berbilang metrik dipilih, fail CSV berasingan dibungkus dalam ZIP.',
exportJsonHelper: 'Eksport sebagai satu fail JSON berstruktur.',
exportXlsxHelper: 'Setiap metrik dieksport sebagai lembaran kerja berasingan.',
exportTruncated: 'Had baris Excel dicapai; set data berikut telah dipotong: {0}',
exportEmptyTables: 'Tiada data dalam julat dipilih untuk: {0}',
exportAggregate: 'Jumlah gabungan (semua peranti)',
exportAllGPUs: 'Setiap model (perincian per model)',
exportOptionsFailed: 'Gagal memuat senarai peranti untuk: {0}. Muat semula halaman dan cuba lagi.',
selectMetrics: 'Pilih sekurang-kurangnya satu metrik untuk dieksport',
selectTimeRange: 'Pilih julat masa untuk dieksport',
},
terminal: {
showTerminalButton: 'Pintasan Terminal',
@@ -2883,6 +2930,9 @@ const message = {
privateKey: 'Kunci Persendirian',
publicKey: 'Kunci Awam',
password: 'Kata Laluan',
existingPassPhrase: 'Frasa laluan kunci peribadi sedia ada',
existingPassPhraseHelper:
'Masukkan frasa laluan asal jika kunci peribadi disulitkan; jika tidak, biarkan kosong. Ini tidak mengubah frasa laluan.',
createMode: 'Kaedah Penciptaan',
generate: 'Jana Automatik',
unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan',
@@ -7257,9 +7307,33 @@ const message = {
orphan_snapshot: 'Syot kilat yatim',
partial_file: 'Fail tidak lengkap',
},
disks: 'Cakera',
resizeDisk: 'Besarkan',
diskTarget: 'Peranti',
diskCapacityAfterResize: 'Jumlah kapasiti selepas pembesaran',
diskResizeHelper:
'Pembesaran hanya menambah kapasiti cakera maya. Selepas itu, besarkan partition dan sistem fail dalam mesin maya.',
diskResizeLimit:
'Mesin maya mesti dimatikan dan cakera qcow2 mestilah bebas serta boleh ditulis, tanpa syot kilat atau rantaian imej asas. Pengecilan tidak disokong.',
diskResizeReason: {
referenceCheck:
'Rujukan cakera maya tidak dapat disahkan. Pembesaran tidak tersedia; semak cakera mesin maya lain.',
referenced: 'Cakera maya ini digunakan oleh mesin maya atau peranti lain; pembesaran belum disokong.',
running: 'Matikan mesin maya sebelum membesarkan cakera.',
saved: 'Sambung semula mesin maya, kemudian matikannya secara biasa sebelum pembesaran.',
snapshots: 'Pembesaran tidak disokong jika terdapat syot kilat supaya pemulihan tidak terjejas.',
backing: 'Cakera bergantung pada imej lain; pembesaran belum disokong.',
shared: 'Cakera ditandakan sebagai dikongsi; pembesaran tidak disokong supaya pengguna lain tidak terjejas.',
unsupported: 'Format, lokasi atau kebenaran akses cakera tidak menyokong pembesaran.',
unavailable: 'Status cakera tidak dapat disahkan. Semak dan muat semula.',
},
diskSize: 'Saiz Cakera',
isoHelper:
'Pasang berbilang ISO. Pilih pemasang sistem pengendalian dahulu, kemudian imej pemacu seperti VirtIO. Setiap imej menggunakan pemacu CD-ROM yang berasingan.',
windowsDriverHelper:
'Imej Windows mungkin tidak menyertakan pemacu rangkaian VirtIO (NetKVM). Lekapkan ISO pemacu VirtIO dan pasang pemacu dalam Windows. Abaikan jika imej sudah menyertakan pemacu tersebut.',
windowsDiskDriverHelper:
'Cakera VirtIO juga mungkin memerlukan pemacu cakera (viostor) dimuatkan; jika tidak, pemasang Windows mungkin tidak dapat mengesan cakera.',
osType: 'Jenis sistem pengendalian',
osOther: 'Lain-lain',
diskBus: 'Bas cakera',
@@ -7311,6 +7385,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Konsol',
consoleShortcutKeys: 'Kekunci pintasan',
consoleCustomKeys: 'Gabungan kekunci tersuai',
consoleMainKey: 'Pilih kekunci utama',
consoleSendKeys: 'Hantar',
consoleNotReady: 'Konsol VNC tidak tersedia. Mulakan VM dan semak konfigurasi grafik VNC.',
consoleDisconnected: 'Konsol VNC telah terputus.',
consoleConnectFailed: 'Gagal menyambung ke konsol VNC. Semak perkhidmatan proksi konsol.',
+82 -1
View File
@@ -1517,6 +1517,35 @@ const message = {
ignoreEos: 'Ignorar EOS',
extraHeaders: 'Cabeçalhos extras',
},
vllmMonitor: {
cleanHelper:
'Limpar apenas o histórico de monitoramento desta instância do vLLM? Outras instâncias e o monitoramento do host e da GPU não serão afetados.',
overview: 'Visão geral',
latencyAnalysis: 'Análise de latência',
requests: 'Requisições',
tokenThroughput: 'Vazão de tokens',
running: 'Solicitações em execução',
waiting: 'Solicitações em espera',
cacheUsage: 'Uso do cache KV',
promptThroughput: 'Taxa de tokens de entrada',
generationThroughput: 'Taxa de tokens de saída',
requestThroughput: 'Solicitações concluídas',
timeToFirstToken: 'Latência média do primeiro token',
timePerOutputToken: 'Latência média entre tokens',
requestLatency: 'Latência média da solicitação',
unavailable:
'Métricas indisponíveis. Verifique se a instância está em execução e se /metrics está acessível.',
loadFailed: 'Falha ao carregar os dados. Tente novamente.',
helper: 'Ajuste a coleta, o intervalo e a retenção nas configurações de monitoramento.',
cache: 'Cache KV',
throughput: 'Solicitações/s',
phaseTime: 'Tempo de preenchimento e decodificação',
prefillTime: 'Tempo médio de preenchimento',
decodeTime: 'Tempo médio de decodificação',
requestLatencyChart: 'Latência da solicitação de ponta a ponta',
timeToFirstTokenChart: 'Tempo até o primeiro token',
timePerOutputTokenChart: 'Latência entre tokens',
},
gpu: {
memoryActivity: 'Atividade de leitura/escrita da memória',
hotspotTemperature: 'Temperatura do ponto quente',
@@ -2150,6 +2179,9 @@ const message = {
cleanLogscope: 'Retenção de logs',
},
monitor: {
hostMonitor: 'Monitoramento do host',
gpuMonitor: 'Monitoramento da GPU',
vllmMonitor: 'Monitoramento do vLLM',
globalFilter: 'Filtro Global',
enableMonitor: 'Status de Monitoramento',
storeDays: 'Dias de Retenção',
@@ -2158,7 +2190,7 @@ const message = {
defaultIO: 'Disco Padrão',
defaultIOHelper: 'Opção de disco padrão exibida nas interfaces de monitoramento e visão geral',
cleanMonitor: 'Limpar Registros de Monitoramento',
cleanHelper: 'Esta operação limpará todos os registros de monitoramento, incluindo GPU. Continuar?',
cleanHelper: 'Esta operação limpará todo o histórico de {0}. Continuar?',
avgLoad: 'Média de carga',
loadDetail: 'Detalhes da carga',
resourceUsage: 'Utilização',
@@ -2216,6 +2248,23 @@ const message = {
profileGoroutineDesc: 'Salva o estado e a pilha de cada goroutine e agrupa caminhos de chamada idênticos.',
profileMutexDesc: 'Mede o tempo de espera causado por contenção de mutex para localizar pontos de disputa.',
profileBlockDesc: 'Mede bloqueios em canais, select e primitivas de sincronização.',
exportRange: 'Período',
exportMetrics: 'Métricas',
exportMetricsHelper: 'As métricas de CPU, memória e carga são mescladas em uma única tabela.',
exportAllDevices: 'Cada dispositivo (detalhe por dispositivo)',
exportAllInterfaces: 'Cada interface (detalhe por interface)',
exportFormat: 'Formato',
exportCsvHelper: 'Ao selecionar várias métricas, são gerados arquivos CSV separados compactados em ZIP.',
exportJsonHelper: 'Exportar como um único arquivo JSON estruturado.',
exportXlsxHelper: 'Cada métrica é exportada em uma planilha separada.',
exportTruncated: 'Limite de linhas do Excel atingido; estes conjuntos foram truncados: {0}',
exportEmptyTables: 'Sem dados no período selecionado para: {0}',
exportAggregate: 'Total combinado (todos os dispositivos)',
exportAllGPUs: 'Cada modelo (detalhe por modelo)',
exportOptionsFailed:
'Falha ao carregar a lista de dispositivos para: {0}. Atualize a página e tente novamente.',
selectMetrics: 'Selecione pelo menos uma métrica para exportar',
selectTimeRange: 'Selecione o período para exportar',
},
terminal: {
showTerminalButton: 'Atalho para o terminal',
@@ -2880,6 +2929,9 @@ const message = {
privateKey: 'Chave Privada',
publicKey: 'Chave Pública',
password: 'Senha',
existingPassPhrase: 'Senha atual da chave privada',
existingPassPhraseHelper:
'Informe a senha original se a chave privada estiver criptografada; caso contrário, deixe em branco. Isso não altera a senha.',
createMode: 'Método de Criação',
generate: 'Gerar Automaticamente',
unSyncPass: 'Senha da chave não pode ser sincronizada',
@@ -7296,9 +7348,34 @@ const message = {
orphan_snapshot: 'Snapshot órfão',
partial_file: 'Arquivo incompleto',
},
disks: 'Discos',
resizeDisk: 'Expandir',
diskTarget: 'Dispositivo',
diskCapacityAfterResize: 'Capacidade total após a expansão',
diskResizeHelper:
'A expansão aumenta apenas a capacidade do disco virtual. Depois, expanda as partições e os sistemas de arquivos dentro da máquina virtual.',
diskResizeLimit:
'A máquina virtual deve estar desligada e o disco qcow2 deve ser independente e gravável, sem snapshots ou cadeia de imagens base. A redução não é suportada.',
diskResizeReason: {
referenceCheck:
'Não foi possível verificar as referências ao disco virtual. A expansão está indisponível; verifique os discos das outras máquinas virtuais.',
referenced:
'Este disco virtual é usado por outra máquina virtual ou dispositivo e ainda não pode ser expandido.',
running: 'Desligue a máquina virtual antes de expandir o disco.',
saved: 'Retome a máquina virtual e desligue-a normalmente antes de expandir.',
snapshots: 'A expansão não está disponível com snapshots para não afetar a restauração.',
backing: 'O disco depende de outra imagem; a expansão ainda não é suportada.',
shared: 'O disco está marcado como compartilhado; a expansão não está disponível para não afetar outros usuários.',
unsupported: 'O formato, local ou as permissões do disco não permitem expansão.',
unavailable: 'Não foi possível verificar o estado do disco. Verifique e atualize.',
},
diskSize: 'Tamanho do Disco',
isoHelper:
'Monte vários ISOs. Selecione primeiro o instalador do sistema operacional e depois as imagens de drivers, como VirtIO. Cada imagem usa uma unidade de CD-ROM separada.',
windowsDriverHelper:
'A imagem do Windows pode não incluir o driver de rede VirtIO (NetKVM). Monte uma ISO de drivers VirtIO e instale o driver no Windows. Ignore este aviso se a imagem já incluir o driver.',
windowsDiskDriverHelper:
'Os discos VirtIO também podem exigir o carregamento do driver de disco (viostor); caso contrário, o instalador do Windows pode não detectar o disco.',
osType: 'Tipo de sistema operacional',
osOther: 'Outro',
diskBus: 'Barramento de disco',
@@ -7350,6 +7427,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Console',
consoleShortcutKeys: 'Atalhos de teclado',
consoleCustomKeys: 'Combinação personalizada',
consoleMainKey: 'Selecionar tecla',
consoleSendKeys: 'Enviar',
consoleNotReady: 'O console VNC está indisponível. Inicie a VM e verifique sua configuração gráfica VNC.',
consoleDisconnected: 'O console VNC foi desconectado.',
consoleConnectFailed: 'Falha ao conectar ao console VNC. Verifique o serviço de proxy do console.',
+80 -1
View File
@@ -1510,6 +1510,34 @@ const message = {
ignoreEos: 'Игнорировать EOS',
extraHeaders: 'Дополнительные заголовки',
},
vllmMonitor: {
cleanHelper:
'Удалить историю мониторинга только этого экземпляра vLLM? Другие экземпляры, мониторинг хоста и GPU не будут затронуты.',
overview: 'Обзор',
latencyAnalysis: 'Анализ задержек',
requests: 'Количество запросов',
tokenThroughput: 'Скорость обработки токенов',
running: 'Выполняемые запросы',
waiting: 'Ожидающие запросы',
cacheUsage: 'Использование KV-кэша',
promptThroughput: 'Скорость входных токенов',
generationThroughput: 'Скорость выходных токенов',
requestThroughput: 'Завершённые запросы',
timeToFirstToken: 'Среднее время до первого токена',
timePerOutputToken: 'Средняя задержка между токенами',
requestLatency: 'Средняя задержка запроса',
unavailable: 'Метрики недоступны. Проверьте, что экземпляр запущен и /metrics доступен.',
loadFailed: 'Не удалось загрузить данные мониторинга. Повторите попытку.',
helper: 'Сбор, интервал и срок хранения настраиваются в параметрах мониторинга.',
cache: 'KV-кэш',
throughput: 'Запросов/с',
phaseTime: 'Время предзаполнения и декодирования',
prefillTime: 'Среднее время предзаполнения',
decodeTime: 'Среднее время декодирования',
requestLatencyChart: 'Сквозная задержка запроса',
timeToFirstTokenChart: 'Время до первого токена',
timePerOutputTokenChart: 'Задержка между токенами',
},
gpu: {
memoryActivity: 'Активность чтения/записи памяти',
hotspotTemperature: 'Температура горячей точки',
@@ -2135,6 +2163,9 @@ const message = {
cleanLogscope: 'Хранение логов',
},
monitor: {
hostMonitor: 'Мониторинг хоста',
gpuMonitor: 'Мониторинг GPU',
vllmMonitor: 'Мониторинг vLLM',
globalFilter: 'Глобальный Фильтр',
enableMonitor: 'Статус Мониторинга',
storeDays: 'Дни Хранения',
@@ -2143,7 +2174,7 @@ const message = {
defaultIO: 'Диск по Умолчанию',
defaultIOHelper: 'Опция диска по умолчанию, отображаемая в интерфейсах мониторинга и обзора',
cleanMonitor: 'Очистить Записи Мониторинга',
cleanHelper: 'Эта операция очистит все записи мониторинга, включая GPU. Продолжить?',
cleanHelper: 'Эта операция удалит всю историю для {0}. Продолжить?',
avgLoad: 'Средняя нагрузка',
loadDetail: 'Детали нагрузки',
resourceUsage: 'Использование',
@@ -2198,6 +2229,22 @@ const message = {
profileGoroutineDesc: 'Сохраняет состояние и стек каждой goroutine и группирует одинаковые пути вызовов.',
profileMutexDesc: 'Измеряет ожидание из-за конкуренции mutex для поиска горячих блокировок.',
profileBlockDesc: 'Измеряет блокировки на каналах, select и примитивах синхронизации.',
exportRange: 'Диапазон времени',
exportMetrics: 'Метрики',
exportMetricsHelper: 'Метрики CPU, памяти и нагрузки объединяются в одну таблицу.',
exportAllDevices: 'Каждое устройство (по отдельности)',
exportAllInterfaces: 'Каждый интерфейс (по отдельности)',
exportFormat: 'Формат',
exportCsvHelper: 'При выборе нескольких метрик CSV-файлы упаковываются в ZIP-архив.',
exportJsonHelper: 'Экспорт в один структурированный JSON-файл.',
exportXlsxHelper: 'Каждая метрика экспортируется на отдельный лист.',
exportTruncated: 'Превышен лимит строк Excel, эти наборы данных обрезаны: {0}',
exportEmptyTables: 'Нет данных за выбранный период: {0}',
exportAggregate: 'Сводный итог (все устройства)',
exportAllGPUs: 'Каждая модель (по отдельности)',
exportOptionsFailed: 'Не удалось загрузить список устройств для: {0}. Обновите страницу и повторите.',
selectMetrics: 'Выберите хотя бы одну метрику для экспорта',
selectTimeRange: 'Выберите диапазон времени для экспорта',
},
terminal: {
showTerminalButton: 'Быстрый доступ к терминалу',
@@ -2856,6 +2903,9 @@ const message = {
privateKey: 'Приватный ключ',
publicKey: 'Публичный ключ',
password: 'Пароль',
existingPassPhrase: 'Текущая парольная фраза закрытого ключа',
existingPassPhraseHelper:
'Если закрытый ключ зашифрован, введите исходную парольную фразу; иначе оставьте поле пустым. Это не изменяет парольную фразу.',
createMode: 'Способ создания',
generate: 'Автогенерация',
unSyncPass: 'Пароль ключа не может быть синхронизирован',
@@ -7264,9 +7314,34 @@ const message = {
orphan_snapshot: 'Неиспользуемый снимок',
partial_file: 'Незавершенный файл',
},
disks: 'Диски',
resizeDisk: 'Расширить',
diskTarget: 'Устройство',
diskCapacityAfterResize: 'Общая ёмкость после расширения',
diskResizeHelper:
'Расширение увеличивает только ёмкость виртуального диска. После этого расширьте разделы и файловые системы внутри виртуальной машины.',
diskResizeLimit:
'Виртуальная машина должна быть выключена, а диск qcow2 — независимым и доступным для записи, без снимков и цепочки базовых образов. Уменьшение не поддерживается.',
diskResizeReason: {
referenceCheck:
'Не удалось проверить ссылки на виртуальный диск. Расширение недоступно; проверьте диски других виртуальных машин.',
referenced:
'Этот виртуальный диск используется другой виртуальной машиной или устройством; расширение пока недоступно.',
running: 'Выключите виртуальную машину перед расширением диска.',
saved: 'Возобновите работу виртуальной машины и штатно выключите её перед расширением.',
snapshots: 'Расширение недоступно при наличии снимков, чтобы не нарушить их восстановление.',
backing: 'Диск зависит от другого образа; расширение пока не поддерживается.',
shared: 'Диск помечен как общий; расширение недоступно, чтобы не затронуть других пользователей.',
unsupported: 'Формат, расположение или права доступа к диску не позволяют расширить его.',
unavailable: 'Не удалось проверить состояние диска. Проверьте его и обновите данные.',
},
diskSize: 'Размер диска',
isoHelper:
'Можно подключить несколько ISO. Сначала выберите установочный образ ОС, затем образы драйверов, например VirtIO. Каждый образ подключается к отдельному приводу CD-ROM.',
windowsDriverHelper:
'Образ Windows может не содержать сетевой драйвер VirtIO (NetKVM). Подключите ISO с драйверами VirtIO и установите драйвер в Windows. Если драйвер уже включён в образ, пропустите этот шаг.',
windowsDiskDriverHelper:
'Для дисков VirtIO также может потребоваться загрузить драйвер диска (viostor), иначе установщик Windows может не обнаружить диск.',
osType: 'Тип операционной системы',
osOther: 'Другая',
diskBus: 'Шина диска',
@@ -7318,6 +7393,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Консоль',
consoleShortcutKeys: 'Сочетания клавиш',
consoleCustomKeys: 'Своя комбинация клавиш',
consoleMainKey: 'Выберите клавишу',
consoleSendKeys: 'Отправить',
consoleNotReady: 'Консоль VNC недоступна. Запустите ВМ и проверьте конфигурацию графики VNC.',
consoleDisconnected: 'Консоль VNC отключена.',
consoleConnectFailed: 'Не удалось подключиться к консоли VNC. Проверьте службу прокси консоли.',
+81 -1
View File
@@ -1517,6 +1517,35 @@ const message = {
ignoreEos: "EOS'u yok say",
extraHeaders: 'Ek Başlıklar',
},
vllmMonitor: {
cleanHelper:
'Yalnızca bu vLLM örneğinin izleme geçmişi temizlensin mi? Diğer örnekler, ana makine ve GPU izlemesi etkilenmez.',
overview: 'Genel bakış',
latencyAnalysis: 'Gecikme analizi',
requests: 'İstek sayısı',
tokenThroughput: 'Token işlem hızı',
running: 'Çalışan istekler',
waiting: 'Bekleyen istekler',
cacheUsage: 'KV önbellek kullanımı',
promptThroughput: 'Girdi token hızı',
generationThroughput: 'Çıktı token hızı',
requestThroughput: 'Tamamlanan istek hızı',
timeToFirstToken: 'Ortalama ilk token gecikmesi',
timePerOutputToken: 'Ortalama token arası gecikme',
requestLatency: 'Ortalama istek gecikmesi',
unavailable:
'Metrikler kullanılamıyor. Örneğin çalıştığını ve /metrics yolunun erişilebilir olduğunu kontrol edin.',
loadFailed: 'İzleme verileri yüklenemedi. Tekrar deneyin.',
helper: 'Toplama, aralık ve saklama süresini izleme ayarlarından yönetin.',
cache: 'KV önbelleği',
throughput: 'İstek hızı',
phaseTime: 'Ön doldurma ve çözümleme süresi',
prefillTime: 'Ortalama ön doldurma süresi',
decodeTime: 'Ortalama çözümleme süresi',
requestLatencyChart: 'Uçtan uca istek gecikmesi',
timeToFirstTokenChart: 'İlk token süresi',
timePerOutputTokenChart: 'Token arası gecikme',
},
gpu: {
memoryActivity: 'Bellek okuma/yazma etkinliği',
hotspotTemperature: 'Sıcak nokta sıcaklığı',
@@ -2143,6 +2172,9 @@ const message = {
cleanLogscope: 'Günlük saklama',
},
monitor: {
hostMonitor: 'Ana Makine İzleme',
gpuMonitor: 'GPU İzleme',
vllmMonitor: 'vLLM İzleme',
globalFilter: 'Genel Filtre',
enableMonitor: 'İzleme Durumu',
storeDays: 'Saklama Günleri',
@@ -2151,7 +2183,7 @@ const message = {
defaultIO: 'Varsayılan Disk',
defaultIOHelper: 'İzleme ve genel bakış arayüzlerinde görüntülenen varsayılan disk seçeneği',
cleanMonitor: 'İzleme Kayıtlarını Temizle',
cleanHelper: 'Bu işlem GPU dahil tüm izleme kayıtlarını temizleyecektir. Devam etmek istiyor musunuz?',
cleanHelper: 'Bu işlem {0} için tüm geçmişi silecektir. Devam edilsin mi?',
avgLoad: 'Ortalama yük',
loadDetail: 'Yük detayı',
resourceUsage: 'Kullanım',
@@ -2204,6 +2236,22 @@ const message = {
profileGoroutineDesc: 'Her goroutine durumunu ve çağrı yığınını kaydeder, aynı çağrı yollarını gruplar.',
profileMutexDesc: 'Kilit çekişmesi noktalarını bulmak için mutex bekleme süresini ölçer.',
profileBlockDesc: 'Channel, select ve eşzamanlama araçlarındaki engelleme süresini ölçer.',
exportRange: 'Zaman aralığı',
exportMetrics: 'Metrikler',
exportMetricsHelper: 'CPU, bellek ve yük metrikleri tek tabloda birleştirilir.',
exportAllDevices: 'Her aygıt (aygıt başına ayrıntı)',
exportAllInterfaces: 'Her arabirim (arabirim başına ayrıntı)',
exportFormat: 'Biçim',
exportCsvHelper: 'Birden fazla metrik seçildiğinde ayrı CSV dosyaları ZIP arşivinde paketlenir.',
exportJsonHelper: 'Tek bir yapılandırılmış JSON dosyası olarak dışa aktar.',
exportXlsxHelper: 'Her metrik ayrı bir çalışma sayfası olarak dışa aktarılır.',
exportTruncated: 'Excel satır sınırı aşıldı; şu veri kümeleri kırpıldı: {0}',
exportEmptyTables: 'Seçilen aralıkta veri yok: {0}',
exportAggregate: 'Birleşik toplam (tüm aygıtlar)',
exportAllGPUs: 'Her model (model başına ayrıntı)',
exportOptionsFailed: 'Şu metrikler için aygıt listesi yüklenemedi: {0}. Sayfayı yenileyip tekrar deneyin.',
selectMetrics: 'Dışa aktarmak için en az bir metrik seçin',
selectTimeRange: 'Dışa aktarılacak zaman aralığını seçin',
},
terminal: {
showTerminalButton: 'Terminal kısayolu',
@@ -2873,6 +2921,9 @@ const message = {
privateKey: 'Özel Anahtar',
publicKey: 'Genel Anahtar',
password: 'Parola',
existingPassPhrase: 'Mevcut özel anahtar parolası',
existingPassPhraseHelper:
'Özel anahtar şifreliyse mevcut parolayı girin; değilse boş bırakın. Bu işlem parolayı değiştirmez.',
createMode: 'Oluşturma Yöntemi',
generate: 'Otomatik Oluştur',
unSyncPass: 'Anahtar parolası senkronize edilemez',
@@ -7260,9 +7311,34 @@ const message = {
orphan_snapshot: 'Sahipsiz anlık görüntü',
partial_file: 'Tamamlanmamış dosya',
},
disks: 'Diskler',
resizeDisk: 'Genişlet',
diskTarget: 'Aygıt',
diskCapacityAfterResize: 'Genişletme sonrası toplam kapasite',
diskResizeHelper:
'Bu işlem yalnızca sanal disk kapasitesini artırır. Ardından sanal makine içindeki bölümleri ve dosya sistemlerini genişletin.',
diskResizeLimit:
'Sanal makine kapalı olmalı ve qcow2 diski bağımsız, yazılabilir, anlık görüntüsüz ve temel imaj zinciri olmadan kullanılmalıdır. Küçültme desteklenmez.',
diskResizeReason: {
referenceCheck:
'Sanal disk başvuruları doğrulanamadı. Genişletme kullanılamıyor; diğer sanal makinelerin disklerini kontrol edin.',
referenced:
'Bu sanal disk başka bir sanal makine veya aygıt tarafından kullanılıyor; genişletme henüz desteklenmiyor.',
running: 'Diski genişletmeden önce sanal makineyi kapatın.',
saved: 'Genişletmeden önce sanal makineyi sürdürün ve normal şekilde kapatın.',
snapshots: 'Geri yüklemeyi etkilememek için anlık görüntüler varken genişletme desteklenmiyor.',
backing: 'Disk başka bir imaja bağlı; genişletme henüz desteklenmiyor.',
shared: 'Disk paylaşımlı olarak işaretlenmiş; diğer kullanıcıları etkilememek için genişletme desteklenmiyor.',
unsupported: 'Disk biçimi, konumu veya erişim izinleri genişletmeyi desteklemiyor.',
unavailable: 'Disk durumu doğrulanamıyor. Kontrol edip yenileyin.',
},
diskSize: 'Disk Boyutu',
isoHelper:
'Birden fazla ISO bağlayabilirsiniz. Önce işletim sistemi kurulum imajını, ardından VirtIO gibi sürücü imajlarını seçin. Her imaj ayrı bir CD-ROM sürücüsü kullanır.',
windowsDriverHelper:
'Windows kalıbı VirtIO ağ sürücüsünü (NetKVM) içermeyebilir. Bir VirtIO sürücü ISO dosyası da bağlayın ve sürücüyü Windows içinde yükleyin. Kalıp sürücüyü zaten içeriyorsa bu bildirimi yok sayın.',
windowsDiskDriverHelper:
'VirtIO diskleri için disk sürücüsünün (viostor) yüklenmesi de gerekebilir; aksi takdirde Windows Kurulumu diski algılamayabilir.',
osType: 'İşletim sistemi türü',
osOther: 'Diğer',
diskBus: 'Disk veri yolu',
@@ -7314,6 +7390,10 @@ const message = {
rx: 'RX',
tx: 'TX',
console: 'Konsol',
consoleShortcutKeys: 'Kısayol tuşları',
consoleCustomKeys: 'Özel tuş kombinasyonu',
consoleMainKey: 'Ana tuşu seçin',
consoleSendKeys: 'Gönder',
consoleNotReady: 'VNC konsolu kullanılamıyor. VMyi başlatın ve VNC grafik yapılandırmasını kontrol edin.',
consoleDisconnected: 'VNC konsolu bağlantısı kesildi.',
consoleConnectFailed: 'VNC konsoluna bağlanılamadı. Konsol proxy hizmetini kontrol edin.',
+74 -1
View File
@@ -1428,6 +1428,33 @@ const message = {
ignoreEos: '忽略 EOS',
extraHeaders: '額外請求標頭',
},
vllmMonitor: {
cleanHelper: '僅清空目前 vLLM 實例的監控歷史,不影響其他實例、主機或 GPU 監控,是否繼續?',
overview: '概覽',
latencyAnalysis: '延遲分析',
requests: '請求數',
tokenThroughput: 'Token 吞吐量',
running: '執行中請求',
waiting: '等待中請求',
cacheUsage: 'KV Cache 使用率',
promptThroughput: '輸入 Token 吞吐',
generationThroughput: '輸出 Token 吞吐',
requestThroughput: '完成請求速率',
timeToFirstToken: '平均首 Token 延遲',
timePerOutputToken: '平均 Token 間延遲',
requestLatency: '平均請求延遲',
unavailable: '無法採集指標,請確認實例已啟動且 /metrics 可存取。',
loadFailed: '監控資料載入失敗,請重試。',
helper: '採集開關、頻率和儲存天數統一在監控設定中調整。',
cache: 'KV 快取',
throughput: '請求吞吐',
phaseTime: '預填充與解碼耗時',
prefillTime: '平均預填充耗時',
decodeTime: '平均解碼耗時',
requestLatencyChart: '端到端請求延遲',
timeToFirstTokenChart: '首 Token 延遲',
timePerOutputTokenChart: 'Token 間延遲',
},
gpu: {
memoryActivity: '顯示記憶體讀寫活躍率',
hotspotTemperature: '熱點溫度',
@@ -2001,6 +2028,9 @@ const message = {
cleanLogscope: '清理類型',
},
monitor: {
hostMonitor: '主機監控',
gpuMonitor: 'GPU 監控',
vllmMonitor: 'vLLM 監控',
globalFilter: '全域過濾',
enableMonitor: '監控狀態',
storeDays: '保存天數',
@@ -2009,7 +2039,7 @@ const message = {
defaultIO: '預設磁碟',
defaultIOHelper: '預設監控和概覽介面顯示的磁碟選項',
cleanMonitor: '清空監控記錄',
cleanHelper: '該操作將清空包括 GPU 在內的所有監控記錄,是否繼續?',
cleanHelper: '此操作將清空 {0} 的所有歷史記錄,是否繼續?',
avgLoad: '平均負載',
loadDetail: '負載詳情',
resourceUsage: '資源使用率',
@@ -2061,6 +2091,22 @@ const message = {
profileGoroutineDesc: '儲存所有 Goroutine 的狀態和呼叫堆疊,並依相同呼叫鏈彙總。',
profileMutexDesc: '統計互斥鎖競爭造成的等待時間,用於定位鎖競爭熱點。',
profileBlockDesc: '統計 Channel、Select 和同步原語的阻塞等待,用於定位長時間阻塞。',
exportRange: '時間範圍',
exportMetrics: '匯出指標',
exportMetricsHelper: 'CPU、記憶體與負載指標將合併到同一張表。',
exportAllDevices: '每個設備(逐設備明細)',
exportAllInterfaces: '每個介面(逐介面明細)',
exportFormat: '匯出格式',
exportCsvHelper: '選擇多個指標時將分別產生 CSV 檔案並打包為 ZIP。',
exportJsonHelper: '匯出為單一結構化 JSON 檔案。',
exportXlsxHelper: '每個指標產生一個獨立工作表。',
exportTruncated: '已超出 Excel 單表行數上限,以下數據集已截斷:{0}',
exportEmptyTables: '以下指標在所選時間段內沒有數據:{0}',
exportAggregate: '合計(所有裝置求和)',
exportAllGPUs: '每個型號(逐型號明細)',
exportOptionsFailed: '無法載入以下指標的裝置清單,請重新整理後重試:{0}',
selectMetrics: '請至少選擇一個匯出指標',
selectTimeRange: '請選擇匯出時間範圍',
},
terminal: {
showTerminalButton: '終端捷徑',
@@ -2674,6 +2720,8 @@ const message = {
privateKey: '私鑰',
publicKey: '公鑰',
password: '密碼',
existingPassPhrase: '現有私鑰密碼',
existingPassPhraseHelper: '私鑰已加密時填寫原密碼,未加密則留空;此處不會修改私鑰密碼。',
createMode: '建立方式',
generate: '自動生成',
unSyncPass: '金鑰密碼無法同步',
@@ -6649,8 +6697,29 @@ const message = {
orphan_snapshot: '孤立快照',
partial_file: '未完成檔案',
},
disks: '磁碟管理',
resizeDisk: '擴容',
diskTarget: '裝置',
diskCapacityAfterResize: '擴容後總容量',
diskResizeHelper: '擴容只增加虛擬磁碟容量,完成後還需在虛擬機內擴充分割區和檔案系統。',
diskResizeLimit: '僅支援關機狀態下、無快照和映像相依鏈的獨立可寫入 qcow2 磁碟擴容,不支援縮容。',
diskResizeReason: {
referenceCheck: '無法完成虛擬硬碟引用檢查,暫不支援擴容,請檢查其他虛擬機的磁碟狀態',
referenced: '此虛擬硬碟被其他虛擬機或裝置使用,暫不支援擴容',
running: '請先關閉虛擬機,再擴容磁碟',
saved: '請先恢復虛擬機執行,再正常關機後擴容',
snapshots: '存在快照,暫不支援擴容,以免影響快照還原',
backing: '磁碟相依於其他映像,暫不支援擴容',
shared: '磁碟已設為共用,暫不支援擴容,以免影響其他使用者',
unsupported: '磁碟格式、位置或讀寫權限不支援擴容',
unavailable: '無法確認磁碟狀態,請檢查後重新整理',
},
diskSize: '磁碟容量',
isoHelper: '支援掛載多個 ISO。請先選擇系統安裝映像,再選擇 VirtIO 等驅動程式映像;每個映像使用獨立光碟機。',
windowsDriverHelper:
'Windows 映像可能未整合 VirtIO 網路卡驅動程式(NetKVM),建議同時掛載 VirtIO 驅動程式 ISO 並在系統中安裝;已整合驅動程式的映像可忽略。',
windowsDiskDriverHelper:
'使用 VirtIO 磁碟時,還可能需要載入磁碟驅動程式(viostor),否則安裝程式可能無法辨識磁碟。',
osType: '作業系統類型',
osOther: '其他',
diskBus: '磁碟匯流排',
@@ -6700,6 +6769,10 @@ const message = {
rx: '接收',
tx: '傳送',
console: '控制台',
consoleShortcutKeys: '快捷按鍵',
consoleCustomKeys: '自訂組合鍵',
consoleMainKey: '選擇主鍵',
consoleSendKeys: '傳送',
consoleNotReady: 'VNC 控制台不可用,請啟動虛擬機並檢查 VNC 圖形設定。',
consoleDisconnected: 'VNC 控制台已中斷連線。',
consoleConnectFailed: 'VNC 控制台連線失敗,請檢查控制台代理服務。',
+74 -1
View File
@@ -1441,6 +1441,33 @@ const message = {
ignoreEos: '忽略 EOS',
extraHeaders: '额外请求头',
},
vllmMonitor: {
cleanHelper: '仅清空当前 vLLM 实例的监控历史,不影响其他实例、主机或 GPU 监控,是否继续?',
overview: '概览',
latencyAnalysis: '延迟分析',
requests: '请求数',
tokenThroughput: 'Token 吞吐',
running: '运行中请求',
waiting: '等待中请求',
cacheUsage: 'KV Cache 使用率',
promptThroughput: '输入 Token 吞吐',
generationThroughput: '输出 Token 吞吐',
requestThroughput: '完成请求速率',
timeToFirstToken: '平均首 Token 延迟',
timePerOutputToken: '平均 Token 间延迟',
requestLatency: '平均请求延迟',
unavailable: '无法采集指标,请确认实例已启动且 /metrics 可访问。',
loadFailed: '监控数据加载失败,请重试。',
helper: '采集开关、频率和保存天数在监控设置的 vLLM 监控中独立调整。',
cache: 'KV 缓存',
throughput: '请求吞吐',
phaseTime: '预填充与解码耗时',
prefillTime: '平均预填充耗时',
decodeTime: '平均解码耗时',
requestLatencyChart: '端到端请求延迟',
timeToFirstTokenChart: '首 Token 延迟',
timePerOutputTokenChart: 'Token 间延迟',
},
gpu: {
memoryActivity: '显存读写活跃率',
hotspotTemperature: '热点温度',
@@ -2030,6 +2057,9 @@ const message = {
cleanLogscope: '清理类型',
},
monitor: {
hostMonitor: '主机监控',
gpuMonitor: 'GPU 监控',
vllmMonitor: 'vLLM 监控',
globalFilter: '全局筛选',
enableMonitor: '监控状态',
storeDays: '保存天数',
@@ -2038,7 +2068,7 @@ const message = {
defaultIO: '默认磁盘',
defaultIOHelper: '默认监控和概览界面显示的磁盘选项',
cleanMonitor: '清空监控记录',
cleanHelper: '该操作将清空包括 GPU 在内的所有监控记录,是否继续?',
cleanHelper: '该操作将清空 {0} 的所有历史记录,是否继续?',
avgLoad: '平均负载',
loadDetail: '负载详情',
@@ -2091,6 +2121,22 @@ const message = {
profileGoroutineDesc: '保存所有 Goroutine 的状态和调用栈,并按相同调用链聚合。',
profileMutexDesc: '统计互斥锁竞争造成的等待时间,用于定位锁竞争热点。',
profileBlockDesc: '统计 Channel、Select 和同步原语的阻塞等待,用于定位长时间阻塞。',
exportRange: '时间范围',
exportMetrics: '导出指标',
exportMetricsHelper: 'CPU、内存与负载指标将合并到同一张表。',
exportAllDevices: '每个设备(逐设备明细)',
exportAllInterfaces: '每个接口(逐接口明细)',
exportFormat: '导出格式',
exportCsvHelper: '选择多个指标时将分别生成 CSV 文件并打包为 ZIP。',
exportJsonHelper: '导出为单个结构化 JSON 文件。',
exportXlsxHelper: '每个指标生成一个独立工作表。',
exportTruncated: '已超出 Excel 单表行数上限,以下数据集已截断:{0}',
exportEmptyTables: '以下指标在所选时间段内没有数据:{0}',
exportAggregate: '合计(所有设备求和)',
exportAllGPUs: '每个型号(逐型号明细)',
exportOptionsFailed: '无法加载以下指标的设备列表,请刷新页面后重试:{0}',
selectMetrics: '请至少选择一个导出指标',
selectTimeRange: '请选择导出时间范围',
},
terminal: {
showTerminalButton: '终端快捷入口',
@@ -2705,6 +2751,8 @@ const message = {
privateKey: '私钥',
publicKey: '公钥',
password: '密码',
existingPassPhrase: '已有私钥密码',
existingPassPhraseHelper: '私钥已加密时填写原密码,未加密则留空;此处不会修改私钥密码。',
createMode: '创建方式',
generate: '自动生成',
unSyncPass: '密钥密码无法同步',
@@ -6712,8 +6760,29 @@ const message = {
orphan_snapshot: '孤立快照',
partial_file: '未完成文件',
},
disks: '磁盘管理',
resizeDisk: '扩容',
diskTarget: '设备',
diskCapacityAfterResize: '扩容后总容量',
diskResizeHelper: '扩容只增加虚拟磁盘容量,完成后还需在虚拟机内扩展分区和文件系统。',
diskResizeLimit: '仅支持关机状态下、无快照和镜像链的独立可写 qcow2 磁盘扩容,不支持缩容。',
diskResizeReason: {
referenceCheck: '无法完成虚拟硬盘引用检查,暂不支持扩容,请检查其他虚拟机的磁盘状态',
referenced: '该虚拟硬盘被其他虚拟机或设备使用,暂不支持扩容',
running: '请先关闭虚拟机,再扩容磁盘',
saved: '请先恢复虚拟机运行,再正常关机后扩容',
snapshots: '存在快照,暂不支持扩容,以免影响快照恢复',
backing: '磁盘依赖其他镜像,暂不支持扩容',
shared: '磁盘已设为共享,暂不支持扩容,以免影响其他使用方',
unsupported: '磁盘格式、位置或读写权限不支持扩容',
unavailable: '无法确认磁盘状态,请检查后刷新',
},
diskSize: '磁盘容量',
isoHelper: '支持挂载多个 ISO。请先选择系统安装镜像,再选择 VirtIO 等驱动镜像;每个镜像使用独立光驱。',
windowsDriverHelper:
'Windows 镜像可能未集成 VirtIO 网卡驱动(NetKVM),建议同时挂载 VirtIO 驱动 ISO 并在系统中安装;已集成驱动的镜像可忽略。',
windowsDiskDriverHelper:
'使用 VirtIO 磁盘时,还可能需要加载磁盘驱动(viostor),否则安装程序可能无法识别磁盘。',
osType: '操作系统类型',
osOther: '其他',
diskBus: '磁盘总线',
@@ -6763,6 +6832,10 @@ const message = {
rx: '接收',
tx: '发送',
console: '控制台',
consoleShortcutKeys: '快捷按键',
consoleCustomKeys: '自定义组合键',
consoleMainKey: '选择主键',
consoleSendKeys: '发送',
consoleNotReady: 'VNC 控制台不可用,请启动虚拟机并检查 VNC 图形配置。',
consoleDisconnected: 'VNC 控制台已断开连接。',
consoleConnectFailed: 'VNC 控制台连接失败,请检查控制台代理服务。',
+12
View File
@@ -32,6 +32,18 @@ const hostRouter = {
permission: 'host_monitor_view',
},
},
{
path: '/hosts/monitor/export',
name: 'HostMonitorExport',
component: () => import('@/views/host/monitor/export/index.vue'),
hidden: true,
meta: {
activeMenu: '/hosts/monitor/monitor',
title: 'menu.monitor',
detail: 'commons.button.export',
permission: 'host_monitor_view',
},
},
{
path: '/hosts/monitor/setting',
name: 'HostMonitorSetting',
+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;
+8 -2
View File
@@ -145,14 +145,20 @@ export function downloadFile(filePath: string, currentNode: string) {
window.open(buildFileDownloadUrl(filePath, currentNode), '_blank');
}
export function downloadWithContent(content: string, fileName: string) {
const downloadUrl = window.URL.createObjectURL(new Blob([content]));
export function downloadBlob(content: string | Uint8Array<ArrayBuffer>, fileName: string, type?: string) {
const downloadUrl = window.URL.createObjectURL(new Blob([content], type ? { type } : undefined));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = fileName;
const event = new MouseEvent('click');
a.dispatchEvent(event);
// revoke asynchronously: revoking right after the click can cancel the download in some browsers
setTimeout(() => window.URL.revokeObjectURL(downloadUrl), 1000);
}
export function downloadWithContent(content: string, fileName: string) {
downloadBlob(content, fileName);
}
const editorLanguages = new Map(
+338
View File
@@ -0,0 +1,338 @@
import { dateFormatRFC3339 } from '@/utils/date';
import { downloadBlob } from '@/utils/file';
export interface MonitorExportTable {
name: string;
fields: string[];
headers: string[];
rows: any[][];
}
const formatCell = (cell: any): string => {
if (cell === null || cell === undefined) {
return '';
}
if (cell instanceof Date) {
// RFC3339 keeps the UTC offset so exported timestamps stay unambiguous across DST transitions
return dateFormatRFC3339(cell);
}
return String(cell);
};
const isNumberCell = (cell: any): boolean => typeof cell === 'number' && Number.isFinite(cell);
export function buildMonitorCSV(table: MonitorExportTable): string {
const escape = (value: string) => {
// neutralize formula execution when a non-numeric cell starts with = + - @ or a control char
let sanitized = value;
if (/^[=+\-@\t]/.test(sanitized) && Number.isNaN(Number(sanitized))) {
sanitized = `'${sanitized}`;
}
return /[",\n\r]/.test(sanitized) ? `"${sanitized.replace(/"/g, '""')}"` : sanitized;
};
const lines = [table.headers.map(escape).join(',')];
for (const row of table.rows) {
lines.push(row.map((cell) => escape(formatCell(cell))).join(','));
}
return String.fromCharCode(0xfeff) + lines.join('\r\n');
}
export function buildMonitorJSON(tables: MonitorExportTable[]): string {
const data: Record<string, any[]> = {};
for (const table of tables) {
data[table.name] = table.rows.map((row) => {
const item: Record<string, any> = {};
table.fields.forEach((field, i) => {
item[field] = row[i] instanceof Date ? formatCell(row[i]) : (row[i] ?? null);
});
return item;
});
}
return JSON.stringify(data, null, 2);
}
// minimal zip writer for csv bundles and xlsx containers: deflate when the runtime offers it, store otherwise
const crcTable = (() => {
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c;
}
return table;
})();
const crc32 = (data: Uint8Array): number => {
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = crcTable[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
};
const dosDateTime = (date: Date) => {
const time =
((date.getHours() & 0x1f) << 11) | ((date.getMinutes() & 0x3f) << 5) | ((date.getSeconds() >> 1) & 0x1f);
const day =
(((date.getFullYear() - 1980) & 0x7f) << 9) | (((date.getMonth() + 1) & 0xf) << 5) | (date.getDate() & 0x1f);
return { time, day };
};
export interface ZipEntry {
name: string;
content: string | Uint8Array<ArrayBuffer>;
}
// deflate-raw through the browser's native CompressionStream; null means unsupported, caller stores instead
const deflateRaw = async (data: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer> | null> => {
if (typeof CompressionStream === 'undefined') {
return null;
}
try {
const stream = new Blob([data]).stream().pipeThrough(new CompressionStream('deflate-raw'));
return new Uint8Array(await new Response(stream).arrayBuffer());
} catch {
// engines that predate the 'deflate-raw' format throw on construction
return null;
}
};
export async function zipFiles(entries: ZipEntry[]): Promise<Uint8Array<ArrayBuffer>> {
const encoder = new TextEncoder();
const chunks: Uint8Array[] = [];
const central: Uint8Array[] = [];
let offset = 0;
const { time, day } = dosDateTime(new Date());
const prepared = await Promise.all(
entries.map(async (entry) => {
const raw = typeof entry.content === 'string' ? encoder.encode(entry.content) : entry.content;
const deflated = await deflateRaw(raw);
const useDeflate = deflated !== null && deflated.length < raw.length;
return {
nameBytes: encoder.encode(entry.name),
crc: crc32(raw),
size: raw.length,
method: useDeflate ? 8 : 0,
data: useDeflate ? deflated : raw,
};
}),
);
for (const item of prepared) {
const local = new DataView(new ArrayBuffer(30));
local.setUint32(0, 0x04034b50, true);
local.setUint16(4, 20, true);
local.setUint16(6, 0x0800, true);
local.setUint16(8, item.method, true);
local.setUint16(10, time, true);
local.setUint16(12, day, true);
local.setUint32(14, item.crc, true);
local.setUint32(18, item.data.length, true);
local.setUint32(22, item.size, true);
local.setUint16(26, item.nameBytes.length, true);
chunks.push(new Uint8Array(local.buffer), item.nameBytes, item.data);
const header = new DataView(new ArrayBuffer(46));
header.setUint32(0, 0x02014b50, true);
header.setUint16(4, 20, true);
header.setUint16(6, 20, true);
header.setUint16(8, 0x0800, true);
header.setUint16(10, item.method, true);
header.setUint16(12, time, true);
header.setUint16(14, day, true);
header.setUint32(16, item.crc, true);
header.setUint32(20, item.data.length, true);
header.setUint32(24, item.size, true);
header.setUint16(28, item.nameBytes.length, true);
header.setUint32(42, offset, true);
central.push(new Uint8Array(header.buffer), item.nameBytes);
offset += 30 + item.nameBytes.length + item.data.length;
}
const centralOffset = offset;
let centralSize = 0;
for (const chunk of central) {
centralSize += chunk.length;
}
const end = new DataView(new ArrayBuffer(22));
end.setUint32(0, 0x06054b50, true);
end.setUint16(8, entries.length, true);
end.setUint16(10, entries.length, true);
end.setUint32(12, centralSize, true);
end.setUint32(16, centralOffset, true);
chunks.push(...central, new Uint8Array(end.buffer));
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(total);
let pos = 0;
for (const chunk of chunks) {
result.set(chunk, pos);
pos += chunk.length;
}
return result;
}
const xmlEscape = (value: string) =>
value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const xlsxColumnName = (index: number): string => {
let name = '';
for (let n = index; ;) {
name = String.fromCharCode(65 + (n % 26)) + name;
n = Math.floor(n / 26) - 1;
if (n < 0) {
return name;
}
}
};
// Excel serial keeps the local wall-clock time so the displayed value matches the panel UI
const xlsxDateSerial = (date: Date) => (date.getTime() - date.getTimezoneOffset() * 60000) / 86400000 + 25569;
const xlsxSheet = (table: MonitorExportTable): string => {
const parts = [
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>`,
];
const writeRow = (index: number, cells: any[], isHeader = false) => {
parts.push(`<row r="${index}">`);
cells.forEach((cell, i) => {
const ref = `${xlsxColumnName(i)}${index}`;
if (!isHeader && cell instanceof Date) {
parts.push(`<c r="${ref}" s="1"><v>${xlsxDateSerial(cell)}</v></c>`);
} else if (!isHeader && isNumberCell(cell)) {
parts.push(`<c r="${ref}"><v>${cell}</v></c>`);
} else {
parts.push(`<c r="${ref}" t="inlineStr"><is><t>${xmlEscape(formatCell(cell))}</t></is></c>`);
}
});
parts.push(`</row>`);
};
writeRow(1, table.headers, true);
table.rows.forEach((row, i) => writeRow(i + 2, row));
parts.push(`</sheetData></worksheet>`);
return parts.join('');
};
export function buildMonitorXLSX(tables: MonitorExportTable[]): Promise<Uint8Array<ArrayBuffer>> {
const sheets = tables.map(
(table, i) => `<sheet name="${xmlEscape(table.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`,
);
const overrides = tables
.map(
(_table, i) =>
`<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`,
)
.join('');
const sheetRels = tables
.map(
(_table, i) =>
`<Relationship Id="rId${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`,
)
.join('');
const entries: ZipEntry[] = [
{
name: '[Content_Types].xml',
content:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
`<Default Extension="xml" ContentType="application/xml"/>` +
`<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
`<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>` +
overrides +
`</Types>`,
},
{
name: '_rels/.rels',
content:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>` +
`</Relationships>`,
},
{
name: 'xl/workbook.xml',
content:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
`<sheets>${sheets.join('')}</sheets></workbook>`,
},
{
name: 'xl/_rels/workbook.xml.rels',
content:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${sheetRels}` +
`<Relationship Id="rId${tables.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>` +
`</Relationships>`,
},
{
name: 'xl/styles.xml',
content:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">` +
`<numFmts count="1"><numFmt numFmtId="164" formatCode="yyyy-mm-dd hh:mm:ss"/></numFmts>` +
`<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>` +
`<fills count="2"><fill><patternFill/></fill><fill><patternFill patternType="gray125"/></fill></fills>` +
`<borders count="1"><border/></borders>` +
`<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>` +
`<cellXfs count="2">` +
`<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>` +
`<xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>` +
`</cellXfs>` +
`<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>` +
`</styleSheet>`,
},
];
tables.forEach((table, i) => {
entries.push({ name: `xl/worksheets/sheet${i + 1}.xml`, content: xlsxSheet(table) });
});
return zipFiles(entries);
}
// Excel worksheets are limited to 1,048,576 rows (header included)
export const XLSX_MAX_DATA_ROWS = 1048575;
// returns the names of tables whose rows were truncated to fit format limits
export async function exportMonitorTables(
tables: MonitorExportTable[],
format: string,
baseName: string,
): Promise<string[]> {
switch (format) {
case 'json':
downloadBlob(buildMonitorJSON(tables), `${baseName}.json`, 'application/json');
return [];
case 'xlsx': {
const truncated: string[] = [];
const limited = tables.map((table) => {
if (table.rows.length <= XLSX_MAX_DATA_ROWS) {
return table;
}
truncated.push(table.name);
return { ...table, rows: table.rows.slice(0, XLSX_MAX_DATA_ROWS) };
});
downloadBlob(
await buildMonitorXLSX(limited),
`${baseName}.xlsx`,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return truncated;
}
default:
if (tables.length === 1) {
downloadBlob(buildMonitorCSV(tables[0]), `${baseName}-${tables[0].name}.csv`, 'text/csv');
} else {
const bundle = await zipFiles(
tables.map((table) => ({ name: `monitor-${table.name}.csv`, content: buildMonitorCSV(table) })),
);
downloadBlob(bundle, `${baseName}.zip`, 'application/zip');
}
return [];
}
}
@@ -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