From 90eca45347a878c5420dccbbb799fac501373eef Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:48:47 +0800 Subject: [PATCH] feat: Merge code of gpu from dev (#7982) --- agent/app/api/v2/ai.go | 256 ++++++++++ agent/app/api/v2/entry.go | 2 + agent/app/api/v2/terminal.go | 65 +++ agent/app/dto/ai.go | 44 ++ agent/app/dto/common_req.go | 5 + agent/app/model/ai.go | 11 + agent/app/repo/ai.go | 69 +++ agent/app/service/ai.go | 393 +++++++++++++++ agent/app/service/dashboard.go | 27 +- agent/app/service/entry.go | 2 + agent/app/service/website_utils.go | 52 ++ agent/init/migration/migrate.go | 1 + agent/init/migration/migrations/init.go | 10 + agent/router/ai.go | 29 ++ agent/router/common.go | 1 + agent/utils/ai_tools/gpu/common/gpu_info.go | 37 ++ agent/utils/ai_tools/gpu/gpu.go | 65 +++ agent/utils/ai_tools/gpu/schema_v12/parser.go | 55 +++ agent/utils/ai_tools/gpu/schema_v12/types.go | 294 +++++++++++ agent/utils/ai_tools/xpu/types.go | 43 ++ agent/utils/ai_tools/xpu/xpu.go | 254 ++++++++++ agent/utils/ai_tools/xpu/xpu_info.go | 49 ++ agent/utils/common/common.go | 20 + agent/utils/nginx/components/server.go | 74 +++ core/constant/common.go | 4 + frontend/src/api/interface/ai.ts | 111 +++++ frontend/src/api/modules/ai.ts | 41 ++ frontend/src/assets/iconfont/iconfont.css | 12 +- frontend/src/assets/iconfont/iconfont.js | 2 +- frontend/src/assets/iconfont/iconfont.json | 7 + frontend/src/assets/iconfont/iconfont.svg | 2 + frontend/src/assets/iconfont/iconfont.ttf | Bin 35168 -> 35420 bytes frontend/src/assets/iconfont/iconfont.woff | Bin 21424 -> 21556 bytes frontend/src/assets/iconfont/iconfont.woff2 | Bin 18352 -> 18468 bytes frontend/src/components/app-status/index.vue | 7 +- .../system-upgrade/upgrade/index.vue | 1 - frontend/src/lang/modules/en.ts | 107 ++-- frontend/src/lang/modules/ja.ts | 106 ++-- frontend/src/lang/modules/ko.ts | 105 ++-- frontend/src/lang/modules/ms.ts | 108 +++-- frontend/src/lang/modules/pt-br.ts | 108 +++-- frontend/src/lang/modules/ru.ts | 110 +++-- frontend/src/lang/modules/tw.ts | 102 ++-- frontend/src/lang/modules/zh.ts | 99 ++-- frontend/src/routers/modules/ai.ts | 34 ++ frontend/src/routers/modules/container.ts | 2 +- frontend/src/routers/modules/database.ts | 2 +- frontend/src/routers/modules/host.ts | 2 +- frontend/src/routers/modules/terminal.ts | 2 +- frontend/src/routers/modules/toolbox.ts | 2 +- frontend/src/views/ai/gpu/index.vue | 367 ++++++++++++++ frontend/src/views/ai/model/add/index.vue | 88 ++++ frontend/src/views/ai/model/conn/index.vue | 139 ++++++ frontend/src/views/ai/model/del/index.vue | 105 ++++ frontend/src/views/ai/model/domain/index.vue | 260 ++++++++++ frontend/src/views/ai/model/index.vue | 457 ++++++++++++++++++ .../src/views/ai/model/terminal/index.vue | 76 +++ 57 files changed, 4067 insertions(+), 359 deletions(-) create mode 100644 agent/app/api/v2/ai.go create mode 100644 agent/app/dto/ai.go create mode 100644 agent/app/model/ai.go create mode 100644 agent/app/repo/ai.go create mode 100644 agent/app/service/ai.go create mode 100644 agent/router/ai.go create mode 100644 agent/utils/ai_tools/gpu/common/gpu_info.go create mode 100644 agent/utils/ai_tools/gpu/gpu.go create mode 100644 agent/utils/ai_tools/gpu/schema_v12/parser.go create mode 100644 agent/utils/ai_tools/gpu/schema_v12/types.go create mode 100644 agent/utils/ai_tools/xpu/types.go create mode 100644 agent/utils/ai_tools/xpu/xpu.go create mode 100644 agent/utils/ai_tools/xpu/xpu_info.go create mode 100644 frontend/src/api/interface/ai.ts create mode 100644 frontend/src/api/modules/ai.ts create mode 100644 frontend/src/routers/modules/ai.ts create mode 100644 frontend/src/views/ai/gpu/index.vue create mode 100644 frontend/src/views/ai/model/add/index.vue create mode 100644 frontend/src/views/ai/model/conn/index.vue create mode 100644 frontend/src/views/ai/model/del/index.vue create mode 100644 frontend/src/views/ai/model/domain/index.vue create mode 100644 frontend/src/views/ai/model/index.vue create mode 100644 frontend/src/views/ai/model/terminal/index.vue diff --git a/agent/app/api/v2/ai.go b/agent/app/api/v2/ai.go new file mode 100644 index 000000000..3addb056a --- /dev/null +++ b/agent/app/api/v2/ai.go @@ -0,0 +1,256 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu" + "github.com/gin-gonic/gin" +) + +// @Tags AI +// @Summary Create Ollama model +// @Accept json +// @Param request body dto.OllamaModelName true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 Ollama 模型 [name]","formatEN":"add Ollama model [name]"} +func (b *BaseApi) CreateOllamaModel(c *gin.Context) { + var req dto.OllamaModelName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := aiToolService.Create(req.Name); err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags AI +// @Summary Rereate Ollama model +// @Accept json +// @Param request body dto.OllamaModelName true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/recreate [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 Ollama 模型重试 [name]","formatEN":"re-add Ollama model [name]"} +func (b *BaseApi) RecreateOllamaModel(c *gin.Context) { + var req dto.OllamaModelName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := aiToolService.Recreate(req.Name); err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags AI +// @Summary Close Ollama model conn +// @Accept json +// @Param request body dto.OllamaModelName true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/close [post] +// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"关闭 Ollama 模型连接 [name]","formatEN":"close conn for Ollama model [name]"} +func (b *BaseApi) CloseOllamaModel(c *gin.Context) { + var req dto.OllamaModelName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := aiToolService.Close(req.Name); err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags AI +// @Summary Sync Ollama model list +// @Success 200 {array} dto.OllamaModelDropList +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"} +func (b *BaseApi) SyncOllamaModel(c *gin.Context) { + list, err := aiToolService.Sync() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags AI +// @Summary Page Ollama models +// @Accept json +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/search [post] +func (b *BaseApi) SearchOllamaModel(c *gin.Context) { + var req dto.SearchWithPage + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + total, list, err := aiToolService.Search(req) + if err != nil { + helper.BadRequest(c, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags AI +// @Summary Page Ollama models +// @Accept json +// @Param request body dto.OllamaModelName true "request" +// @Success 200 {string} details +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/load [post] +func (b *BaseApi) LoadOllamaModelDetail(c *gin.Context) { + var req dto.OllamaModelName + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + detail, err := aiToolService.LoadDetail(req.Name) + if err != nil { + helper.BadRequest(c, err) + return + } + + helper.SuccessWithData(c, detail) +} + +// @Tags AI +// @Summary Delete Ollama model +// @Accept json +// @Param request body dto.ForceDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/ollama/model/del [post] +// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"ollama_models","output_column":"name","output_value":"names"}],"formatZH":"删除 Ollama 模型 [names]","formatEN":"remove Ollama model [names]"} +func (b *BaseApi) DeleteOllamaModel(c *gin.Context) { + var req dto.ForceDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := aiToolService.Delete(req); err != nil { + helper.BadRequest(c, err) + return + } + + helper.SuccessWithOutData(c) +} + +// @Tags AI +// @Summary Load gpu / xpu info +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/gpu/load [get] +func (b *BaseApi) LoadGpuInfo(c *gin.Context) { + ok, client := gpu.New() + if ok { + info, err := client.LoadGpuInfo() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, info) + return + } + xpuOK, xpuClient := xpu.New() + if xpuOK { + info, err := xpuClient.LoadGpuInfo() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, info) + return + } + helper.SuccessWithData(c, &common.GpuInfo{}) +} + +// @Tags AI +// @Summary Bind domain +// @Accept json +// @Param request body dto.OllamaBindDomain true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/domain/bind [post] +func (b *BaseApi) BindDomain(c *gin.Context) { + var req dto.OllamaBindDomain + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := aiToolService.BindDomain(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags AI +// @Summary Get bind domain +// @Accept json +// @Param request body dto.OllamaBindDomainReq true "request" +// @Success 200 {object} dto.OllamaBindDomainRes +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/domain/get [post] +func (b *BaseApi) GetBindDomain(c *gin.Context) { + var req dto.OllamaBindDomainReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := aiToolService.GetBindDomain(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// Tags AI +// Summary Update bind domain +// Accept json +// Param request body dto.OllamaBindDomain true "request" +// Success 200 +// Security ApiKeyAuth +// Security Timestamp +// Router /ai/domain/update [post] +func (b *BaseApi) UpdateBindDomain(c *gin.Context) { + var req dto.OllamaBindDomain + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := aiToolService.UpdateBindDomain(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index d622c8972..324c3100f 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -16,6 +16,8 @@ var ( appService = service.NewIAppService() appInstallService = service.NewIAppInstalledService() + aiToolService = service.NewIAIToolService() + containerService = service.NewIContainerService() composeTemplateService = service.NewIComposeTemplateService() imageRepoService = service.NewIImageRepoService() diff --git a/agent/app/api/v2/terminal.go b/agent/app/api/v2/terminal.go index 59b8149e0..360861dfe 100644 --- a/agent/app/api/v2/terminal.go +++ b/agent/app/api/v2/terminal.go @@ -10,6 +10,7 @@ import ( "time" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/terminal" @@ -165,6 +166,70 @@ func (b *BaseApi) ContainerWsSsh(c *gin.Context) { } } +func (b *BaseApi) OllamaWsSsh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + if global.CONF.Base.IsDemo { + if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { + return + } + } + + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return + } + name := c.Query("name") + if cmd.CheckIllegal(name) { + if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) { + return + } + } + container, err := service.LoadContainerName() + if wshandleError(wsConn, errors.WithMessage(err, " load container name for ollama failed")) { + return + } + commands := []string{"ollama", "run", name} + + pidMap := loadMapFromDockerTop(container) + fmt.Println("pidMap") + for k, v := range pidMap { + fmt.Println(k, v) + } + itemCmds := append([]string{"exec", "-it", container}, commands...) + slave, err := terminal.NewCommand(itemCmds) + if wshandleError(wsConn, err) { + return + } + defer killBash(container, strings.Join(commands, " "), pidMap) + defer slave.Close() + + tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false) + if wshandleError(wsConn, err) { + return + } + + quitChan := make(chan bool, 3) + tty.Start(quitChan) + go slave.Wait(quitChan) + + <-quitChan + + global.LOG.Info("websocket finished") + if wshandleError(wsConn, err) { + return + } +} + func wshandleError(ws *websocket.Conn, err error) bool { if err != nil { global.LOG.Errorf("handler ws faled:, err: %v", err) diff --git a/agent/app/dto/ai.go b/agent/app/dto/ai.go new file mode 100644 index 000000000..6c2065cae --- /dev/null +++ b/agent/app/dto/ai.go @@ -0,0 +1,44 @@ +package dto + +import "time" + +type OllamaModelInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Size string `json:"size"` + From string `json:"from"` + LogFileExist bool `json:"logFileExist"` + + Status string `json:"status"` + Message string `json:"message"` + CreatedAt time.Time `json:"createdAt"` +} + +type OllamaModelDropList struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type OllamaModelName struct { + Name string `json:"name"` +} + +type OllamaBindDomain struct { + Domain string `json:"domain" validate:"required"` + AppInstallID uint `json:"appInstallID" validate:"required"` + SSLID uint `json:"sslID"` + WebsiteID uint `json:"websiteID"` + IPList string `json:"ipList"` +} + +type OllamaBindDomainReq struct { + AppInstallID uint `json:"appInstallID" validate:"required"` +} + +type OllamaBindDomainRes struct { + Domain string `json:"domain"` + SSLID uint `json:"sslID"` + AllowIPs []string `json:"allowIPs"` + WebsiteID uint `json:"websiteID"` + ConnUrl string `json:"connUrl"` +} diff --git a/agent/app/dto/common_req.go b/agent/app/dto/common_req.go index 0315f1091..89f419400 100644 --- a/agent/app/dto/common_req.go +++ b/agent/app/dto/common_req.go @@ -71,3 +71,8 @@ type UpdateGroup struct { type OperateWithTask struct { TaskID string `json:"taskID"` } + +type ForceDelete struct { + IDs []uint `json:"ids"` + ForceDelete bool `json:"forceDelete"` +} diff --git a/agent/app/model/ai.go b/agent/app/model/ai.go new file mode 100644 index 000000000..2165e96e8 --- /dev/null +++ b/agent/app/model/ai.go @@ -0,0 +1,11 @@ +package model + +type OllamaModel struct { + BaseModel + + Name string `json:"name"` + Size string `json:"size"` + From string `json:"from"` + Status string `json:"status"` + Message string `json:"message"` +} diff --git a/agent/app/repo/ai.go b/agent/app/repo/ai.go new file mode 100644 index 000000000..f0368f896 --- /dev/null +++ b/agent/app/repo/ai.go @@ -0,0 +1,69 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type AiRepo struct{} + +type IAiRepo interface { + Get(opts ...DBOption) (model.OllamaModel, error) + List(opts ...DBOption) ([]model.OllamaModel, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.OllamaModel, error) + Create(cronjob *model.OllamaModel) error + Update(id uint, vars map[string]interface{}) error + Delete(opts ...DBOption) error +} + +func NewIAiRepo() IAiRepo { + return &AiRepo{} +} + +func (u *AiRepo) Get(opts ...DBOption) (model.OllamaModel, error) { + var item model.OllamaModel + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&item).Error + return item, err +} + +func (u *AiRepo) List(opts ...DBOption) ([]model.OllamaModel, error) { + var list []model.OllamaModel + db := global.DB.Model(&model.OllamaModel{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&list).Error + return list, err +} + +func (u *AiRepo) Page(page, size int, opts ...DBOption) (int64, []model.OllamaModel, error) { + var list []model.OllamaModel + db := global.DB.Model(&model.OllamaModel{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&list).Error + return count, list, err +} + +func (u *AiRepo) Create(item *model.OllamaModel) error { + return global.DB.Create(item).Error +} + +func (u *AiRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.OllamaModel{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *AiRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.OllamaModel{}).Error +} diff --git a/agent/app/service/ai.go b/agent/app/service/ai.go new file mode 100644 index 000000000..f00b1d151 --- /dev/null +++ b/agent/app/service/ai.go @@ -0,0 +1,393 @@ +package service + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "path" + "strings" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/jinzhu/copier" +) + +type AIToolService struct{} + +type IAIToolService interface { + Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) + Create(name string) error + Close(name string) error + Recreate(name string) error + Delete(req dto.ForceDelete) error + Sync() ([]dto.OllamaModelDropList, error) + LoadDetail(name string) (string, error) + BindDomain(req dto.OllamaBindDomain) error + GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error) + UpdateBindDomain(req dto.OllamaBindDomain) error +} + +func NewIAIToolService() IAIToolService { + return &AIToolService{} +} + +func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) { + var options []repo.DBOption + if len(req.Info) != 0 { + options = append(options, repo.WithByLikeName(req.Info)) + } + total, list, err := aiRepo.Page(req.Page, req.PageSize, options...) + if err != nil { + return 0, nil, err + } + var dtoLists []dto.OllamaModelInfo + for _, itemModel := range list { + var item dto.OllamaModelInfo + if err := copier.Copy(&item, &itemModel); err != nil { + return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) + } + logPath := path.Join(global.Dir.DataDir, "log", "AITools", itemModel.Name) + if _, err := os.Stat(logPath); err == nil { + item.LogFileExist = true + } + dtoLists = append(dtoLists, item) + } + return int64(total), dtoLists, err +} + +func (u *AIToolService) LoadDetail(name string) (string, error) { + if cmd.CheckIllegal(name) { + return "", buserr.New("ErrCmdIllegal") + } + containerName, err := LoadContainerName() + if err != nil { + return "", err + } + stdout, err := cmd.Execf("docker exec %s ollama show %s", containerName, name) + if err != nil { + return "", err + } + return stdout, err +} + +func (u *AIToolService) Create(name string) error { + if cmd.CheckIllegal(name) { + return buserr.New("ErrCmdIllegal") + } + modelInfo, _ := aiRepo.Get(repo.WithByName(name)) + if modelInfo.ID != 0 { + return buserr.New("ErrRecordExist") + } + containerName, err := LoadContainerName() + if err != nil { + return err + } + logItem := path.Join(global.Dir.DataDir, "log", "AITools", name) + if _, err := os.Stat(path.Dir(logItem)); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(logItem), os.ModePerm); err != nil { + return err + } + } + info := model.OllamaModel{ + Name: name, + From: "local", + Status: constant.StatusWaiting, + } + if err := aiRepo.Create(&info); err != nil { + return err + } + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + go pullOllamaModel(file, containerName, info) + return nil +} + +func (u *AIToolService) Close(name string) error { + if cmd.CheckIllegal(name) { + return buserr.New("ErrCmdIllegal") + } + containerName, err := LoadContainerName() + if err != nil { + return err + } + stdout, err := cmd.Execf("docker exec %s ollama stop %s", containerName, name) + if err != nil { + return fmt.Errorf("handle ollama stop %s failed, stdout: %s, err: %v", name, stdout, err) + } + return nil +} + +func (u *AIToolService) Recreate(name string) error { + if cmd.CheckIllegal(name) { + return buserr.New("ErrCmdIllegal") + } + modelInfo, _ := aiRepo.Get(repo.WithByName(name)) + if modelInfo.ID == 0 { + return buserr.New("ErrRecordNotFound") + } + containerName, err := LoadContainerName() + if err != nil { + return err + } + if err := aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusWaiting, "from": "local"}); err != nil { + return err + } + logItem := path.Join(global.Dir.DataDir, "log", "AITools", name) + if _, err := os.Stat(path.Dir(logItem)); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(path.Dir(logItem), os.ModePerm); err != nil { + return err + } + } + file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + go pullOllamaModel(file, containerName, modelInfo) + return nil +} + +func (u *AIToolService) Delete(req dto.ForceDelete) error { + ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs)) + if len(ollamaList) == 0 { + return buserr.New("ErrRecordNotFound") + } + containerName, err := LoadContainerName() + if err != nil && !req.ForceDelete { + return err + } + for _, item := range ollamaList { + if item.Status != constant.StatusDeleted { + stdout, err := cmd.Execf("docker exec %s ollama rm %s", containerName, item.Name) + if err != nil && !req.ForceDelete { + return fmt.Errorf("handle ollama rm %s failed, stdout: %s, err: %v", item.Name, stdout, err) + } + } + _ = aiRepo.Delete(repo.WithByID(item.ID)) + logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name) + _ = os.Remove(logItem) + } + return nil +} + +func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) { + containerName, err := LoadContainerName() + if err != nil { + return nil, err + } + stdout, err := cmd.Execf("docker exec %s ollama list", containerName) + if err != nil { + return nil, err + } + var list []model.OllamaModel + lines := strings.Split(stdout, "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) < 5 { + continue + } + if parts[0] == "NAME" { + continue + } + list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]}) + } + listInDB, _ := aiRepo.List() + var dropList []dto.OllamaModelDropList + for _, itemModel := range listInDB { + isExit := false + for i := 0; i < len(list); i++ { + if list[i].Name == itemModel.Name { + _ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size}) + list = append(list[:i], list[(i+1):]...) + isExit = true + break + } + } + if !isExit && itemModel.Status != constant.StatusWaiting { + _ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""}) + dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name}) + continue + } + } + for _, item := range list { + item.Status = constant.StatusSuccess + item.From = "remote" + _ = aiRepo.Create(&item) + } + + return dropList, nil +} + +func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error { + nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty) + if nginxInstall.ID == 0 { + return buserr.New("ErrOpenrestyInstall") + } + var ( + ipList []string + err error + ) + if len(req.IPList) > 0 { + ipList, err = common.HandleIPList(req.IPList) + if err != nil { + return err + } + } + createWebsiteReq := request.WebsiteCreate{ + Domains: []request.WebsiteDomain{{Domain: req.Domain}}, + Alias: strings.ToLower(req.Domain), + Type: constant.Deployment, + AppType: constant.InstalledApp, + AppInstallID: req.AppInstallID, + } + websiteService := NewIWebsiteService() + if err := websiteService.CreateWebsite(createWebsiteReq); err != nil { + return err + } + website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain))) + if err != nil { + return err + } + if len(ipList) > 0 { + if err = ConfigAllowIPs(ipList, website); err != nil { + return err + } + } + if req.SSLID > 0 { + sslReq := request.WebsiteHTTPSOp{ + WebsiteID: website.ID, + Enable: true, + Type: "existed", + WebsiteSSLID: req.SSLID, + HttpConfig: "HTTPSOnly", + } + if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil { + return err + } + } + if err = ConfigAIProxy(website); err != nil { + return err + } + return nil +} + +func (u *AIToolService) GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error) { + install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID)) + if err != nil { + return nil, err + } + res := &dto.OllamaBindDomainRes{} + website, _ := websiteRepo.GetFirst(websiteRepo.WithAppInstallId(install.ID)) + if website.ID == 0 { + return res, nil + } + res.WebsiteID = website.ID + res.Domain = website.PrimaryDomain + if website.WebsiteSSLID > 0 { + res.SSLID = website.WebsiteSSLID + } + res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain) + res.AllowIPs = GetAllowIps(website) + return res, nil +} + +func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error { + nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty) + if nginxInstall.ID == 0 { + return buserr.New("ErrOpenrestyInstall") + } + var ( + ipList []string + err error + ) + if len(req.IPList) > 0 { + ipList, err = common.HandleIPList(req.IPList) + if err != nil { + return err + } + } + websiteService := NewIWebsiteService() + website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + if err = ConfigAllowIPs(ipList, website); err != nil { + return err + } + if req.SSLID > 0 { + sslReq := request.WebsiteHTTPSOp{ + WebsiteID: website.ID, + Enable: true, + Type: "existed", + WebsiteSSLID: req.SSLID, + HttpConfig: "HTTPSOnly", + } + if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil { + return err + } + return nil + } + if website.WebsiteSSLID > 0 && req.SSLID == 0 { + sslReq := request.WebsiteHTTPSOp{ + WebsiteID: website.ID, + Enable: false, + } + if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil { + return err + } + } + return nil +} + +func LoadContainerName() (string, error) { + ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "") + if err != nil { + return "", fmt.Errorf("ollama service is not found, err: %v", err) + } + if ollamaBaseInfo.Status != constant.Running { + return "", fmt.Errorf("container %s of ollama is not running, please check and retry!", ollamaBaseInfo.ContainerName) + } + return ollamaBaseInfo.ContainerName, nil +} + +func pullOllamaModel(file *os.File, containerName string, info model.OllamaModel) { + defer file.Close() + cmd := exec.Command("docker", "exec", containerName, "ollama", "pull", info.Name) + multiWriter := io.MultiWriter(os.Stdout, file) + cmd.Stdout = multiWriter + cmd.Stderr = multiWriter + _ = cmd.Run() + itemSize, err := loadModelSize(info.Name, containerName) + if len(itemSize) != 0 { + _ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize}) + } else { + _ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) + } + _, _ = file.WriteString("ollama pull completed!") +} + +func loadModelSize(name string, containerName string) (string, error) { + stdout, err := cmd.Execf("docker exec %s ollama list | grep %s", containerName, name) + if err != nil { + return "", err + } + lines := strings.Split(string(stdout), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) < 5 { + continue + } + return parts[2] + " " + parts[3], nil + } + return "", fmt.Errorf("no such model %s in ollama list, std: %s", name, string(stdout)) +} diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go index 4a8efc361..c3ea58a4b 100644 --- a/agent/app/service/dashboard.go +++ b/agent/app/service/dashboard.go @@ -17,10 +17,11 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/copier" - "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/host" @@ -492,7 +493,17 @@ func loadDiskInfo() []dto.DiskInfo { } func loadGPUInfo() []dto.GPUInfo { - list := xpack.LoadGpuInfo() + ok, client := gpu.New() + var list []interface{} + if ok { + info, err := client.LoadGpuInfo() + if err != nil || len(info.GPUs) == 0 { + return nil + } + for _, item := range info.GPUs { + list = append(list, item) + } + } if len(list) == 0 { return nil } @@ -542,7 +553,17 @@ func ArryContains(arr []string, element string) bool { } func loadXpuInfo() []dto.XPUInfo { - list := xpack.LoadXpuInfo() + var list []interface{} + ok, xpuClient := xpu.New() + if ok { + xpus, err := xpuClient.LoadDashData() + if err != nil || len(xpus) == 0 { + return nil + } + for _, item := range xpus { + list = append(list, item) + } + } if len(list) == 0 { return nil } diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 8cb7d0a0e..2d2f6210b 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -11,6 +11,8 @@ var ( launcherRepo = repo.NewILauncherRepo() appInstallResourceRepo = repo.NewIAppInstallResourceRpo() + aiRepo = repo.NewIAiRepo() + mysqlRepo = repo.NewIMysqlRepo() postgresqlRepo = repo.NewIPostgresqlRepo() databaseRepo = repo.NewIDatabaseRepo() diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go index cd1c60884..648bdc398 100644 --- a/agent/app/service/website_utils.go +++ b/agent/app/service/website_utils.go @@ -1231,3 +1231,55 @@ func openProxyCache(website model.Website) error { proxyCachePath := fmt.Sprintf("/www/sites/%s/cache levels=1:2 keys_zone=proxy_cache_zone_of_%s:5m max_size=1g inactive=24h", website.Alias, website.Alias) return updateNginxConfig("", []dto.NginxParam{{Name: "proxy_cache_path", Params: []string{proxyCachePath}}}, &website) } + +func ConfigAllowIPs(ips []string, website model.Website) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return err + } + nginxConfig := nginxFull.SiteConfig + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + server.RemoveDirective("allow", nil) + server.RemoveDirective("deny", nil) + if len(ips) > 0 { + server.UpdateAllowIPs(ips) + } + if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxConfig.OldContent, config.FilePath, nginxFull.Install.ContainerName) +} + +func GetAllowIps(website model.Website) []string { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + dirs := server.GetDirectives() + var ips []string + for _, dir := range dirs { + if dir.GetName() == "allow" { + ips = append(ips, dir.GetParameters()...) + } + } + return ips +} + +func ConfigAIProxy(website model.Website) error { + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + dirs := server.GetDirectives() + for _, dir := range dirs { + if dir.GetName() == "location" && dir.GetParameters()[0] == "/" { + server.UpdateRootProxyForAi([]string{fmt.Sprintf("http://%s", website.Proxy)}) + } + } + return nil +} diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index f5021fbba..142da1dbf 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -24,6 +24,7 @@ func InitAgentDB() { migrations.InitBackup, migrations.UpdateAppTag, migrations.UpdateApp, + migrations.AddOllamaModel, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 8ca7c17cd..553743913 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -265,3 +265,13 @@ var UpdateApp = &gormigrate.Migration{ return nil }, } + +var AddOllamaModel = &gormigrate.Migration{ + ID: "20250218-add-ollama-model", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.OllamaModel{}); err != nil { + return err + } + return nil + }, +} diff --git a/agent/router/ai.go b/agent/router/ai.go new file mode 100644 index 000000000..2ade7abc0 --- /dev/null +++ b/agent/router/ai.go @@ -0,0 +1,29 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/agent/app/api/v2" + "github.com/gin-gonic/gin" +) + +type AIToolsRouter struct { +} + +func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { + aiToolsRouter := Router.Group("ai") + + baseApi := v1.ApiGroupApp.BaseApi + { + aiToolsRouter.GET("/ollama/exec", baseApi.OllamaWsSsh) + aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel) + aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel) + aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel) + aiToolsRouter.POST("/ollama/model/search", baseApi.SearchOllamaModel) + aiToolsRouter.POST("/ollama/model/sync", baseApi.SyncOllamaModel) + aiToolsRouter.POST("/ollama/model/load", baseApi.LoadOllamaModelDetail) + aiToolsRouter.POST("/ollama/model/del", baseApi.DeleteOllamaModel) + aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo) + aiToolsRouter.POST("/domain/bind", baseApi.BindDomain) + aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain) + aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain) + } +} diff --git a/agent/router/common.go b/agent/router/common.go index f54a07ddc..ace1b56d9 100644 --- a/agent/router/common.go +++ b/agent/router/common.go @@ -21,5 +21,6 @@ func commonGroups() []CommonRouter { &RuntimeRouter{}, &ProcessRouter{}, &WebsiteCARouter{}, + &AIToolsRouter{}, } } diff --git a/agent/utils/ai_tools/gpu/common/gpu_info.go b/agent/utils/ai_tools/gpu/common/gpu_info.go new file mode 100644 index 000000000..3d827eb40 --- /dev/null +++ b/agent/utils/ai_tools/gpu/common/gpu_info.go @@ -0,0 +1,37 @@ +package common + +type GpuInfo struct { + CudaVersion string `json:"cudaVersion"` + DriverVersion string `json:"driverVersion"` + Type string `json:"type"` + + GPUs []GPU `json:"gpu"` +} + +type GPU struct { + Index uint `json:"index"` + ProductName string `json:"productName"` + PersistenceMode string `json:"persistenceMode"` + BusID string `json:"busID"` + DisplayActive string `json:"displayActive"` + ECC string `json:"ecc"` + FanSpeed string `json:"fanSpeed"` + + Temperature string `json:"temperature"` + PerformanceState string `json:"performanceState"` + PowerDraw string `json:"powerDraw"` + MaxPowerLimit string `json:"maxPowerLimit"` + MemUsed string `json:"memUsed"` + MemTotal string `json:"memTotal"` + GPUUtil string `json:"gpuUtil"` + ComputeMode string `json:"computeMode"` + MigMode string `json:"migMode"` + Processes []Process `json:"processes"` +} + +type Process struct { + Pid string `json:"pid"` + Type string `json:"type"` + ProcessName string `json:"processName"` + UsedMemory string `json:"usedMemory"` +} diff --git a/agent/utils/ai_tools/gpu/gpu.go b/agent/utils/ai_tools/gpu/gpu.go new file mode 100644 index 000000000..ac21c32a8 --- /dev/null +++ b/agent/utils/ai_tools/gpu/gpu.go @@ -0,0 +1,65 @@ +package gpu + +import ( + "bytes" + _ "embed" + "encoding/xml" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/schema_v12" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type NvidiaSMI struct{} + +func New() (bool, NvidiaSMI) { + return cmd.Which("nvidia-smi"), NvidiaSMI{} +} + +func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) { + itemData, err := cmd.ExecWithTimeOut("nvidia-smi -q -x", 5*time.Second) + if err != nil { + return nil, fmt.Errorf("calling nvidia-smi failed, err: %w", err) + } + data := []byte(itemData) + schema := "v11" + + buf := bytes.NewBuffer(data) + decoder := xml.NewDecoder(buf) + for { + token, err := decoder.Token() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("reading token failed: %w", err) + } + d, ok := token.(xml.Directive) + if !ok { + continue + } + directive := string(d) + if !strings.HasPrefix(directive, "DOCTYPE") { + continue + } + parts := strings.Split(directive, " ") + s := strings.Trim(parts[len(parts)-1], "\" ") + if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") { + schema = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd") + } else { + global.LOG.Debugf("Cannot find schema version in %q", directive) + } + break + } + + if schema != "v12" { + return &common.GpuInfo{}, nil + } + return schema_v12.Parse(data) +} diff --git a/agent/utils/ai_tools/gpu/schema_v12/parser.go b/agent/utils/ai_tools/gpu/schema_v12/parser.go new file mode 100644 index 000000000..cc2c629fa --- /dev/null +++ b/agent/utils/ai_tools/gpu/schema_v12/parser.go @@ -0,0 +1,55 @@ +package schema_v12 + +import ( + "encoding/xml" + + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common" +) + +func Parse(buf []byte) (*common.GpuInfo, error) { + var ( + s smi + info common.GpuInfo + ) + if err := xml.Unmarshal(buf, &s); err != nil { + return nil, err + } + + info.Type = "nvidia" + info.CudaVersion = s.CudaVersion + info.DriverVersion = s.DriverVersion + if len(s.Gpu) == 0 { + return &info, nil + } + for i := 0; i < len(s.Gpu); i++ { + var gpuItem common.GPU + gpuItem.Index = uint(i) + gpuItem.ProductName = s.Gpu[i].ProductName + gpuItem.PersistenceMode = s.Gpu[i].PersistenceMode + gpuItem.BusID = s.Gpu[i].ID + gpuItem.DisplayActive = s.Gpu[i].DisplayActive + gpuItem.ECC = s.Gpu[i].EccErrors.Volatile.DramUncorrectable + gpuItem.FanSpeed = s.Gpu[i].FanSpeed + + gpuItem.Temperature = s.Gpu[i].Temperature.GpuTemp + gpuItem.PerformanceState = s.Gpu[i].PerformanceState + gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw + gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.MaxPowerLimit + gpuItem.MemUsed = s.Gpu[i].FbMemoryUsage.Used + gpuItem.MemTotal = s.Gpu[i].FbMemoryUsage.Total + gpuItem.GPUUtil = s.Gpu[i].Utilization.GpuUtil + gpuItem.ComputeMode = s.Gpu[i].ComputeMode + gpuItem.MigMode = s.Gpu[i].MigMode.CurrentMig + + for _, process := range s.Gpu[i].Processes.ProcessInfo { + gpuItem.Processes = append(gpuItem.Processes, common.Process{ + Pid: process.Pid, + Type: process.Type, + ProcessName: process.ProcessName, + UsedMemory: process.UsedMemory, + }) + } + info.GPUs = append(info.GPUs, gpuItem) + } + return &info, nil +} diff --git a/agent/utils/ai_tools/gpu/schema_v12/types.go b/agent/utils/ai_tools/gpu/schema_v12/types.go new file mode 100644 index 000000000..d878ca8ba --- /dev/null +++ b/agent/utils/ai_tools/gpu/schema_v12/types.go @@ -0,0 +1,294 @@ +package schema_v12 + +type smi struct { + AttachedGpus string `xml:"attached_gpus"` + CudaVersion string `xml:"cuda_version"` + DriverVersion string `xml:"driver_version"` + Gpu []struct { + ID string `xml:"id,attr"` + AccountedProcesses struct{} `xml:"accounted_processes"` + AccountingMode string `xml:"accounting_mode"` + AccountingModeBufferSize string `xml:"accounting_mode_buffer_size"` + AddressingMode string `xml:"addressing_mode"` + ApplicationsClocks struct { + GraphicsClock string `xml:"graphics_clock"` + MemClock string `xml:"mem_clock"` + } `xml:"applications_clocks"` + Bar1MemoryUsage struct { + Free string `xml:"free"` + Total string `xml:"total"` + Used string `xml:"used"` + } `xml:"bar1_memory_usage"` + BoardID string `xml:"board_id"` + BoardPartNumber string `xml:"board_part_number"` + CcProtectedMemoryUsage struct { + Free string `xml:"free"` + Total string `xml:"total"` + Used string `xml:"used"` + } `xml:"cc_protected_memory_usage"` + ClockPolicy struct { + AutoBoost string `xml:"auto_boost"` + AutoBoostDefault string `xml:"auto_boost_default"` + } `xml:"clock_policy"` + Clocks struct { + GraphicsClock string `xml:"graphics_clock"` + MemClock string `xml:"mem_clock"` + SmClock string `xml:"sm_clock"` + VideoClock string `xml:"video_clock"` + } `xml:"clocks"` + ClocksEventReasons struct { + ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"` + ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"` + ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"` + ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"` + ClocksEventReasonHwSlowdown string `xml:"clocks_event_reason_hw_slowdown"` + ClocksEventReasonHwThermalSlowdown string `xml:"clocks_event_reason_hw_thermal_slowdown"` + ClocksEventReasonSwPowerCap string `xml:"clocks_event_reason_sw_power_cap"` + ClocksEventReasonSwThermalSlowdown string `xml:"clocks_event_reason_sw_thermal_slowdown"` + ClocksEventReasonSyncBoost string `xml:"clocks_event_reason_sync_boost"` + } `xml:"clocks_event_reasons"` + ComputeMode string `xml:"compute_mode"` + DefaultApplicationsClocks struct { + GraphicsClock string `xml:"graphics_clock"` + MemClock string `xml:"mem_clock"` + } `xml:"default_applications_clocks"` + DeferredClocks struct { + MemClock string `xml:"mem_clock"` + } `xml:"deferred_clocks"` + DisplayActive string `xml:"display_active"` + DisplayMode string `xml:"display_mode"` + DriverModel struct { + CurrentDm string `xml:"current_dm"` + PendingDm string `xml:"pending_dm"` + } `xml:"driver_model"` + EccErrors struct { + Aggregate struct { + DramCorrectable string `xml:"dram_correctable"` + DramUncorrectable string `xml:"dram_uncorrectable"` + SramCorrectable string `xml:"sram_correctable"` + SramUncorrectable string `xml:"sram_uncorrectable"` + } `xml:"aggregate"` + Volatile struct { + DramCorrectable string `xml:"dram_correctable"` + DramUncorrectable string `xml:"dram_uncorrectable"` + SramCorrectable string `xml:"sram_correctable"` + SramUncorrectable string `xml:"sram_uncorrectable"` + } `xml:"volatile"` + } `xml:"ecc_errors"` + EccMode struct { + CurrentEcc string `xml:"current_ecc"` + PendingEcc string `xml:"pending_ecc"` + } `xml:"ecc_mode"` + EncoderStats struct { + AverageFps string `xml:"average_fps"` + AverageLatency string `xml:"average_latency"` + SessionCount string `xml:"session_count"` + } `xml:"encoder_stats"` + Fabric struct { + State string `xml:"state"` + Status string `xml:"status"` + } `xml:"fabric"` + FanSpeed string `xml:"fan_speed"` + FbMemoryUsage struct { + Free string `xml:"free"` + Reserved string `xml:"reserved"` + Total string `xml:"total"` + Used string `xml:"used"` + } `xml:"fb_memory_usage"` + FbcStats struct { + AverageFps string `xml:"average_fps"` + AverageLatency string `xml:"average_latency"` + SessionCount string `xml:"session_count"` + } `xml:"fbc_stats"` + GpuFruPartNumber string `xml:"gpu_fru_part_number"` + GpuModuleID string `xml:"gpu_module_id"` + GpuOperationMode struct { + CurrentGom string `xml:"current_gom"` + PendingGom string `xml:"pending_gom"` + } `xml:"gpu_operation_mode"` + GpuPartNumber string `xml:"gpu_part_number"` + GpuPowerReadings struct { + CurrentPowerLimit string `xml:"current_power_limit"` + DefaultPowerLimit string `xml:"default_power_limit"` + MaxPowerLimit string `xml:"max_power_limit"` + MinPowerLimit string `xml:"min_power_limit"` + PowerDraw string `xml:"power_draw"` + PowerState string `xml:"power_state"` + RequestedPowerLimit string `xml:"requested_power_limit"` + } `xml:"gpu_power_readings"` + GpuResetStatus struct { + DrainAndResetRecommended string `xml:"drain_and_reset_recommended"` + ResetRequired string `xml:"reset_required"` + } `xml:"gpu_reset_status"` + GpuVirtualizationMode struct { + HostVgpuMode string `xml:"host_vgpu_mode"` + VirtualizationMode string `xml:"virtualization_mode"` + } `xml:"gpu_virtualization_mode"` + GspFirmwareVersion string `xml:"gsp_firmware_version"` + Ibmnpu struct { + RelaxedOrderingMode string `xml:"relaxed_ordering_mode"` + } `xml:"ibmnpu"` + InforomVersion struct { + EccObject string `xml:"ecc_object"` + ImgVersion string `xml:"img_version"` + OemObject string `xml:"oem_object"` + PwrObject string `xml:"pwr_object"` + } `xml:"inforom_version"` + MaxClocks struct { + GraphicsClock string `xml:"graphics_clock"` + MemClock string `xml:"mem_clock"` + SmClock string `xml:"sm_clock"` + VideoClock string `xml:"video_clock"` + } `xml:"max_clocks"` + MaxCustomerBoostClocks struct { + GraphicsClock string `xml:"graphics_clock"` + } `xml:"max_customer_boost_clocks"` + MigDevices struct { + MigDevice []struct { + Index string `xml:"index"` + GpuInstanceID string `xml:"gpu_instance_id"` + ComputeInstanceID string `xml:"compute_instance_id"` + EccErrorCount struct { + Text string `xml:",chardata" json:"text"` + VolatileCount struct { + SramUncorrectable string `xml:"sram_uncorrectable"` + } `xml:"volatile_count" json:"volatile_count"` + } `xml:"ecc_error_count" json:"ecc_error_count"` + FbMemoryUsage struct { + Total string `xml:"total"` + Reserved string `xml:"reserved"` + Used string `xml:"used"` + Free string `xml:"free"` + } `xml:"fb_memory_usage" json:"fb_memory_usage"` + Bar1MemoryUsage struct { + Total string `xml:"total"` + Used string `xml:"used"` + Free string `xml:"free"` + } `xml:"bar1_memory_usage" json:"bar1_memory_usage"` + } `xml:"mig_device" json:"mig_device"` + } `xml:"mig_devices" json:"mig_devices"` + MigMode struct { + CurrentMig string `xml:"current_mig"` + PendingMig string `xml:"pending_mig"` + } `xml:"mig_mode"` + MinorNumber string `xml:"minor_number"` + ModulePowerReadings struct { + CurrentPowerLimit string `xml:"current_power_limit"` + DefaultPowerLimit string `xml:"default_power_limit"` + MaxPowerLimit string `xml:"max_power_limit"` + MinPowerLimit string `xml:"min_power_limit"` + PowerDraw string `xml:"power_draw"` + PowerState string `xml:"power_state"` + RequestedPowerLimit string `xml:"requested_power_limit"` + } `xml:"module_power_readings"` + MultigpuBoard string `xml:"multigpu_board"` + Pci struct { + AtomicCapsInbound string `xml:"atomic_caps_inbound"` + AtomicCapsOutbound string `xml:"atomic_caps_outbound"` + PciBridgeChip struct { + BridgeChipFw string `xml:"bridge_chip_fw"` + BridgeChipType string `xml:"bridge_chip_type"` + } `xml:"pci_bridge_chip"` + PciBus string `xml:"pci_bus"` + PciBusID string `xml:"pci_bus_id"` + PciDevice string `xml:"pci_device"` + PciDeviceID string `xml:"pci_device_id"` + PciDomain string `xml:"pci_domain"` + PciGpuLinkInfo struct { + LinkWidths struct { + CurrentLinkWidth string `xml:"current_link_width"` + MaxLinkWidth string `xml:"max_link_width"` + } `xml:"link_widths"` + PcieGen struct { + CurrentLinkGen string `xml:"current_link_gen"` + DeviceCurrentLinkGen string `xml:"device_current_link_gen"` + MaxDeviceLinkGen string `xml:"max_device_link_gen"` + MaxHostLinkGen string `xml:"max_host_link_gen"` + MaxLinkGen string `xml:"max_link_gen"` + } `xml:"pcie_gen"` + } `xml:"pci_gpu_link_info"` + PciSubSystemID string `xml:"pci_sub_system_id"` + ReplayCounter string `xml:"replay_counter"` + ReplayRolloverCounter string `xml:"replay_rollover_counter"` + RxUtil string `xml:"rx_util"` + TxUtil string `xml:"tx_util"` + } `xml:"pci"` + PerformanceState string `xml:"performance_state"` + PersistenceMode string `xml:"persistence_mode"` + PowerReadings struct { + PowerState string `xml:"power_state"` + PowerManagement string `xml:"power_management"` + PowerDraw string `xml:"power_draw"` + PowerLimit string `xml:"power_limit"` + DefaultPowerLimit string `xml:"default_power_limit"` + EnforcedPowerLimit string `xml:"enforced_power_limit"` + MinPowerLimit string `xml:"min_power_limit"` + MaxPowerLimit string `xml:"max_power_limit"` + } `xml:"power_readings"` + Processes struct { + ProcessInfo []struct { + Pid string `xml:"pid"` + Type string `xml:"type"` + ProcessName string `xml:"process_name"` + UsedMemory string `xml:"used_memory"` + } `xml:"process_info"` + } `xml:"processes"` + ProductArchitecture string `xml:"product_architecture"` + ProductBrand string `xml:"product_brand"` + ProductName string `xml:"product_name"` + RemappedRows struct { + // Manually added + Correctable string `xml:"remapped_row_corr"` + Uncorrectable string `xml:"remapped_row_unc"` + Pending string `xml:"remapped_row_pending"` + Failure string `xml:"remapped_row_failure"` + } `xml:"remapped_rows"` + RetiredPages struct { + DoubleBitRetirement struct { + RetiredCount string `xml:"retired_count"` + RetiredPagelist string `xml:"retired_pagelist"` + } `xml:"double_bit_retirement"` + MultipleSingleBitRetirement struct { + RetiredCount string `xml:"retired_count"` + RetiredPagelist string `xml:"retired_pagelist"` + } `xml:"multiple_single_bit_retirement"` + PendingBlacklist string `xml:"pending_blacklist"` + PendingRetirement string `xml:"pending_retirement"` + } `xml:"retired_pages"` + Serial string `xml:"serial"` + SupportedClocks struct { + SupportedMemClock []struct { + SupportedGraphicsClock []string `xml:"supported_graphics_clock"` + Value string `xml:"value"` + } `xml:"supported_mem_clock"` + } `xml:"supported_clocks"` + SupportedGpuTargetTemp struct { + GpuTargetTempMax string `xml:"gpu_target_temp_max"` + GpuTargetTempMin string `xml:"gpu_target_temp_min"` + } `xml:"supported_gpu_target_temp"` + Temperature struct { + GpuTargetTemperature string `xml:"gpu_target_temperature"` + GpuTemp string `xml:"gpu_temp"` + GpuTempMaxGpuThreshold string `xml:"gpu_temp_max_gpu_threshold"` + GpuTempMaxMemThreshold string `xml:"gpu_temp_max_mem_threshold"` + GpuTempMaxThreshold string `xml:"gpu_temp_max_threshold"` + GpuTempSlowThreshold string `xml:"gpu_temp_slow_threshold"` + GpuTempTlimit string `xml:"gpu_temp_tlimit"` + MemoryTemp string `xml:"memory_temp"` + } `xml:"temperature"` + Utilization struct { + DecoderUtil string `xml:"decoder_util"` + EncoderUtil string `xml:"encoder_util"` + GpuUtil string `xml:"gpu_util"` + JpegUtil string `xml:"jpeg_util"` + MemoryUtil string `xml:"memory_util"` + OfaUtil string `xml:"ofa_util"` + } `xml:"utilization"` + UUID string `xml:"uuid"` + VbiosVersion string `xml:"vbios_version"` + Voltage struct { + GraphicsVolt string `xml:"graphics_volt"` + } `xml:"voltage"` + } `xml:"gpu"` + Timestamp string `xml:"timestamp"` +} diff --git a/agent/utils/ai_tools/xpu/types.go b/agent/utils/ai_tools/xpu/types.go new file mode 100644 index 000000000..e78f6a7a5 --- /dev/null +++ b/agent/utils/ai_tools/xpu/types.go @@ -0,0 +1,43 @@ +package xpu + +type DeviceUtilByProc struct { + DeviceID int `json:"device_id"` + MemSize float64 `json:"mem_size"` + ProcessID int `json:"process_id"` + ProcessName string `json:"process_name"` + SharedMemSize float64 `json:"shared_mem_size"` +} + +type DeviceUtilByProcList struct { + DeviceUtilByProcList []DeviceUtilByProc `json:"device_util_by_proc_list"` +} + +type Device struct { + DeviceFunctionType string `json:"device_function_type"` + DeviceID int `json:"device_id"` + DeviceName string `json:"device_name"` + DeviceType string `json:"device_type"` + DrmDevice string `json:"drm_device"` + PciBdfAddress string `json:"pci_bdf_address"` + PciDeviceID string `json:"pci_device_id"` + UUID string `json:"uuid"` + VendorName string `json:"vendor_name"` + + MemoryPhysicalSizeByte string `json:"memory_physical_size_byte"` + MemoryFreeSizeByte string `json:"memory_free_size_byte"` + DriverVersion string `json:"driver_version"` +} + +type DeviceInfo struct { + DeviceList []Device `json:"device_list"` +} + +type DeviceLevelMetric struct { + MetricsType string `json:"metrics_type"` + Value float64 `json:"value"` +} + +type DeviceStats struct { + DeviceID int `json:"device_id"` + DeviceLevel []DeviceLevelMetric `json:"device_level"` +} diff --git a/agent/utils/ai_tools/xpu/xpu.go b/agent/utils/ai_tools/xpu/xpu.go new file mode 100644 index 000000000..1c9b5a11a --- /dev/null +++ b/agent/utils/ai_tools/xpu/xpu.go @@ -0,0 +1,254 @@ +package xpu + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "sync" + "time" + + baseGlobal "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +type XpuSMI struct{} + +func New() (bool, XpuSMI) { + return cmd.Which("xpu-smi"), XpuSMI{} +} + +func (x XpuSMI) loadDeviceData(device Device, wg *sync.WaitGroup, res *[]XPUSimpleInfo, mu *sync.Mutex) { + defer wg.Done() + + var xpu XPUSimpleInfo + xpu.DeviceID = device.DeviceID + xpu.DeviceName = device.DeviceName + + var xpuData, statsData string + var xpuErr, statsErr error + + var wgCmd sync.WaitGroup + wgCmd.Add(2) + + go func() { + defer wgCmd.Done() + xpuData, xpuErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi discovery -d %d -j", device.DeviceID), 5*time.Second) + }() + + go func() { + defer wgCmd.Done() + statsData, statsErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi stats -d %d -j", device.DeviceID), 5*time.Second) + }() + + wgCmd.Wait() + + if xpuErr != nil { + baseGlobal.LOG.Errorf("calling xpu-smi discovery failed for device %d, err: %v\n", device.DeviceID, xpuErr) + return + } + + var info Device + if err := json.Unmarshal([]byte(xpuData), &info); err != nil { + baseGlobal.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err) + return + } + + bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64) + if err != nil { + baseGlobal.LOG.Errorf("Error parsing memory size for device %d, err: %v\n", device.DeviceID, err) + return + } + xpu.Memory = fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) + + if statsErr != nil { + baseGlobal.LOG.Errorf("calling xpu-smi stats failed for device %d, err: %v\n", device.DeviceID, statsErr) + return + } + + var stats DeviceStats + if err := json.Unmarshal([]byte(statsData), &stats); err != nil { + baseGlobal.LOG.Errorf("statsData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err) + return + } + + for _, stat := range stats.DeviceLevel { + switch stat.MetricsType { + case "XPUM_STATS_POWER": + xpu.Power = fmt.Sprintf("%.1fW", stat.Value) + case "XPUM_STATS_GPU_CORE_TEMPERATURE": + xpu.Temperature = fmt.Sprintf("%.1f°C", stat.Value) + case "XPUM_STATS_MEMORY_USED": + xpu.MemoryUsed = fmt.Sprintf("%.1fMB", stat.Value) + case "XPUM_STATS_MEMORY_UTILIZATION": + xpu.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value) + } + } + + mu.Lock() + *res = append(*res, xpu) + mu.Unlock() +} + +func (x XpuSMI) LoadDashData() ([]XPUSimpleInfo, error) { + data, err := cmd.ExecWithTimeOut("xpu-smi discovery -j", 5*time.Second) + if err != nil { + return nil, fmt.Errorf("calling xpu-smi failed, err: %w", err) + } + + var deviceInfo DeviceInfo + if err := json.Unmarshal([]byte(data), &deviceInfo); err != nil { + return nil, fmt.Errorf("deviceInfo json unmarshal failed, err: %w", err) + } + + var res []XPUSimpleInfo + var wg sync.WaitGroup + var mu sync.Mutex + + for _, device := range deviceInfo.DeviceList { + wg.Add(1) + go x.loadDeviceData(device, &wg, &res, &mu) + } + + wg.Wait() + + sort.Slice(res, func(i, j int) bool { + return res[i].DeviceID < res[j].DeviceID + }) + return res, nil +} + +func (x XpuSMI) LoadGpuInfo() (*XpuInfo, error) { + data, err := cmd.ExecWithTimeOut("xpu-smi discovery -j", 5*time.Second) + if err != nil { + return nil, fmt.Errorf("calling xpu-smi failed, err: %w", err) + } + var deviceInfo DeviceInfo + if err := json.Unmarshal([]byte(data), &deviceInfo); err != nil { + return nil, fmt.Errorf("deviceInfo json unmarshal failed, err: %w", err) + } + res := &XpuInfo{ + Type: "xpu", + } + + var wg sync.WaitGroup + var mu sync.Mutex + + for _, device := range deviceInfo.DeviceList { + wg.Add(1) + go x.loadDeviceInfo(device, &wg, res, &mu) + } + + wg.Wait() + + processData, err := cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi ps -j"), 5*time.Second) + if err != nil { + return nil, fmt.Errorf("calling xpu-smi ps failed, err: %w", err) + } + var psList DeviceUtilByProcList + if err := json.Unmarshal([]byte(processData), &psList); err != nil { + return nil, fmt.Errorf("processData json unmarshal failed, err: %w", err) + } + for _, ps := range psList.DeviceUtilByProcList { + process := Process{ + PID: ps.ProcessID, + Command: ps.ProcessName, + } + if ps.SharedMemSize > 0 { + process.SHR = fmt.Sprintf("%.1f MB", ps.SharedMemSize/1024) + } + if ps.MemSize > 0 { + process.Memory = fmt.Sprintf("%.1f MB", ps.MemSize/1024) + } + for index, xpu := range res.Xpu { + if xpu.Basic.DeviceID == ps.DeviceID { + res.Xpu[index].Processes = append(res.Xpu[index].Processes, process) + } + } + } + + return res, nil +} + +func (x XpuSMI) loadDeviceInfo(device Device, wg *sync.WaitGroup, res *XpuInfo, mu *sync.Mutex) { + defer wg.Done() + + xpu := Xpu{ + Basic: Basic{ + DeviceID: device.DeviceID, + DeviceName: device.DeviceName, + VendorName: device.VendorName, + PciBdfAddress: device.PciBdfAddress, + }, + } + + var xpuData, statsData string + var xpuErr, statsErr error + + var wgCmd sync.WaitGroup + wgCmd.Add(2) + + go func() { + defer wgCmd.Done() + xpuData, xpuErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi discovery -d %d -j", device.DeviceID), 5*time.Second) + }() + + go func() { + defer wgCmd.Done() + statsData, statsErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi stats -d %d -j", device.DeviceID), 5*time.Second) + }() + + wgCmd.Wait() + + if xpuErr != nil { + baseGlobal.LOG.Errorf("calling xpu-smi discovery failed for device %d, err: %v\n", device.DeviceID, xpuErr) + return + } + + var info Device + if err := json.Unmarshal([]byte(xpuData), &info); err != nil { + baseGlobal.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err) + return + } + + res.DriverVersion = info.DriverVersion + xpu.Basic.DriverVersion = info.DriverVersion + + bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64) + if err != nil { + baseGlobal.LOG.Errorf("Error parsing memory size for device %d, err: %v\n", device.DeviceID, err) + return + } + xpu.Basic.Memory = fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) + xpu.Basic.FreeMemory = info.MemoryFreeSizeByte + + if statsErr != nil { + baseGlobal.LOG.Errorf("calling xpu-smi stats failed for device %d, err: %v\n", device.DeviceID, statsErr) + return + } + + var stats DeviceStats + if err := json.Unmarshal([]byte(statsData), &stats); err != nil { + baseGlobal.LOG.Errorf("statsData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err) + return + } + + for _, stat := range stats.DeviceLevel { + switch stat.MetricsType { + case "XPUM_STATS_POWER": + xpu.Stats.Power = fmt.Sprintf("%.1fW", stat.Value) + case "XPUM_STATS_GPU_FREQUENCY": + xpu.Stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value) + case "XPUM_STATS_GPU_CORE_TEMPERATURE": + xpu.Stats.Temperature = fmt.Sprintf("%.1f°C", stat.Value) + case "XPUM_STATS_MEMORY_USED": + xpu.Stats.MemoryUsed = fmt.Sprintf("%.1fMB", stat.Value) + case "XPUM_STATS_MEMORY_UTILIZATION": + xpu.Stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value) + } + } + + mu.Lock() + res.Xpu = append(res.Xpu, xpu) + mu.Unlock() +} diff --git a/agent/utils/ai_tools/xpu/xpu_info.go b/agent/utils/ai_tools/xpu/xpu_info.go new file mode 100644 index 000000000..9c7d45656 --- /dev/null +++ b/agent/utils/ai_tools/xpu/xpu_info.go @@ -0,0 +1,49 @@ +package xpu + +type XpuInfo struct { + Type string `json:"type"` + DriverVersion string `json:"driverVersion"` + + Xpu []Xpu `json:"xpu"` +} + +type Xpu struct { + Basic Basic `json:"basic"` + Stats Stats `json:"stats"` + Processes []Process `json:"processes"` +} + +type Basic struct { + DeviceID int `json:"deviceID"` + DeviceName string `json:"deviceName"` + VendorName string `json:"vendorName"` + DriverVersion string `json:"driverVersion"` + Memory string `json:"memory"` + FreeMemory string `json:"freeMemory"` + PciBdfAddress string `json:"pciBdfAddress"` +} + +type Stats struct { + Power string `json:"power"` + Frequency string `json:"frequency"` + Temperature string `json:"temperature"` + MemoryUsed string `json:"memoryUsed"` + MemoryUtil string `json:"memoryUtil"` +} + +type Process struct { + PID int `json:"pid"` + Command string `json:"command"` + SHR string `json:"shr"` + Memory string `json:"memory"` +} + +type XPUSimpleInfo struct { + DeviceID int `json:"deviceID"` + DeviceName string `json:"deviceName"` + Memory string `json:"memory"` + Temperature string `json:"temperature"` + MemoryUsed string `json:"memoryUsed"` + Power string `json:"power"` + MemoryUtil string `json:"memoryUtil"` +} diff --git a/agent/utils/common/common.go b/agent/utils/common/common.go index aaee4bf59..41a9302cd 100644 --- a/agent/utils/common/common.go +++ b/agent/utils/common/common.go @@ -16,6 +16,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "golang.org/x/net/idna" ) @@ -357,3 +358,22 @@ func GetLang(c *gin.Context) string { } return lang } + +func HandleIPList(content string) ([]string, error) { + ipList := strings.Split(content, "\n") + var res []string + for _, ip := range ipList { + if ip == "" { + continue + } + if net.ParseIP(ip) != nil { + res = append(res, ip) + continue + } + if _, _, err := net.ParseCIDR(ip); err != nil { + return nil, buserr.New("ErrParseIP") + } + res = append(res, ip) + } + return res, nil +} diff --git a/agent/utils/nginx/components/server.go b/agent/utils/nginx/components/server.go index a5b0446e8..9ed53569a 100644 --- a/agent/utils/nginx/components/server.go +++ b/agent/utils/nginx/components/server.go @@ -260,6 +260,53 @@ func (s *Server) UpdateRoot(path string) { s.UpdateDirective("root", []string{path}) } +func (s *Server) UpdateRootProxyForAi(proxy []string) { + newDir := Directive{ + Name: "location", + Parameters: []string{"/"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = []IDirective{ + &Directive{ + Name: "proxy_buffering", + Parameters: []string{ + "off", + }, + }, + &Directive{ + Name: "proxy_cache", + Parameters: []string{ + "off", + }, + }, + &Directive{ + Name: "proxy_http_version", + Parameters: []string{ + "1.1", + }, + }, + &Directive{ + Name: "proxy_set_header", + Parameters: []string{ + "Connection", "''", + }, + }, + &Directive{ + Name: "chunked_transfer_encoding", + Parameters: []string{ + "off", + }, + }, + } + block.Directives = append(block.Directives, &Directive{ + Name: "proxy_pass", + Parameters: proxy, + }) + newDir.Block = block + s.UpdateDirectiveBySecondKey("location", "/", newDir) +} + func (s *Server) UpdateRootLocation() { newDir := Directive{ Name: "location", @@ -393,3 +440,30 @@ func (s *Server) AddHTTP2HTTPS() { newDir.Block = block s.UpdateDirectiveBySecondKey("if", "($scheme", newDir) } + +func (s *Server) UpdateAllowIPs(ips []string) { + index := -1 + for i, directive := range s.Directives { + if directive.GetName() == "location" && directive.GetParameters()[0] == "/" { + index = i + break + } + } + ipDirectives := make([]IDirective, 0) + for _, ip := range ips { + ipDirectives = append(ipDirectives, &Directive{ + Name: "allow", + Parameters: []string{ip}, + }) + } + ipDirectives = append(ipDirectives, &Directive{ + Name: "deny", + Parameters: []string{"all"}, + }) + if index != -1 { + newDirectives := append(ipDirectives, s.Directives[index:]...) + s.Directives = append(s.Directives[:index], newDirectives...) + } else { + s.Directives = append(s.Directives, ipDirectives...) + } +} diff --git a/core/constant/common.go b/core/constant/common.go index bcf6a1132..bcd05de1d 100644 --- a/core/constant/common.go +++ b/core/constant/common.go @@ -47,6 +47,10 @@ var WebUrlMap = map[string]struct{}{ "/apps/upgrade": {}, "/apps/setting": {}, + "/ai": {}, + "/ai/model": {}, + "/ai/gpu": {}, + "/containers": {}, "/containers/container": {}, "/containers/image": {}, diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts new file mode 100644 index 000000000..a120bea66 --- /dev/null +++ b/frontend/src/api/interface/ai.ts @@ -0,0 +1,111 @@ +import { ReqPage } from '.'; + +export namespace AI { + export interface OllamaModelInfo { + id: number; + name: string; + size: string; + from: string; + logFileExist: boolean; + status: string; + message: string; + createdAt: Date; + } + export interface OllamaModelDropInfo { + id: number; + name: string; + } + export interface OllamaModelSearch extends ReqPage { + info: string; + } + + export interface Info { + cudaVersion: string; + driverVersion: string; + type: string; + gpu: GPU[]; + } + export interface GPU { + index: number; + productName: string; + persistenceMode: string; + busID: string; + displayActive: string; + ecc: string; + fanSpeed: string; + + temperature: string; + performanceState: string; + powerDraw: string; + maxPowerLimit: string; + memUsed: string; + memTotal: string; + gpuUtil: string; + computeMode: string; + migMode: string; + processes: Process[]; + } + export interface Process { + pid: string; + type: string; + processName: string; + usedMemory: string; + } + + export interface XpuInfo { + type: string; + driverVersion: string; + xpu: Xpu[]; + } + + interface Xpu { + basic: Basic; + stats: Stats; + processes: XpuProcess[]; + } + + interface Basic { + deviceID: number; + deviceName: string; + vendorName: string; + driverVersion: string; + memory: string; + freeMemory: string; + pciBdfAddress: string; + } + + interface Stats { + power: string; + frequency: string; + temperature: string; + memoryUsed: string; + memoryUtil: string; + } + + interface XpuProcess { + pid: number; + command: string; + shr: string; + memory: string; + } + + export interface BindDomain { + domain: string; + sslID: number; + ipList: string; + appInstallID: number; + websiteID?: number; + } + + export interface BindDomainReq { + appInstallID: number; + } + + export interface BindDomainRes { + domain: string; + sslID: number; + allowIPs: string[]; + websiteID?: number; + connUrl: string; + } +} diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts new file mode 100644 index 000000000..464f553cd --- /dev/null +++ b/frontend/src/api/modules/ai.ts @@ -0,0 +1,41 @@ +import { AI } from '@/api/interface/ai'; +import http from '@/api'; +import { ResPage } from '../interface'; + +export const createOllamaModel = (name: string) => { + return http.post(`/ai/ollama/model`, { name: name }); +}; +export const recreateOllamaModel = (name: string) => { + return http.post(`/ai/ollama/model/recreate`, { name: name }); +}; +export const deleteOllamaModel = (ids: Array, force: boolean) => { + return http.post(`/ai/ollama/model/del`, { ids: ids, forceDelete: force }); +}; +export const searchOllamaModel = (params: AI.OllamaModelSearch) => { + return http.post>(`/ai/ollama/model/search`, params); +}; +export const loadOllamaModel = (name: string) => { + return http.post(`/ai/ollama/model/load`, { name: name }); +}; +export const syncOllamaModel = () => { + return http.post>(`/ai/ollama/model/sync`); +}; +export const closeOllamaModel = (name: string) => { + return http.post(`/ai/ollama/close`, { name: name }); +}; + +export const loadGPUInfo = () => { + return http.get(`/ai/gpu/load`); +}; + +export const bindDomain = (req: AI.BindDomain) => { + return http.post(`/ai/domain/bind`, req); +}; + +export const getBindDomain = (req: AI.BindDomainReq) => { + return http.post(`/ai/domain/get`, req); +}; + +export const updateBindDomain = (req: AI.BindDomain) => { + return http.post(`/ai/domain/update`, req); +}; diff --git a/frontend/src/assets/iconfont/iconfont.css b/frontend/src/assets/iconfont/iconfont.css index 9f71214b1..43fa56b79 100644 --- a/frontend/src/assets/iconfont/iconfont.css +++ b/frontend/src/assets/iconfont/iconfont.css @@ -1,9 +1,9 @@ @font-face { font-family: "iconfont"; /* Project id 4776196 */ - src: url('iconfont.woff2?t=1740384606757') format('woff2'), - url('iconfont.woff?t=1740384606757') format('woff'), - url('iconfont.ttf?t=1740384606757') format('truetype'), - url('iconfont.svg?t=1740384606757#iconfont') format('svg'); + src: url('iconfont.woff2?t=1740392092454') format('woff2'), + url('iconfont.woff?t=1740392092454') format('woff'), + url('iconfont.ttf?t=1740392092454') format('truetype'), + url('iconfont.svg?t=1740392092454#iconfont') format('svg'); } .iconfont { @@ -14,6 +14,10 @@ -moz-osx-font-smoothing: grayscale; } +.p-jiqiren2:before { + content: "\e61b"; +} + .p-terminal2:before { content: "\e82f"; } diff --git a/frontend/src/assets/iconfont/iconfont.js b/frontend/src/assets/iconfont/iconfont.js index 92eec1736..6fdd959b6 100644 --- a/frontend/src/assets/iconfont/iconfont.js +++ b/frontend/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4776196='',(h=>{var l=(a=(a=document.getElementsByTagName("script"))[a.length-1]).getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var c,t,p,z,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(l&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}c=function(){var l,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_4776196,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(p=c,z=h.document,v=!1,m(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,d())})}function d(){v||(v=!0,p())}function m(){try{z.documentElement.doScroll("left")}catch(l){return void setTimeout(m,50)}d()}})(window); \ No newline at end of file +window._iconfont_svg_string_4776196='',(h=>{var l=(a=(a=document.getElementsByTagName("script"))[a.length-1]).getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var c,t,p,z,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(l&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}c=function(){var l,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_4776196,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(p=c,z=h.document,v=!1,m(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,d())})}function d(){v||(v=!0,p())}function m(){try{z.documentElement.doScroll("left")}catch(l){return void setTimeout(m,50)}d()}})(window); \ No newline at end of file diff --git a/frontend/src/assets/iconfont/iconfont.json b/frontend/src/assets/iconfont/iconfont.json index c9418f627..3d8a75f37 100644 --- a/frontend/src/assets/iconfont/iconfont.json +++ b/frontend/src/assets/iconfont/iconfont.json @@ -5,6 +5,13 @@ "css_prefix_text": "p-", "description": "", "glyphs": [ + { + "icon_id": "10505865", + "name": "机器人", + "font_class": "jiqiren2", + "unicode": "e61b", + "unicode_decimal": 58907 + }, { "icon_id": "5127551", "name": "terminal", diff --git a/frontend/src/assets/iconfont/iconfont.svg b/frontend/src/assets/iconfont/iconfont.svg index 57ce7ab90..fb9331c2d 100644 --- a/frontend/src/assets/iconfont/iconfont.svg +++ b/frontend/src/assets/iconfont/iconfont.svg @@ -14,6 +14,8 @@ /> + + diff --git a/frontend/src/assets/iconfont/iconfont.ttf b/frontend/src/assets/iconfont/iconfont.ttf index 4842a94fdd91db07bdb7bff5c048def5ff0fbee6..3d2d8059806c5d2b0c0b0ccfb4368fdd5c584cd6 100644 GIT binary patch delta 2165 zcma)+Yiv|S7>3_-cDrrqZnxX+w%wMNUSM0hRhHg>w#dbDQxp`aAl`};3Mo)7k)p(1 zjbeg^AXQ8VAq0s*F^H59V;XEAAxh#UMhHKQDVKa_DK+}T+aJ5W!v{wE<(WJ)=ggTi zXXg9fcZT-bKfG=4iN&v+49o&t%YcmPHQn1+zxeqZ9|PVgz_yXD&W=?z!*x>u43VZT zcBG5zTiy?l@m=e;_ICSH*8ppTba!ujuA}Px`ImvL1mAzUzN5Ft>UVv~cZGZ|+t9JT zGv~e56TqwE0eel)#x1_BjUObp06X90BEETRpmO>OSR6~fvQLDc@wfhlEH^uVYghgl zd#6qPnd)`>UHk|cuuU&7^WBQRQtNv=mFoY++Gb50xYY;yG+ejtYT1tAj|SkjcX4zN zW*`kQafwGVq_}^dYq1sI4^s}{;J?Zt9QjWGQVAYp~cgA^%DBcxa{ zl9UpKsfCm&OfsZGVY(q^;e^qLL=;K@Ql-EZ=S_{m1VLg76#}^pwE~%|P&JU{3iSi& zRH!7#YK5ADJg-n)kZlTe2I*C(ILHnXXGQTrUQsAR$m%utFz5ZYuNy9HzlQcfjHI z2Kod}n?lFHnWNA1Q+AG6pg)j2Xyk$Qtz4c)Pt9e6_y)KFKc3Zq7dBU+KS)vpVOyKz3kJ z;9y`lw=MT*-h#Z#!C3HMFd3={tqfhwFUr4Eu%}?C@I<&Ye7tCL(XYjyjh$CgQF6F+ zRq3U1IpcPh1uF_GcSZI^;saIpR~@e2QGGtTBl=@)B!<|$*w>TVCmosGJo(7f7n+}K zKHJjO@;f}KRBG65xn}YlsHQzzF~gY4Di4$gCNx%7M5b8{O)XYkeu)+IM6FPHU4AGK z^i-BtL>dDPP4(&ZcV6kxwE9rxg!)jN)!i@b=;)wbGddW`^m;QBrV3f1+;BMeuY{?O zMNOBftcFCw0!jSSWrs$i*2VUv-pqD0iq%qncQFtS2kdBW_@5X<~dcQEGrC2Prt#qihz<^v7L9qe?qBaICN~wlRRRYD3OuT^) z#vmop5JHF=(x@TWHas+#m`I55pf5aNLQEC1&J=1?;)NI@{<}LS>XT>ZJ7=HUzO42C z>(Zg{*un6DWP0x9iJK5v4|$0#Tle(*+`9S*L>pmuw0Cp&vrXf_e+G<+j$S>;waC}% zpB6KFx9#ljjo!Kfp$XBub;mQ^9WABTp`cIiPi^b&?+Xn@&gos1?(4R9Z`)k-!NQa9 zT04Zd^zC>lwsY{@fkAjflSa?x_y}aKZ4La4OU~I{AX|2$9KNi>~>aZIrC=oBE}X zC1jJdh~-$GC58`2)`imV;o4!2{G%P^{r|M%ocvcC!nYCrPaDXq>8B|sAdEAQbH+Zlq--eRM18xtC$gD4l8(+mDF+{%jo1` z9-)|aO`Xz5Ylh=Fu~7PHZNVK+g6ZOLu{EW_q0M1Rk3-wUA|7oR%l2sNSdK?Q!16o_ z2j<2S3Jm7P5(*LK#u8~o3v&Yq#SM#lSgLEGN1?=uJPImS>`{2J5|09nm3kC%tjwd} zW91&D0juyRAy}p6*9qkYlfWIy5;oPNd|?TXGKbZ8ltau7Ae2qaEl?=0SiMJK#u_}z zH8#uR#BNOkwUTWJ<=+6*(23rDk~1@7c-BP zj0GNPnzUtfBP4HZ+#{i5S3QzEcFiO46G)T}*?>U*IAjQdWgc0BKnr)sBm~;ELv|t9 z=#g;UgSH#}k6Y3f`v2P^A K@b>*bg#Q9Axw{Ym diff --git a/frontend/src/assets/iconfont/iconfont.woff b/frontend/src/assets/iconfont/iconfont.woff index 5bd37d10c5d7534f85d7d060f41c303dd84e4e81..139f90339072dd1172130b12648901127b463a5a 100644 GIT binary patch delta 20985 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 diff --git a/frontend/src/assets/iconfont/iconfont.woff2 b/frontend/src/assets/iconfont/iconfont.woff2 index ef57a3691dfa1e0b71cebbf0224a778b6bf25992..85aafba97956a451e29e7f515ef62eb4a7f3b71d 100644 GIT binary patch literal 18468 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�wc2Fyy{YA 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 {{ $t('commons.operate.restart') }} - + { - console.log(params); upgradeInfo.value = params.upgradeInfo; upgradeVersion.value = params.upgradeVersion; drawerVisible.value = true; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 820b3dc0a..17255a35c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -9,6 +9,7 @@ const message = { lingxia: 'Lingxia', colon: ': ', button: { + run: 'Run', prev: 'Previous', next: 'Next', create: 'Create ', @@ -333,6 +334,7 @@ const message = { firewall: 'Firewall', ssl: 'Certificate', database: 'Database', + aiTools: 'AI', container: 'Container', cronjob: 'Cronjob', host: 'Host', @@ -589,6 +591,67 @@ const message = { remoteConnHelper2: 'Use this address for non-container or external connections', localIP: 'Local IP', }, + aiTools: { + model: { + model: 'Model', + create: 'Add Model', + create_helper: 'Pull "{0}"', + ollama_doc: 'You can visit the Ollama official website to search and find more models.', + container_conn_helper: 'Use this address for inter-container access or connection', + ollama_sync: 'Syncing Ollama model found the following models do not exist, do you want to delete them?', + from_remote: 'This model was not downloaded via 1Panel, no related pull logs.', + no_logs: 'The pull logs for this model have been deleted and cannot be viewed.', + }, + proxy: { + proxy: 'AI Proxy Enhancement', + proxyHelper1: 'Bind domain and enable HTTPS for enhanced transmission security', + proxyHelper2: 'Limit IP access to prevent exposure on the public internet', + proxyHelper3: 'Enable streaming', + proxyHelper4: 'Once created, you can view and manage it in the website list', + proxyHelper5: + 'After enabling, you can disable external access to the port in the App Store - Installed - Ollama - Parameters to improve security.', + proxyHelper6: 'To disable proxy configuration, you can delete it from the website list.', + whiteListHelper: 'Restrict access to only IPs in the whitelist', + }, + gpu: { + gpu: 'GPU Monitor', + base: 'Basic Information', + gpuHelper: 'NVIDIA-SMI or XPU-SMI command not detected on the current system. Please check and try again!', + driverVersion: 'Driver Version', + cudaVersion: 'CUDA Version', + process: 'Process Information', + type: 'Type', + typeG: 'Graphics', + typeC: 'Compute', + typeCG: 'Compute + Graphics', + processName: 'Process Name', + processMemoryUsage: 'Memory Usage', + temperatureHelper: 'High GPU temperature can cause GPU frequency throttling', + performanceStateHelper: 'From P0 (maximum performance) to P12 (minimum performance)', + busID: 'Bus ID', + persistenceMode: 'Persistence Mode', + enabled: 'Enabled', + disabled: 'Disabled', + persistenceModeHelper: + 'Persistence mode allows quicker task responses but increases standby power consumption.', + displayActive: 'Graphics Card Initialized', + displayActiveT: 'Yes', + displayActiveF: 'No', + ecc: 'Error Correction and Check Technology', + computeMode: 'Compute Mode', + default: 'Default', + exclusiveProcess: 'Exclusive Process', + exclusiveThread: 'Exclusive Thread', + prohibited: 'Prohibited', + defaultHelper: 'Default: Processes can execute concurrently', + exclusiveProcessHelper: + 'Exclusive Process: Only one CUDA context can use the GPU, but can be shared by multiple threads', + exclusiveThreadHelper: 'Exclusive Thread: Only one thread in a CUDA context can use the GPU', + prohibitedHelper: 'Prohibited: Processes are not allowed to execute simultaneously', + migModeHelper: 'Used to create MIG instances for physical isolation of the GPU at the user level.', + migModeNA: 'Not Supported', + }, + }, container: { create: 'Create', createByCommand: 'Create by command', @@ -1807,7 +1870,6 @@ const message = { waf: 'Upgrading to the professional version can provide features such as interception map, logs, block records, geographical location blocking, custom rules, custom interception pages, etc.', tamper: 'Upgrading to the professional version can protect websites from unauthorized modifications or tampering.', tamperHelper: 'Operation failed, the file or folder has tamper protection enabled. Please check and try again!', - gpu: 'Upgrading to the professional version can help users visually monitor important parameters of GPU such as workload, temperature, memory usage in real time.', setting: 'Upgrading to the professional version allows customization of panel logo, welcome message, and other information.', monitor: @@ -2995,44 +3057,6 @@ const message = { disableHelper: 'The anti-tampering function of website {0} is about to be disabled. Do you want to continue?', }, - gpu: { - gpu: 'GPU Monitor', - base: 'Basic Information', - gpuHelper: 'NVIDIA-SMI or XPU-SMI command not detected on the current system. Please check and try again!', - driverVersion: 'Driver Version', - cudaVersion: 'CUDA Version', - process: 'Process Information', - type: 'Type', - typeG: 'Graphics', - typeC: 'Compute', - typeCG: 'Compute + Graphics', - processName: 'Process Name', - processMemoryUsage: 'Memory Usage', - temperatureHelper: 'High GPU temperature can cause GPU frequency throttling', - performanceStateHelper: 'From P0 (maximum performance) to P12 (minimum performance)', - busID: 'Bus ID', - persistenceMode: 'Persistence Mode', - enabled: 'Enabled', - disabled: 'Disabled', - persistenceModeHelper: - 'Persistence mode allows quicker task responses but increases standby power consumption.', - displayActive: 'Graphics Card Initialized', - displayActiveT: 'Yes', - displayActiveF: 'No', - ecc: 'Error Correction and Check Technology', - computeMode: 'Compute Mode', - default: 'Default', - exclusiveProcess: 'Exclusive Process', - exclusiveThread: 'Exclusive Thread', - prohibited: 'Prohibited', - defaultHelper: 'Default: Processes can execute concurrently', - exclusiveProcessHelper: - 'Exclusive Process: Only one CUDA context can use the GPU, but can be shared by multiple threads', - exclusiveThreadHelper: 'Exclusive Thread: Only one thread in a CUDA context can use the GPU', - prohibitedHelper: 'Prohibited: Processes are not allowed to execute simultaneously', - migModeHelper: 'Used to create MIG instances for physical isolation of the GPU at the user level.', - migModeNA: 'Not Supported', - }, setting: { setting: 'Panel Settings', title: 'Panel Description', @@ -3076,11 +3100,6 @@ const message = { tamperContent4: 'Record file access and operation logs for subsequent auditing and analysis by administrators, as well as to identify potential security threats.', - gpuTitle1: 'Overview Monitoring', - gpuContent1: 'Display the current GPU usage on the overview page.', - gpuTitle2: 'GPU Details', - gpuContent2: 'Show GPU parameters in finer detail.', - settingTitle1: 'Custom Welcome Message', settingContent1: 'Set a custom welcome message on the 1Panel login page.', settingTitle2: 'Custom Logo', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 1d224a3fe..0b06ccfde 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -9,6 +9,7 @@ const message = { fit2cloud: 'FIT2CLOUD', lingxia: 'Lingxia', button: { + run: '実行', create: '作成する', add: '追加', save: '保存', @@ -327,6 +328,7 @@ const message = { firewall: 'ファイアウォール', ssl: '証明書|証明書', database: 'データベース|データベース', + aiTools: 'AI', container: 'コンテナ|コンテナ', cronjob: 'クロンジョブ|クロンの仕事', host: 'ホスト|ホスト', @@ -581,6 +583,67 @@ const message = { 'この接続アドレスは、非コンテナまたは外部アプリケーションで実行されているアプリケーションで使用できます。', localIP: 'ローカルIP', }, + aiTools: { + model: { + model: 'モデル', + create: 'モデルを追加', + create_helper: 'を取得 "{0}"', + ollama_doc: 'Ollama の公式ウェブサイトを訪れて、さらに多くのモデルを検索して見つけることができます。', + container_conn_helper: 'コンテナ間のアクセスまたは接続にこのアドレスを使用', + ollama_sync: 'Ollamaモデルの同期中に、以下のモデルが存在しないことが判明しました。削除しますか?', + from_remote: 'このモデルは1Panelを介してダウンロードされておらず、関連するプルログはありません。', + no_logs: 'このモデルのプルログは削除されており、関連するログを表示できません。', + }, + proxy: { + proxy: 'AI プロキシ強化', + proxyHelper1: 'ドメインをバインドし、HTTPS を有効にして通信のセキュリティを強化', + proxyHelper2: 'IP アクセスを制限し、パブリックインターネットでの露出を防止', + proxyHelper3: 'ストリーミングを有効にする', + proxyHelper4: '作成後、ウェブサイトリストで確認および管理できます', + proxyHelper5: + '有効にすると、アプリストア - インストール済み - Ollama - パラメータでポートの外部アクセスを無効にし、セキュリティを向上させることができます。', + proxyHelper6: 'プロキシ設定を無効にするには、ウェブサイトリストから削除できます。', + whiteListHelper: 'ホワイトリスト内のIPのみアクセスを許可する', + }, + gpu: { + gpu: 'GPUモニター', + base: '基本情報', + gpuHelper: + '現在のシステムでNVIDIA-SMIまたはXPU-SMIコマンドが検出されませんでした。確認して再試行してください!', + driverVersion: 'ドライバーバージョン', + cudaVersion: 'CUDAバージョン', + process: 'プロセス情報', + type: 'タイプ', + typeG: 'グラフィックス', + typeC: 'コンピュート', + typeCG: 'コンピュート + グラフィックス', + processName: 'プロセス名', + processMemoryUsage: 'メモリ使用量', + temperatureHelper: '高いGPU温度はGPUの周波数制限を引き起こす可能性があります', + performanceStateHelper: 'P0(最大性能)からP12(最小性能)まで', + busID: 'バスID', + persistenceMode: '永続モード', + enabled: '有効', + disabled: '無効', + persistenceModeHelper: '永続モードはタスクの応答速度を速くしますが、待機時の消費電力が増加します。', + displayActive: 'グラフィックカード初期化済み', + displayActiveT: 'はい', + displayActiveF: 'いいえ', + ecc: 'エラー訂正およびチェック技術', + computeMode: 'コンピュートモード', + default: 'デフォルト', + exclusiveProcess: '専用プロセス', + exclusiveThread: '専用スレッド', + prohibited: '禁止', + defaultHelper: 'デフォルト:プロセスは並行して実行できます', + exclusiveProcessHelper: + '専用プロセス:1つのCUDAコンテキストのみがGPUを使用できますが、複数のスレッドで共有できます', + exclusiveThreadHelper: '専用スレッド:CUDAコンテキスト内の1つのスレッドのみがGPUを使用できます', + prohibitedHelper: '禁止:プロセスは同時に実行できません', + migModeHelper: 'ユーザーレベルでGPUの物理的分離を行うためのMIGインスタンスを作成するために使用されます。', + migModeNA: 'サポートされていません', + }, + }, container: { create: 'コンテナを作成します', edit: 'コンテナを編集します', @@ -1670,7 +1733,6 @@ const message = { introduce: '機能の紹介', waf: 'プロフェッショナルバージョンにアップグレードすると、インターセプトマップ、ログ、ブロックレコード、地理的位置ブロッキング、カスタムルール、カスタムインターセプトページなどの機能を提供できます。', tamper: 'プロのバージョンにアップグレードすると、不正な変更や改ざんからWebサイトを保護できます。', - gpu: 'プロのバージョンにアップグレードすることで、ユーザーはワークロード、温度、メモリ使用量などのGPUの重要なパラメーターをリアルタイムで視覚的に監視するのに役立ちます。', setting: 'プロのバージョンにアップグレードすることで、パネルロゴ、ウェルカムメッセージ、その他の情報のカスタマイズが可能になります。', monitor: @@ -2806,44 +2868,6 @@ const message = { 'ウェブサイト {0} の改ざん防止機能が有効になろうとしています。セキュリティを強化するために続行しますか?', disableHelper: 'ウェブサイト {0} の改ざん防止機能が無効になろうとしています。続行しますか?', }, - gpu: { - gpu: 'GPUモニター', - base: '基本情報', - gpuHelper: - '現在のシステムでNVIDIA-SMIまたはXPU-SMIコマンドが検出されませんでした。確認して再試行してください!', - driverVersion: 'ドライバーバージョン', - cudaVersion: 'CUDAバージョン', - process: 'プロセス情報', - type: 'タイプ', - typeG: 'グラフィックス', - typeC: 'コンピュート', - typeCG: 'コンピュート + グラフィックス', - processName: 'プロセス名', - processMemoryUsage: 'メモリ使用量', - temperatureHelper: '高いGPU温度はGPUの周波数制限を引き起こす可能性があります', - performanceStateHelper: 'P0(最大性能)からP12(最小性能)まで', - busID: 'バスID', - persistenceMode: '永続モード', - enabled: '有効', - disabled: '無効', - persistenceModeHelper: '永続モードはタスクの応答速度を速くしますが、待機時の消費電力が増加します。', - displayActive: 'グラフィックカード初期化済み', - displayActiveT: 'はい', - displayActiveF: 'いいえ', - ecc: 'エラー訂正およびチェック技術', - computeMode: 'コンピュートモード', - default: 'デフォルト', - exclusiveProcess: '専用プロセス', - exclusiveThread: '専用スレッド', - prohibited: '禁止', - defaultHelper: 'デフォルト:プロセスは並行して実行できます', - exclusiveProcessHelper: - '専用プロセス:1つのCUDAコンテキストのみがGPUを使用できますが、複数のスレッドで共有できます', - exclusiveThreadHelper: '専用スレッド:CUDAコンテキスト内の1つのスレッドのみがGPUを使用できます', - prohibitedHelper: '禁止:プロセスは同時に実行できません', - migModeHelper: 'ユーザーレベルでGPUの物理的分離を行うためのMIGインスタンスを作成するために使用されます。', - migModeNA: 'サポートされていません', - }, setting: { setting: 'パネル設定', title: 'パネルの説明', @@ -2887,10 +2911,6 @@ const message = { tamperTitle4: 'ログ記録と分析', tamperContent4: 'ファイルのアクセスおよび操作ログを記録し、後の監査および分析に使用、また潜在的なセキュリティ脅威を特定。', - gpuTitle1: '概要ページモニタリング', - gpuContent1: '概要ページでGPUの現在の使用状況を表示します。', - gpuTitle2: 'GPU詳細情報', - gpuContent2: 'GPUの各パラメータをより詳細に表示します。', settingTitle1: 'カスタムウェルカムメッセージ', settingContent1: '1Panelのログインページにカスタムウェルカムメッセージを設定。', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index befbb5fae..8996281ae 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -9,6 +9,7 @@ const message = { fit2cloud: 'FIT2CLOUD', lingxia: 'Lingxia', button: { + run: '실행', create: '생성', add: '추가', save: '저장', @@ -328,6 +329,7 @@ const message = { firewall: '방화벽', ssl: '인증서 | 인증서들', database: '데이터베이스 | 데이터베이스들', + aiTools: 'AI', container: '컨테이너 | 컨테이너들', cronjob: '크론 작업 | 크론 작업들', host: '호스트 | 호스트들', @@ -577,6 +579,66 @@ const message = { '이 연결 주소는 컨테이너 외부 또는 외부 애플리케이션에서 실행 중인 애플리케이션에서 사용할 수 있습니다.', localIP: '로컬 IP', }, + aiTools: { + model: { + model: '모델', + create: '모델 추가', + create_helper: '가져오기 "{0}"', + ollama_doc: 'Ollama 공식 웹사이트를 방문하여 더 많은 모델을 검색하고 찾을 수 있습니다.', + container_conn_helper: '컨테이너 간 접근 또는 연결에 이 주소를 사용', + ollama_sync: 'Ollama 모델 동기화 중 다음 모델이 존재하지 않음을 발견했습니다. 삭제하시겠습니까?', + from_remote: '이 모델은 1Panel을 통해 다운로드되지 않았으며 관련 풀 로그가 없습니다.', + no_logs: '이 모델의 풀 로그가 삭제되어 관련 로그를 볼 수 없습니다.', + }, + proxy: { + proxy: 'AI 프록시 강화', + proxyHelper1: '도메인을 바인딩하고 HTTPS를 활성화하여 전송 보안을 강화', + proxyHelper2: 'IP 접근을 제한하여 공용 인터넷에서의 노출을 방지', + proxyHelper3: '스트리밍을 활성화', + proxyHelper4: '생성 후, 웹사이트 목록에서 이를 보고 관리할 수 있습니다', + proxyHelper5: + '활성화한 후, 앱 스토어 - 설치됨 - Ollama - 매개변수에서 포트 외부 접근을 비활성화하여 보안을 강화할 수 있습니다.', + proxyHelper6: '프록시 구성을 비활성화하려면 웹사이트 목록에서 삭제할 수 있습니다.', + whiteListHelper: '화이트리스트에 있는 IP만 접근 허용', + }, + gpu: { + gpu: 'GPU 모니터', + base: '기본 정보', + gpuHelper: '현재 시스템에서 NVIDIA-SMI 또는 XPU-SMI 명령이 감지되지 않았습니다. 확인 후 다시 시도하세요!', + driverVersion: '드라이버 버전', + cudaVersion: 'CUDA 버전', + process: '프로세스 정보', + type: '유형', + typeG: '그래픽', + typeC: '연산', + typeCG: '연산 + 그래픽', + processName: '프로세스 이름', + processMemoryUsage: '메모리 사용량', + temperatureHelper: 'GPU 온도가 높으면 GPU 주파수 제한이 발생할 수 있습니다.', + performanceStateHelper: 'P0(최대 성능)부터 P12(최소 성능)까지', + busID: '버스 ID', + persistenceMode: '지속 모드', + enabled: '활성화됨', + disabled: '비활성화됨', + persistenceModeHelper: '지속 모드는 작업 응답 속도를 빠르게 하지만 대기 전력 소비를 증가시킵니다.', + displayActive: '그래픽 카드 초기화됨', + displayActiveT: '예', + displayActiveF: '아니요', + ecc: '오류 감지 및 수정 기술', + computeMode: '연산 모드', + default: '기본값', + exclusiveProcess: '단독 프로세스', + exclusiveThread: '단독 스레드', + prohibited: '금지됨', + defaultHelper: '기본값: 프로세스가 동시에 실행될 수 있음', + exclusiveProcessHelper: + '단독 프로세스: 하나의 CUDA 컨텍스트만 GPU 를 사용할 수 있지만, 여러 스레드에서 공유 가능', + exclusiveThreadHelper: '단독 스레드: CUDA 컨텍스트의 하나의 스레드만 GPU 를 사용할 수 있음', + prohibitedHelper: '금지됨: 프로세스가 동시에 실행되는 것이 허용되지 않음', + migModeHelper: '사용자 수준에서 GPU 를 물리적으로 분리하는 MIG 인스턴스를 생성하는 데 사용됩니다.', + migModeNA: '지원되지 않음', + }, + }, container: { create: '컨테이너 만들기', edit: '컨테이너 편집', @@ -1643,7 +1705,6 @@ const message = { introduce: '기능 소개', waf: '전문 버전으로 업그레이드하면 차단 맵, 로그, 차단 기록, 지리적 위치 차단, 사용자 정의 규칙, 사용자 정의 차단 페이지 등의 기능을 제공받을 수 있습니다.', tamper: '전문 버전으로 업그레이드하면 웹사이트를 무단 수정이나 변조로부터 보호할 수 있습니다.', - gpu: '전문 버전으로 업그레이드하면 GPU 의 작업 부하, 온도, 메모리 사용량 등 중요한 매개변수를 실시간으로 시각적으로 모니터링할 수 있습니다.', setting: '전문 버전으로 업그레이드하면 패널 로고, 환영 메시지 등 정보를 사용자 정의할 수 있습니다.', monitor: '전문 버전으로 업그레이드하면 웹사이트의 실시간 상태, 방문자 트렌드, 방문자 출처, 요청 로그 등 정보를 확인할 수 있습니다.', @@ -2763,43 +2824,6 @@ const message = { '웹사이트 {0}의 방지 조작 기능을 활성화하여 웹사이트 보안을 강화하려고 합니다. 계속하시겠습니까?', disableHelper: '웹사이트 {0}의 방지 조작 기능을 비활성화하려고 합니다. 계속하시겠습니까?', }, - gpu: { - gpu: 'GPU 모니터', - base: '기본 정보', - gpuHelper: '현재 시스템에서 NVIDIA-SMI 또는 XPU-SMI 명령이 감지되지 않았습니다. 확인 후 다시 시도하세요!', - driverVersion: '드라이버 버전', - cudaVersion: 'CUDA 버전', - process: '프로세스 정보', - type: '유형', - typeG: '그래픽', - typeC: '연산', - typeCG: '연산 + 그래픽', - processName: '프로세스 이름', - processMemoryUsage: '메모리 사용량', - temperatureHelper: 'GPU 온도가 높으면 GPU 주파수 제한이 발생할 수 있습니다.', - performanceStateHelper: 'P0(최대 성능)부터 P12(최소 성능)까지', - busID: '버스 ID', - persistenceMode: '지속 모드', - enabled: '활성화됨', - disabled: '비활성화됨', - persistenceModeHelper: '지속 모드는 작업 응답 속도를 빠르게 하지만 대기 전력 소비를 증가시킵니다.', - displayActive: '그래픽 카드 초기화됨', - displayActiveT: '예', - displayActiveF: '아니요', - ecc: '오류 감지 및 수정 기술', - computeMode: '연산 모드', - default: '기본값', - exclusiveProcess: '단독 프로세스', - exclusiveThread: '단독 스레드', - prohibited: '금지됨', - defaultHelper: '기본값: 프로세스가 동시에 실행될 수 있음', - exclusiveProcessHelper: - '단독 프로세스: 하나의 CUDA 컨텍스트만 GPU 를 사용할 수 있지만, 여러 스레드에서 공유 가능', - exclusiveThreadHelper: '단독 스레드: CUDA 컨텍스트의 하나의 스레드만 GPU 를 사용할 수 있음', - prohibitedHelper: '금지됨: 프로세스가 동시에 실행되는 것이 허용되지 않음', - migModeHelper: '사용자 수준에서 GPU 를 물리적으로 분리하는 MIG 인스턴스를 생성하는 데 사용됩니다.', - migModeNA: '지원되지 않음', - }, setting: { setting: '패널 설정', title: '패널 설명', @@ -2840,11 +2864,6 @@ const message = { tamperContent4: '파일 접근 및 작업 로그를 기록하여 관리자가 감사 및 분석을 수행할 수 있도록 하고, 잠재적 보안 위협을 식별합니다.', - gpuTitle1: '개요 모니터링', - gpuContent1: '개요 페이지에서 현재 GPU 사용량을 표시합니다.', - gpuTitle2: 'GPU 세부 정보', - gpuContent2: 'GPU 매개변수를 더 세부적으로 표시합니다.', - settingTitle1: '사용자 정의 환영 메시지', settingContent1: '1Panel 로그인 페이지에 사용자 정의 환영 메시지를 설정합니다.', settingTitle2: '사용자 정의 로고', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index fdb83ef2d..bc748eab3 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -9,6 +9,7 @@ const message = { fit2cloud: 'FIT2CLOUD', lingxia: 'Lingxia', button: { + run: 'Jalankan', create: 'Cipta', add: 'Tambah', save: 'Simpan', @@ -334,6 +335,7 @@ const message = { firewall: 'Firewall', ssl: 'Certificate | Certificates', database: 'Database | Databases', + aiTools: 'AI', container: 'Container | Containers', cronjob: 'Cron Job | Cron Jobs', host: 'Host | Hosts', @@ -592,6 +594,68 @@ const message = { 'Alamat sambungan ini boleh digunakan oleh aplikasi yang berjalan di luar kontena atau aplikasi luaran.', localIP: 'IP Tempatan', }, + aiTools: { + model: { + model: 'Model', + create: 'Tambah Model', + create_helper: 'Tarik "{0}"', + ollama_doc: 'Anda boleh melawat laman web rasmi Ollama untuk mencari dan menemui lebih banyak model.', + container_conn_helper: 'Gunakan alamat ini untuk akses atau sambungan antara kontena', + ollama_sync: + 'Sincronizando o modelo Ollama, encontrou que os seguintes modelos não existem, deseja excluí-los?', + from_remote: 'Este modelo não foi baixado via 1Panel, sem logs de pull relacionados.', + no_logs: 'Os logs de pull deste modelo foram excluídos e não podem ser visualizados.', + }, + proxy: { + proxy: 'Peningkatan Proksi AI', + proxyHelper1: 'Ikatkan domain dan aktifkan HTTPS untuk meningkatkan keselamatan penghantaran', + proxyHelper2: 'Hadkan akses IP untuk mengelakkan pendedahan di internet awam', + proxyHelper3: 'Aktifkan penstriman', + proxyHelper4: 'Setelah selesai, anda boleh melihat dan mengurusnya dalam senarai laman web', + proxyHelper5: + 'Selepas diaktifkan, anda boleh melumpuhkan akses luaran ke port dalam App Store - Dipasang - Ollama - Parameter untuk meningkatkan keselamatan.', + proxyHelper6: 'Untuk melumpuhkan konfigurasi proksi, anda boleh memadamnya dari senarai laman web.', + whiteListHelper: 'Hadkan akses kepada hanya IP dalam senarai putih', + }, + gpu: { + gpu: 'Monitor GPU', + base: 'Maklumat Asas', + gpuHelper: 'Perintah NVIDIA-SMI atau XPU-SMI tidak dikesan pada sistem semasa. Sila periksa dan cuba lagi!', + driverVersion: 'Versi Pemacu', + cudaVersion: 'Versi CUDA', + process: 'Maklumat Proses', + type: 'Jenis', + typeG: 'Grafik', + typeC: 'Pengiraan', + typeCG: 'Pengiraan + Grafik', + processName: 'Nama Proses', + processMemoryUsage: 'Penggunaan Memori', + temperatureHelper: 'Suhu GPU yang tinggi boleh menyebabkan pelambatan frekuensi GPU', + performanceStateHelper: 'Dari P0 (prestasi maksimum) hingga P12 (prestasi minimum)', + busID: 'ID Bas', + persistenceMode: 'Mod Ketekalan', + enabled: 'Diaktifkan', + disabled: 'Dilumpuhkan', + persistenceModeHelper: + 'Mod ketekalan membolehkan respons tugas lebih cepat tetapi meningkatkan penggunaan kuasa sedia.', + displayActive: 'Kad Grafik Dimulakan', + displayActiveT: 'Ya', + displayActiveF: 'Tidak', + ecc: 'Teknologi Pemeriksaan dan Pembetulan Ralat', + computeMode: 'Mod Pengiraan', + default: 'Asal', + exclusiveProcess: 'Proses Eksklusif', + exclusiveThread: 'Thread Eksklusif', + prohibited: 'Dilarang', + defaultHelper: 'Asal: Proses boleh dilaksanakan secara serentak', + exclusiveProcessHelper: + 'Proses Eksklusif: Hanya satu konteks CUDA boleh menggunakan GPU, tetapi boleh dikongsi oleh berbilang thread', + exclusiveThreadHelper: 'Thread Eksklusif: Hanya satu thread dalam konteks CUDA boleh menggunakan GPU', + prohibitedHelper: 'Dilarang: Proses tidak dibenarkan dilaksanakan serentak', + migModeHelper: 'Digunakan untuk membuat contoh MIG bagi pengasingan fizikal GPU pada tahap pengguna.', + migModeNA: 'Tidak Disokong', + }, + }, container: { create: 'Cipta kontena', edit: 'Sunting kontena', @@ -1726,7 +1790,6 @@ const message = { introduce: 'Pengenalan Ciri', waf: 'Menaik taraf ke versi profesional boleh menyediakan ciri seperti peta pencegahan, log, rekod blok, sekatan lokasi geografi, peraturan tersuai, halaman pencegahan tersuai, dan sebagainya.', tamper: 'Menaik taraf ke versi profesional boleh melindungi laman web daripada pengubahsuaian atau manipulasi tanpa kebenaran.', - gpu: 'Menaik taraf ke versi profesional boleh membantu pengguna memantau parameter penting GPU secara visual seperti beban kerja, suhu, penggunaan memori secara masa nyata.', setting: 'Menaik taraf ke versi profesional membolehkan penyesuaian logo panel, mesej selamat datang, dan maklumat lain.', monitor: @@ -2871,44 +2934,6 @@ const message = { 'Ciri anti-pemalsuan laman web {0} akan diaktifkan untuk meningkatkan keselamatan laman web. Adakah anda mahu meneruskan?', disableHelper: 'Ciri anti-pemalsuan laman web {0} akan dilumpuhkan. Adakah anda mahu meneruskan?', }, - gpu: { - gpu: 'Monitor GPU', - base: 'Maklumat Asas', - gpuHelper: 'Perintah NVIDIA-SMI atau XPU-SMI tidak dikesan pada sistem semasa. Sila periksa dan cuba lagi!', - driverVersion: 'Versi Pemacu', - cudaVersion: 'Versi CUDA', - process: 'Maklumat Proses', - type: 'Jenis', - typeG: 'Grafik', - typeC: 'Pengiraan', - typeCG: 'Pengiraan + Grafik', - processName: 'Nama Proses', - processMemoryUsage: 'Penggunaan Memori', - temperatureHelper: 'Suhu GPU yang tinggi boleh menyebabkan pelambatan frekuensi GPU', - performanceStateHelper: 'Dari P0 (prestasi maksimum) hingga P12 (prestasi minimum)', - busID: 'ID Bas', - persistenceMode: 'Mod Ketekalan', - enabled: 'Diaktifkan', - disabled: 'Dilumpuhkan', - persistenceModeHelper: - 'Mod ketekalan membolehkan respons tugas lebih cepat tetapi meningkatkan penggunaan kuasa sedia.', - displayActive: 'Kad Grafik Dimulakan', - displayActiveT: 'Ya', - displayActiveF: 'Tidak', - ecc: 'Teknologi Pemeriksaan dan Pembetulan Ralat', - computeMode: 'Mod Pengiraan', - default: 'Asal', - exclusiveProcess: 'Proses Eksklusif', - exclusiveThread: 'Thread Eksklusif', - prohibited: 'Dilarang', - defaultHelper: 'Asal: Proses boleh dilaksanakan secara serentak', - exclusiveProcessHelper: - 'Proses Eksklusif: Hanya satu konteks CUDA boleh menggunakan GPU, tetapi boleh dikongsi oleh berbilang thread', - exclusiveThreadHelper: 'Thread Eksklusif: Hanya satu thread dalam konteks CUDA boleh menggunakan GPU', - prohibitedHelper: 'Dilarang: Proses tidak dibenarkan dilaksanakan serentak', - migModeHelper: 'Digunakan untuk membuat contoh MIG bagi pengasingan fizikal GPU pada tahap pengguna.', - migModeNA: 'Tidak Disokong', - }, setting: { setting: 'Tetapan Panel', title: 'Deskripsi Panel', @@ -2952,11 +2977,6 @@ const message = { tamperContent4: 'Rekod log akses dan operasi fail untuk audit dan analisis selanjutnya oleh pentadbir serta mengenal pasti potensi ancaman keselamatan.', - gpuTitle1: 'Pemantauan Gambaran Keseluruhan', - gpuContent1: 'Papar penggunaan semasa GPU pada halaman gambaran keseluruhan.', - gpuTitle2: 'Butiran GPU', - gpuContent2: 'Tunjukkan parameter GPU dengan lebih terperinci.', - settingTitle1: 'Mesej Selamat Datang Tersuai', settingContent1: 'Tetapkan mesej selamat datang tersuai pada halaman log masuk 1Panel.', settingTitle2: 'Logo Tersuai', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index d1a1c67f6..91822ca82 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -9,6 +9,7 @@ const message = { fit2cloud: 'FIT2CLOUD', lingxia: 'Lingxia', button: { + run: 'Executar', create: 'Criar', add: 'Adicionar', save: 'Salvar', @@ -332,6 +333,7 @@ const message = { firewall: 'Firewall', ssl: 'Certificado | Certificados', database: 'Banco de Dados | Bancos de Dados', + aiTools: 'AI', container: 'Container | Containers', cronjob: 'Tarefa Cron | Tarefas Cron', host: 'Host | Hosts', @@ -589,6 +591,67 @@ const message = { 'Este endereço de conexão pode ser utilizado por aplicações que estão fora do contêiner ou por aplicações externas.', localIP: 'IP local', }, + aiTools: { + model: { + model: 'Modelo', + create: 'Adicionar Modelo', + create_helper: 'Puxar "{0}"', + ollama_doc: 'Você pode visitar o site oficial da Ollama para pesquisar e encontrar mais modelos.', + container_conn_helper: 'Use este endereço para acesso ou conexão entre contêineres', + ollama_sync: + 'Menyelaraskan model Ollama mendapati model berikut tidak wujud, adakah anda ingin memadamnya?', + from_remote: 'Model ini tidak dimuat turun melalui 1Panel, tiada log pengambilan berkaitan.', + no_logs: 'Log pengambilan untuk model ini telah dipadam dan tidak dapat dilihat.', + }, + proxy: { + proxy: 'Melhoria de Proxy AI', + proxyHelper1: 'Vincule o domínio e habilite o HTTPS para aumentar a segurança na transmissão', + proxyHelper2: 'Limite o acesso por IP para evitar exposição na internet pública', + proxyHelper3: 'Habilite a transmissão em fluxo', + proxyHelper4: 'Após a criação, você pode visualizar e gerenciar no lista de sites', + proxyHelper6: 'Para desativar a configuração de proxy, você pode excluí-la da lista de sites.', + whiteListHelper: 'Restringir o acesso apenas aos IPs na lista branca', + }, + gpu: { + gpu: 'Monitor de GPU', + base: 'Informações Básicas', + gpuHelper: + 'Comando NVIDIA-SMI ou XPU-SMI não detectado no sistema atual. Por favor, verifique e tente novamente!', + driverVersion: 'Versão do Driver', + cudaVersion: 'Versão do CUDA', + process: 'Informações do Processo', + type: 'Tipo', + typeG: 'Gráficos', + typeC: 'Cálculo', + typeCG: 'Cálculo + Gráficos', + processName: 'Nome do Processo', + processMemoryUsage: 'Uso de Memória', + temperatureHelper: 'Temperaturas altas da GPU podem causar limitação de frequência da GPU.', + performanceStateHelper: 'De P0 (máximo desempenho) a P12 (mínimo desempenho).', + busID: 'ID do Barramento', + persistenceMode: 'Modo de Persistência', + enabled: 'Ativado', + disabled: 'Desativado', + persistenceModeHelper: + 'O modo de persistência permite respostas mais rápidas às tarefas, mas aumenta o consumo de energia em standby.', + displayActive: 'Placa Gráfica Inicializada', + displayActiveT: 'Sim', + displayActiveF: 'Não', + ecc: 'Tecnologia de Correção e Verificação de Erros', + computeMode: 'Modo de Cálculo', + default: 'Padrão', + exclusiveProcess: 'Processo Exclusivo', + exclusiveThread: 'Thread Exclusivo', + prohibited: 'Proibido', + defaultHelper: 'Padrão: Processos podem ser executados simultaneamente.', + exclusiveProcessHelper: + 'Processo Exclusivo: Apenas um contexto CUDA pode usar a GPU, mas pode ser compartilhado por múltiplas threads.', + exclusiveThreadHelper: 'Thread Exclusivo: Apenas uma thread em um contexto CUDA pode usar a GPU.', + prohibitedHelper: 'Proibido: Não é permitido que processos sejam executados simultaneamente.', + migModeHelper: 'Usado para criar instâncias MIG para isolamento físico da GPU no nível do usuário.', + migModeNA: 'Não Suportado', + }, + }, container: { create: 'Criar contêiner', edit: 'Editar contêiner', @@ -1714,7 +1777,6 @@ const message = { introduce: 'Introdução de recursos', waf: 'O upgrade para a versão profissional pode fornecer recursos como mapa de intercepção, logs, registros de bloqueio, bloqueio por localização geográfica, regras personalizadas, páginas de intercepção personalizadas, etc.', tamper: 'O upgrade para a versão profissional pode proteger sites contra modificações ou adulterações não autorizadas.', - gpu: 'O upgrade para a versão profissional pode ajudar os usuários a monitorar visualmente parâmetros importantes da GPU, como carga de trabalho, temperatura e uso de memória em tempo real.', setting: 'O upgrade para a versão profissional permite a personalização do logo do painel, mensagem de boas-vindas e outras informações.', monitor: @@ -2875,45 +2937,6 @@ const message = { 'A função de anti-alteração do site {0} está prestes a ser ativada para aumentar a segurança do site. Deseja continuar?', disableHelper: 'A função de anti-alteração do site {0} está prestes a ser desativada. Deseja continuar?', }, - gpu: { - gpu: 'Monitor de GPU', - base: 'Informações Básicas', - gpuHelper: - 'Comando NVIDIA-SMI ou XPU-SMI não detectado no sistema atual. Por favor, verifique e tente novamente!', - driverVersion: 'Versão do Driver', - cudaVersion: 'Versão do CUDA', - process: 'Informações do Processo', - type: 'Tipo', - typeG: 'Gráficos', - typeC: 'Cálculo', - typeCG: 'Cálculo + Gráficos', - processName: 'Nome do Processo', - processMemoryUsage: 'Uso de Memória', - temperatureHelper: 'Temperaturas altas da GPU podem causar limitação de frequência da GPU.', - performanceStateHelper: 'De P0 (máximo desempenho) a P12 (mínimo desempenho).', - busID: 'ID do Barramento', - persistenceMode: 'Modo de Persistência', - enabled: 'Ativado', - disabled: 'Desativado', - persistenceModeHelper: - 'O modo de persistência permite respostas mais rápidas às tarefas, mas aumenta o consumo de energia em standby.', - displayActive: 'Placa Gráfica Inicializada', - displayActiveT: 'Sim', - displayActiveF: 'Não', - ecc: 'Tecnologia de Correção e Verificação de Erros', - computeMode: 'Modo de Cálculo', - default: 'Padrão', - exclusiveProcess: 'Processo Exclusivo', - exclusiveThread: 'Thread Exclusivo', - prohibited: 'Proibido', - defaultHelper: 'Padrão: Processos podem ser executados simultaneamente.', - exclusiveProcessHelper: - 'Processo Exclusivo: Apenas um contexto CUDA pode usar a GPU, mas pode ser compartilhado por múltiplas threads.', - exclusiveThreadHelper: 'Thread Exclusivo: Apenas uma thread em um contexto CUDA pode usar a GPU.', - prohibitedHelper: 'Proibido: Não é permitido que processos sejam executados simultaneamente.', - migModeHelper: 'Usado para criar instâncias MIG para isolamento físico da GPU no nível do usuário.', - migModeNA: 'Não Suportado', - }, setting: { setting: 'Configurações do Painel', title: 'Descrição do Painel', @@ -2958,11 +2981,6 @@ const message = { tamperContent4: 'Registra logs de acesso e operações em arquivos para auditoria e análise posteriores, ajudando administradores a identificar ameaças de segurança potenciais.', - gpuTitle1: 'Monitoramento Geral', - gpuContent1: 'Exibir o uso atual do GPU na página de visão geral.', - gpuTitle2: 'Detalhes do GPU', - gpuContent2: 'Mostrar os parâmetros do GPU com mais detalhes.', - settingTitle1: 'Mensagem de Boas-vindas Personalizada', settingContent1: 'Defina uma mensagem de boas-vindas personalizada na página de login do 1Panel.', settingTitle2: 'Logo Personalizado', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e2ad3b9cb..df1c68951 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -9,6 +9,7 @@ const message = { fit2cloud: 'FIT2CLOUD', lingxia: 'Lingxia', button: { + run: 'Запуск', create: 'Создать ', add: 'Добавить ', save: 'Сохранить ', @@ -328,6 +329,7 @@ const message = { firewall: 'Firewall', ssl: 'Сертификат | Сертификаты', database: 'База данных | Базы данных', + aiTools: 'AI', container: 'Контейнер | Контейнеры', cronjob: 'Cron | Задачи Cron', host: 'Хост | Хосты', @@ -586,6 +588,69 @@ const message = { 'Этот адрес подключения может использоваться приложениями, работающими вне контейнера или внешними приложениями.', localIP: 'Локальный IP', }, + aiTools: { + model: { + model: 'Модель', + create: 'Добавить модель', + create_helper: 'Загрузить "{0}"', + ollama_doc: 'Вы можете посетить официальный сайт Ollama, чтобы искать и находить больше моделей.', + container_conn_helper: 'Используйте этот адрес для доступа или подключения между контейнерами', + ollama_sync: + 'Синхронизация модели Ollama обнаружила, что следующие модели не существуют, хотите удалить их?', + from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.', + no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.', + }, + proxy: { + proxy: 'Усиление AI-прокси', + proxyHelper1: 'Привяжите домен и включите HTTPS для повышения безопасности передачи данных', + proxyHelper2: 'Ограничьте доступ по IP, чтобы предотвратить утечку данных в публичной сети', + proxyHelper3: 'Включите потоковую передачу', + proxyHelper4: 'После создания вы можете просматривать и управлять этим в списке сайтов', + proxyHelper5: + 'После включения вы можете отключить внешний доступ к порту в Магазине приложений - Установленные - Ollama - Параметры для повышения безопасности.', + proxyHelper6: 'Чтобы отключить настройку прокси, вы можете удалить её из списка сайтов.', + whiteListHelper: 'Ограничить доступ только для IP-адресов из белого списка', + }, + gpu: { + gpu: 'Мониторинг GPU', + base: 'Основная информация', + gpuHelper: 'Команда NVIDIA-SMI или XPU-SMI не обнаружена в текущей системе. Проверьте и попробуйте снова!', + driverVersion: 'Версия драйвера', + cudaVersion: 'Версия CUDA', + process: 'Информация о процессе', + type: 'Тип', + typeG: 'Графика', + typeC: 'Вычисления', + typeCG: 'Вычисления + Графика', + processName: 'Имя процесса', + processMemoryUsage: 'Использование памяти', + temperatureHelper: 'Высокая температура GPU может вызвать снижение частоты GPU', + performanceStateHelper: 'От P0 (максимальная производительность) до P12 (минимальная производительность)', + busID: 'ID шины', + persistenceMode: 'Режим постоянства', + enabled: 'Включен', + disabled: 'Выключен', + persistenceModeHelper: + 'Режим постоянства позволяет быстрее реагировать на задачи, но увеличивает потребление энергии в режиме ожидания.', + displayActive: 'Инициализация видеокарты', + displayActiveT: 'Да', + displayActiveF: 'Нет', + ecc: 'Технология проверки и коррекции ошибок (ECC)', + computeMode: 'Режим вычислений', + default: 'По умолчанию', + exclusiveProcess: 'Исключительный процесс', + exclusiveThread: 'Исключительный поток', + prohibited: 'Запрещено', + defaultHelper: 'По умолчанию: процессы могут выполняться одновременно', + exclusiveProcessHelper: + 'Исключительный процесс: только один контекст CUDA может использовать GPU, но его могут разделять несколько потоков', + exclusiveThreadHelper: 'Исключительный поток: только один поток в контексте CUDA может использовать GPU', + prohibitedHelper: 'Запрещено: процессам не разрешено выполняться одновременно', + migModeHelper: + 'Используется для создания MIG-инстансов для физической изоляции GPU на уровне пользователя.', + migModeNA: 'Не поддерживается', + }, + }, container: { create: 'Создать контейнер', edit: 'Редактировать контейнер', @@ -1710,7 +1775,6 @@ const message = { introduce: 'Описание функций', waf: 'Обновление до профессиональной версии предоставляет такие функции, как карта перехватов, логи, записи блокировок, блокировка по географическому положению, пользовательские правила, пользовательские страницы перехвата и т.д.', tamper: 'Обновление до профессиональной версии может защитить веб-сайты от несанкционированных изменений или подделок.', - gpu: 'Обновление до профессиональной версии помогает пользователям визуально отслеживать важные параметры GPU, такие как нагрузка, температура, использование памяти в реальном времени.', setting: 'Обновление до профессиональной версии позволяет настраивать логотип панели, приветственное сообщение и другую информацию.', monitor: @@ -2864,45 +2928,6 @@ const message = { 'Функция защиты от модификации для сайта {0} будет включена для повышения безопасности сайта. Вы хотите продолжить?', disableHelper: 'Функция защиты от модификации для сайта {0} будет отключена. Вы хотите продолжить?', }, - gpu: { - gpu: 'Мониторинг GPU', - base: 'Основная информация', - gpuHelper: 'Команда NVIDIA-SMI или XPU-SMI не обнаружена в текущей системе. Проверьте и попробуйте снова!', - driverVersion: 'Версия драйвера', - cudaVersion: 'Версия CUDA', - process: 'Информация о процессе', - type: 'Тип', - typeG: 'Графика', - typeC: 'Вычисления', - typeCG: 'Вычисления + Графика', - processName: 'Имя процесса', - processMemoryUsage: 'Использование памяти', - temperatureHelper: 'Высокая температура GPU может вызвать снижение частоты GPU', - performanceStateHelper: 'От P0 (максимальная производительность) до P12 (минимальная производительность)', - busID: 'ID шины', - persistenceMode: 'Режим постоянства', - enabled: 'Включен', - disabled: 'Выключен', - persistenceModeHelper: - 'Режим постоянства позволяет быстрее реагировать на задачи, но увеличивает потребление энергии в режиме ожидания.', - displayActive: 'Инициализация видеокарты', - displayActiveT: 'Да', - displayActiveF: 'Нет', - ecc: 'Технология проверки и коррекции ошибок (ECC)', - computeMode: 'Режим вычислений', - default: 'По умолчанию', - exclusiveProcess: 'Исключительный процесс', - exclusiveThread: 'Исключительный поток', - prohibited: 'Запрещено', - defaultHelper: 'По умолчанию: процессы могут выполняться одновременно', - exclusiveProcessHelper: - 'Исключительный процесс: только один контекст CUDA может использовать GPU, но его могут разделять несколько потоков', - exclusiveThreadHelper: 'Исключительный поток: только один поток в контексте CUDA может использовать GPU', - prohibitedHelper: 'Запрещено: процессам не разрешено выполняться одновременно', - migModeHelper: - 'Используется для создания MIG-инстансов для физической изоляции GPU на уровне пользователя.', - migModeNA: 'Не поддерживается', - }, setting: { setting: 'Настройки Панели', title: 'Описание Панели', @@ -2947,11 +2972,6 @@ const message = { tamperContent4: 'Записывайте логи доступа и операций с файлами для последующего аудита и анализа администраторами, а также для выявления потенциальных угроз безопасности.', - gpuTitle1: 'Мониторинг обзора', - gpuContent1: 'Отображение текущего использования GPU на странице обзора.', - gpuTitle2: 'Детали GPU', - gpuContent2: 'Показать параметры GPU с большей точностью.', - settingTitle1: 'Пользовательское Приветственное Сообщение', settingContent1: 'Установите пользовательское приветственное сообщение на странице входа в 1Panel.', settingTitle2: 'Пользовательский Логотип', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index bc7ec539f..5b1967861 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -9,6 +9,7 @@ const message = { lingxia: '凌霞', colon: ': ', button: { + run: '執行', prev: '上一步', next: '下一步', create: '創建', @@ -327,6 +328,7 @@ const message = { firewall: '防火墻', ssl: '證書', database: '數據庫', + aiTools: 'AI', container: '容器', cronjob: '計劃任務', host: '主機', @@ -570,6 +572,64 @@ const message = { remoteConnHelper2: '非容器或外部連接使用此地址', localIP: '本機 IP', }, + aiTools: { + model: { + model: '模型', + create: '新增模型', + create_helper: '拉取 "{0}"', + ollama_doc: '您可以瀏覽 Ollama 官方網站,搜尋並查找更多模型。', + container_conn_helper: '容器間瀏覽或連接使用此地址', + ollama_sync: '同步 Ollama 模型發現下列模型不存在,是否刪除?', + from_remote: '該模型並非透過 1Panel 下載,無相關拉取日誌。', + no_logs: '該模型的拉取日誌已被刪除,無法查看相關日誌。', + }, + proxy: { + proxy: 'AI 代理增強', + proxyHelper1: '綁定域名並啟用 HTTPS,提高傳輸安全性', + proxyHelper2: '限制 IP 瀏覽,防止在網路上暴露', + proxyHelper3: '啟用即時串流', + proxyHelper4: '創建後,您可以在網站列表中查看並管理', + proxyHelper5: '啟用後,您可以在應用商店 - 已安裝 - Ollama - 參數中取消埠外部瀏覽以提高安全性', + proxyHelper6: '如需關閉代理配置,可以在網站列表中刪除', + whiteListHelper: '限制僅白名單中的 IP 可瀏覽', + }, + gpu: { + gpu: 'GPU 監控', + base: '基礎資訊', + gpuHelper: '目前系統未檢測到 NVIDIA-SMI 或者 XPU-SMI 指令,請檢查後重試!', + driverVersion: '驅動版本', + cudaVersion: 'CUDA 版本', + process: '行程資訊', + type: '類型', + typeG: '圖形', + typeC: '計算', + typeCG: '計算+圖形', + processName: '行程名稱', + processMemoryUsage: '記憶體使用', + temperatureHelper: 'GPU 溫度過高會導致 GPU 頻率下降', + performanceStateHelper: '從 P0 (最大性能) 到 P12 (最小性能)', + busID: '匯流排地址', + persistenceMode: '持續模式', + enabled: '開啟', + disabled: '關閉', + persistenceModeHelper: '持續模式能更加快速地響應任務,但相應待機功耗也會增加', + displayActive: '顯卡初始化', + displayActiveT: '是', + displayActiveF: '否', + ecc: '是否開啟錯誤檢查和糾正技術', + computeMode: '計算模式', + default: '預設', + exclusiveProcess: '行程排他', + exclusiveThread: '執行緒排他', + prohibited: '禁止', + defaultHelper: '預設: 行程可以並發執行', + exclusiveProcessHelper: '行程排他: 只有一個 CUDA 上下文可以使用 GPU, 但可以由多個執行緒共享', + exclusiveThreadHelper: '執行緒排他: 只有一個執行緒在 CUDA 上下文中可以使用 GPU', + prohibitedHelper: '禁止: 不允許行程同時執行', + migModeHelper: '用於建立 MIG 實例,在用戶層實現 GPU 的物理隔離。', + migModeNA: '不支援', + }, + }, container: { create: '創建容器', createByCommand: '命令創建', @@ -1697,7 +1757,6 @@ const message = { waf: '升級專業版可以獲得攔截地圖、日誌、封鎖記錄、地理位置封禁、自定義規則、自定義攔截頁面等功能。', tamper: '升級專業版可以保護網站免受未經授權的修改或篡改。', tamperHelper: '操作失敗,該文件或文件夾已經開啟防篡改,請檢查後重試!', - gpu: '升級專業版可以幫助用戶實時直觀查看到 GPU 的工作負載、溫度、顯存等重要參數。', setting: '升級專業版可以自定義面板 Logo、歡迎簡介等信息。', monitor: '升級專業版可以查看網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。 ', alert: '陞級專業版可通過簡訊接收告警資訊,並查看告警日誌,全面掌控各類關鍵事件,確保系統運行無憂。', @@ -2774,42 +2833,6 @@ const message = { enableHelper: '即將啟用 {0} 網站的防篡改功能,以提升網站安全性,是否繼續?', disableHelper: '即將關閉 {0} 網站的防篡改功能,是否繼續?', }, - gpu: { - gpu: 'GPU 监控', - base: '基礎資訊', - gpuHelper: '目前系統未檢測到 NVIDIA-SMI或者XPU-SMI 指令,請檢查後重試!', - driverVersion: '驅動版本', - cudaVersion: 'CUDA 版本', - process: '行程資訊', - type: '類型', - typeG: '圖形', - typeC: '計算', - typeCG: '計算+圖形', - processName: '行程名稱', - processMemoryUsage: '顯存使用', - temperatureHelper: 'GPU 溫度過高會導致 GPU 頻率下降', - performanceStateHelper: '從 P0 (最大性能) 到 P12 (最小性能)', - busID: '總線地址', - persistenceMode: '持續模式', - enabled: '開啟', - disabled: '關閉', - persistenceModeHelper: '持續模式能更加快速地響應任務,但相應待機功耗也會增加', - displayActive: '顯卡初始化', - displayActiveT: '是', - displayActiveF: '否', - ecc: '是否開啟錯誤檢查和紀正技術', - computeMode: '計算模式', - default: '預設', - exclusiveProcess: '行程排他', - exclusiveThread: '線程排他', - prohibited: '禁止', - defaultHelper: '預設: 行程可以並發執行', - exclusiveProcessHelper: '行程排他: 只有一個 CUDA 上下文可以使用 GPU, 但可以由多個線程共享', - exclusiveThreadHelper: '線程排他: 只有一個線程在 CUDA 上下文中可以使用 GPU', - prohibitedHelper: '禁止: 不允許行程同時執行', - migModeHelper: '用於建立 MIG 實例,在用戶層實現 GPU 的物理隔離。', - migModeNA: '不支援', - }, setting: { setting: '界面設定', title: '面板描述', @@ -2847,11 +2870,6 @@ const message = { tamperTitle4: '日誌紀錄與分析', tamperContent4: '紀錄檔案瀏覽和操作日誌,以便管理員進行後續的審計和分析,以及發現潛在的安全威脅。', - gpuTitle1: '概覽頁監控', - gpuContent1: '在概覽頁上顯示 GPU 當前使用情況。', - gpuTitle2: 'GPU 詳細資訊', - gpuContent2: '更加細緻地顯示 GPU 各項參數。', - settingTitle1: '自訂歡迎語', settingContent1: '在 1Panel 登入頁上設定自訂的歡迎語。', settingTitle2: '自訂 Logo', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 065e71474..29ba22db9 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -9,6 +9,7 @@ const message = { lingxia: '凌霞', colon: ': ', button: { + run: '运行', prev: '上一步', next: '下一步', create: '创建', @@ -325,6 +326,7 @@ const message = { firewall: '防火墙', ssl: '证书', database: '数据库', + aiTools: 'AI', container: '容器', cronjob: '计划任务', host: '主机', @@ -568,6 +570,65 @@ const message = { remoteConnHelper2: '非容器或外部连接使用此地址', localIP: '本机 IP', }, + aiTools: { + model: { + model: '模型', + create: '添加模型', + create_helper: '拉取 "{0}"', + ollama_doc: '您可以访问 Ollama 官网,搜索并查找更多模型。', + container_conn_helper: '容器间访问或连接使用此地址', + ollama_sync: '同步 Ollama 模型发现下列模型不存在,是否删除?', + from_remote: '该模型并非通过 1Panel 下载,无相关拉取日志。', + no_logs: '该模型的拉取日志已被删除,无法查看相关日志。', + }, + proxy: { + proxy: 'AI 代理增强', + proxyHelper1: '绑定域名并开启 HTTPS,增强传输安全性', + proxyHelper2: '限制 IP 访问,防止在公网暴露', + proxyHelper3: '开启流式传输', + proxyHelper4: '创建完成之后可以在网站列表中查看并管理', + proxyHelper5: '创建完成之后可以在应用商店 - 已安装 - ollama - 参数中取消端口外部访问以提高安全性', + proxyHelper6: '如需关闭代理配置,可以在网站列表中删除', + whiteListHelper: '限制仅白名单中的 IP 可访问', + }, + gpu: { + gpu: 'GPU 监控', + base: '基础信息', + gpuHelper: '当前系统未检测到 NVIDIA-SMI或者XPU-SMI 指令,请检查后重试!', + driverVersion: '驱动版本', + cudaVersion: 'CUDA 版本', + process: '进程信息', + type: '类型', + typeG: '图形', + typeC: '计算', + typeCG: '计算+图形', + processName: '进程名称', + processMemoryUsage: '显存使用', + temperatureHelper: 'GPU 温度过高会导致 GPU 频率下降', + performanceStateHelper: '从 P0 (最大性能) 到 P12 (最小性能)', + busID: '总线地址', + persistenceMode: '持续模式', + enabled: '开启', + disabled: '关闭', + persistenceModeHelper: '持续模式能更加快速地响应任务,但相应待机功耗也会增加', + displayActive: '显卡初始化', + displayActiveT: '是', + displayActiveF: '否', + ecc: '是否开启错误检查和纠正技术', + computeMode: '计算模式', + default: '默认', + exclusiveProcess: '进程排他', + exclusiveThread: '线程排他', + prohibited: '禁止', + defaultHelper: '默认: 进程可以并发执行', + exclusiveProcessHelper: '进程排他: 只有一个 CUDA 上下文可以使用 GPU, 但可以由多个线程共享', + exclusiveThreadHelper: '线程排他: 只有一个线程在 CUDA 上下文中可以使用 GPU', + prohibitedHelper: '禁止: 不允许进程同时执行', + migModeHelper: '用于创建 MIG 实例,在用户层实现 GPU 的物理隔离。', + migModeNA: '不支持', + shr: '共享显存', + }, + }, container: { create: '创建容器', createByCommand: '命令创建', @@ -1665,7 +1726,6 @@ const message = { waf: '升级专业版可以获得拦截地图、日志、封锁记录、地理位置封禁、自定义规则、自定义拦截页面等功能。', tamper: '升级专业版可以保护网站免受未经授权的修改或篡改。', tamperHelper: '操作失败,该文件或文件夹已经开启防篡改,请检查后重试!', - gpu: '升级专业版可以帮助用户实时直观查看到 GPU 的工作负载、温度、显存等重要参数。', setting: '升级专业版可以自定义面板 Logo、欢迎简介等信息。', monitor: '升级专业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。', alert: '升级专业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。', @@ -2803,38 +2863,6 @@ const message = { enableHelper: '即将启用下列网站的防篡改功能,以提升网站安全性,是否继续?', disableHelper: '即将关闭下列网站的防篡改功能,是否继续?', }, - gpu: { - gpu: 'GPU 监控', - base: '基础信息', - gpuHelper: '当前系统未检测到 NVIDIA-SMI 指令,请检查后重试!', - driverVersion: '驱动版本', - cudaVersion: 'CUDA 版本', - process: '进程信息', - typeG: '图形', - typeC: '计算', - typeCG: '计算+图形', - processName: '进程名称', - processMemoryUsage: '显存使用', - temperatureHelper: 'GPU 温度过高会导致 GPU 频率下降', - performanceStateHelper: '从 P0 (最大性能) 到 P12 (最小性能)', - busID: '总线地址', - persistenceMode: '持续模式', - persistenceModeHelper: '持续模式能更加快速地响应任务,但相应待机功耗也会增加', - displayActive: '显卡初始化', - displayActiveT: '是', - displayActiveF: '否', - ecc: '是否开启错误检查和纠正技术', - computeMode: '计算模式', - exclusiveProcess: '进程排他', - exclusiveThread: '线程排他', - prohibited: '禁止', - defaultHelper: '默认: 进程可以并发执行', - exclusiveProcessHelper: '进程排他: 只有一个 CUDA 上下文可以使用 GPU, 但可以由多个线程共享', - exclusiveThreadHelper: '线程排他: 只有一个线程在 CUDA 上下文中可以使用 GPU', - prohibitedHelper: '禁止: 不允许进程同时执行', - migModeHelper: '用于创建 MIG 实例,在用户层实现 GPU 的物理隔离。', - migModeNA: '不支持', - }, setting: { setting: '界面设置', title: '面板描述', @@ -2870,11 +2898,6 @@ const message = { tamperTitle4: '日志记录与分析', tamperContent4: '记录文件访问和操作日志,以便管理员进行后续的审计和分析,以及发现潜在的安全威胁。', - gpuTitle1: '概览页监控', - gpuContent1: '在概览页上显示 GPU 当前使用情况。', - gpuTitle2: 'GPU 详细信息', - gpuContent2: '更加细粒度的显示出 GPU 各项参数。', - settingTitle1: '自定义欢迎语', settingContent1: '在 1Panel 登录页上设置自定义的欢迎语。', settingTitle2: '自定义 Logo', diff --git a/frontend/src/routers/modules/ai.ts b/frontend/src/routers/modules/ai.ts new file mode 100644 index 000000000..2baa7c76d --- /dev/null +++ b/frontend/src/routers/modules/ai.ts @@ -0,0 +1,34 @@ +import { Layout } from '@/routers/constant'; + +const databaseRouter = { + sort: 4, + path: '/ai', + component: Layout, + redirect: '/ai/model', + meta: { + icon: 'p-jiqiren2', + title: 'menu.aiTools', + }, + children: [ + { + path: '/ai/model', + name: 'OllamaModel', + component: () => import('@/views/ai/model/index.vue'), + meta: { + title: 'aiTools.model.model', + requiresAuth: true, + }, + }, + { + path: '/ai/gpu', + name: 'GPU', + component: () => import('@/views/ai/gpu/index.vue'), + meta: { + title: 'aiTools.gpu.gpu', + requiresAuth: true, + }, + }, + ], +}; + +export default databaseRouter; diff --git a/frontend/src/routers/modules/container.ts b/frontend/src/routers/modules/container.ts index d95b073e3..376f881b3 100644 --- a/frontend/src/routers/modules/container.ts +++ b/frontend/src/routers/modules/container.ts @@ -1,7 +1,7 @@ import { Layout } from '@/routers/constant'; const containerRouter = { - sort: 5, + sort: 6, path: '/containers', component: Layout, redirect: '/containers/container', diff --git a/frontend/src/routers/modules/database.ts b/frontend/src/routers/modules/database.ts index 84787705b..f1261dc07 100644 --- a/frontend/src/routers/modules/database.ts +++ b/frontend/src/routers/modules/database.ts @@ -1,7 +1,7 @@ import { Layout } from '@/routers/constant'; const databaseRouter = { - sort: 4, + sort: 5, path: '/databases', component: Layout, redirect: '/databases/mysql', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 21149ef97..34293a11f 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -1,7 +1,7 @@ import { Layout } from '@/routers/constant'; const hostRouter = { - sort: 6, + sort: 7, path: '/hosts', component: Layout, redirect: '/hosts/security', diff --git a/frontend/src/routers/modules/terminal.ts b/frontend/src/routers/modules/terminal.ts index 5a8bc1a00..e3ac89537 100644 --- a/frontend/src/routers/modules/terminal.ts +++ b/frontend/src/routers/modules/terminal.ts @@ -1,7 +1,7 @@ import { Layout } from '@/routers/constant'; const terminalRouter = { - sort: 7, + sort: 8, path: '/terminal', component: Layout, redirect: '/terminal', diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index 5ae5c9e4c..e4cd657a0 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/frontend/src/routers/modules/toolbox.ts @@ -1,7 +1,7 @@ import { Layout } from '@/routers/constant'; const toolboxRouter = { - sort: 8, + sort: 9, path: '/toolbox', component: Layout, redirect: '/toolbox/supervisor', diff --git a/frontend/src/views/ai/gpu/index.vue b/frontend/src/views/ai/gpu/index.vue new file mode 100644 index 000000000..721965527 --- /dev/null +++ b/frontend/src/views/ai/gpu/index.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/frontend/src/views/ai/model/add/index.vue b/frontend/src/views/ai/model/add/index.vue new file mode 100644 index 000000000..61904739c --- /dev/null +++ b/frontend/src/views/ai/model/add/index.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/views/ai/model/conn/index.vue b/frontend/src/views/ai/model/conn/index.vue new file mode 100644 index 000000000..6b413bab0 --- /dev/null +++ b/frontend/src/views/ai/model/conn/index.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/frontend/src/views/ai/model/del/index.vue b/frontend/src/views/ai/model/del/index.vue new file mode 100644 index 000000000..b804e902f --- /dev/null +++ b/frontend/src/views/ai/model/del/index.vue @@ -0,0 +1,105 @@ + + + diff --git a/frontend/src/views/ai/model/domain/index.vue b/frontend/src/views/ai/model/domain/index.vue new file mode 100644 index 000000000..6e0f76c33 --- /dev/null +++ b/frontend/src/views/ai/model/domain/index.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/frontend/src/views/ai/model/index.vue b/frontend/src/views/ai/model/index.vue new file mode 100644 index 000000000..9e7e48c85 --- /dev/null +++ b/frontend/src/views/ai/model/index.vue @@ -0,0 +1,457 @@ + + + + + diff --git a/frontend/src/views/ai/model/terminal/index.vue b/frontend/src/views/ai/model/terminal/index.vue new file mode 100644 index 000000000..993053a79 --- /dev/null +++ b/frontend/src/views/ai/model/terminal/index.vue @@ -0,0 +1,76 @@ + + +