Compare commits

..
22 Commits
Author SHA1 Message Date
蘭 e29679768b feat: add MCP error metadata handling and improve source policy resolution (#14270)
* feat: add MCP error metadata handling and improve source policy resolution

* fix: adjust height-diff for improved table layout
2026-11-02 22:17:12 +08:00
CityFun 8e1b761f4c fix(app): allow upgrades from removed versions (#14280) 2026-11-02 22:17:03 +08:00
蘭 963c1463a8 fix: update MCP access permissions and improve titles in routing (#14281) 2026-11-02 22:16:53 +08:00
ssongliu 2401bb16a2 feat(ai): connect local model accounts and add Ollama management page (#14282)
* feat(ai): connect local model accounts and add Ollama management page

* fix(ai): register Ollama management frontend path
2026-11-02 22:16:43 +08:00
ssongliu c06c4e338b feat(vm): enable community virtual machines and migrate xpack data (#14279)
* feat(vm): enable community virtual machines and migrate xpack data

* fix(vm): stop ISO download polling on failure or cancellation

* fix(vm): respect deleteFile when deleting failed ISO imports
2026-11-02 18:22:58 +08:00
ssongliu f8c61993c5 feat(ai): open source model marketplace and migrate historical data (#14277)
* feat(ai): open source model marketplace and migrate historical data

* fix(ai): wait for canceled model downloads before retrying
2026-11-02 17:46:26 +08:00
ssongliu b31e683f1b fix: correct local inference engine operation task logs (#14274) 2026-11-02 17:27:43 +08:00
Yuki Guo 50f0a37bee feat: add remote Docker Hub image search (#14271) 2026-11-02 11:35:26 +08:00
ssongliu 51cf40a648 feat(ai): show local inference engine ports (#14273) 2026-11-02 11:21:17 +08:00
Yuki Guo 7ec31cd5e9 Merge pull request #14246 from 1Panel-dev/pr@dev-v2@feat_image_check
feat(container): add image update checks with scheduling and task logs
2026-11-02 10:48:03 +08:00
Yuki Guo 50a45b72f3 Merge branch 'dev-v2' into pr@dev-v2@feat_image_check 2026-11-02 09:44:27 +08:00
ssongliu 13362f84a6 fix(ai): synchronize inference engine task states and unify management (#14269) 2026-11-01 23:36:15 +08:00
ssongliu 046dc94d05 fix(script): bypass stale resource caches during library sync (#14268) 2026-11-01 22:32:58 +08:00
ssongliu 2059119d89 fix(dashboard): restore monitor layout and compact single GPU status (#14267) 2026-11-01 22:32:06 +08:00
ssongliu 7f4c503982 fix(ai): improve GPU capability hints and separate engine logs (#14263) 2026-11-01 01:06:37 +08:00
ssongliu 93ebc6a8b9 feat(ai): streamline Ollama model pool management (#14259) 2026-10-30 23:50:11 +08:00
王贺andzhengkunwang223 9a49f38ffd fix(network): disable MPTCP by default (#14258)
Set multipathtcp=0 in the core and agent module defaults so Linux listeners use standard TCP without requiring service environment changes.

Co-authored-by: zhengkunwang223 <31820853+zhengkunwang223@users.noreply.github.com>
2026-10-30 23:31:05 +08:00
ssongliu 7156addf79 feat(ai): refine model pools, navigation and GPU deployment configuration (#14257) 2026-10-30 23:00:23 +08:00
ssongliu c6329a62db feat(dashboard): refine monitoring, quick access and installation messages (#14256) 2026-10-30 22:46:20 +08:00
guoyuqi 9644d5429f refactor: reuse base settings for image update proxy checks 2026-10-30 17:42:24 +08:00
guoyuqi 8b6328fd04 feat(container): add image update checks with scheduling and task logs 2026-10-29 19:02:44 +08:00
guoyuqi e9c2162921 feat(container): add image update checks with scheduling and task logs 2026-10-29 18:25:32 +08:00
260 changed files with 34415 additions and 2325 deletions
+2
View File
@@ -79,3 +79,5 @@ opencode.json
superpowers superpowers
.worktrees/ .worktrees/
.codex/ .codex/
/agent/utils/vm/helper/bin/1panel-kvm*
+5 -2
View File
@@ -32,7 +32,10 @@ build_core_on_linux:
cd $(CORE_PATH) \ cd $(CORE_PATH) \
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN)
build_agent_on_linux: prepare_vm_helper:
bash $(AGENT_PATH)/utils/vm/helper/prepare.sh
build_agent_on_linux: prepare_vm_helper
cd $(AGENT_PATH) \ cd $(AGENT_PATH) \
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN)
@@ -40,7 +43,7 @@ build_core_on_darwin:
cd $(CORE_PATH) \ cd $(CORE_PATH) \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(CORE_NAME) $(CORE_MAIN)
build_agent_on_darwin: build_agent_on_darwin: prepare_vm_helper
cd $(AGENT_PATH) \ cd $(AGENT_PATH) \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN)
@@ -0,0 +1,43 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) GetContainerImageCheckUpdateStatus(c *gin.Context) {
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, status)
}
func (b *BaseApi) ContainerImageCheckUpdate(c *gin.Context) {
taskID, err := service.StartContainerImageCheckUpdate()
if err != nil {
helper.InternalServer(c, err)
return
}
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
status.TaskID = taskID
helper.SuccessWithData(c, status)
}
func (b *BaseApi) SetContainerImageCheckUpdateSchedule(c *gin.Context) {
var req struct {
Enabled *bool `json:"enabled" validate:"required"`
}
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := service.SetContainerImageCheckUpdateSchedule(*req.Enabled); err != nil {
helper.InternalServer(c, err)
return
}
b.GetContainerImageCheckUpdateStatus(c)
}
+2
View File
@@ -11,12 +11,14 @@ var ApiGroupApp = new(ApiGroup)
type BaseApi struct{} type BaseApi struct{}
var ( var (
vmService = service.NewIVMService()
dashboardService = service.NewIDashboardService() dashboardService = service.NewIDashboardService()
appService = service.NewIAppService() appService = service.NewIAppService()
appInstallService = service.NewIAppInstalledService() appInstallService = service.NewIAppInstalledService()
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService() appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
modelDownloaderService = service.NewIModelDownloaderService()
aiToolService = service.NewIAIToolService() aiToolService = service.NewIAIToolService()
mcpServerService = service.NewIMcpServerService() mcpServerService = service.NewIMcpServerService()
tensorrtLLMService = service.NewITensorRTLLMService() tensorrtLLMService = service.NewITensorRTLLMService()
+26 -4
View File
@@ -12,7 +12,6 @@ import (
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -23,7 +22,7 @@ import (
) )
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) applyErrorMetadata(ctx, code, msgKey, err)
res := dto.Response{ res := dto.Response{
Code: code, Code: code,
Message: "", Message: "",
@@ -34,7 +33,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
} }
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) { func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) applyErrorMetadata(ctx, code, msgKey, err)
res := dto.Response{ res := dto.Response{
Code: code, Code: code,
ErrorCode: businessCode, ErrorCode: businessCode,
@@ -45,7 +44,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri
} }
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) { func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err) applyErrorMetadata(ctx, code, msgKey, err)
res := dto.Response{ res := dto.Response{
Code: code, Code: code,
Data: data, Data: data,
@@ -209,3 +208,26 @@ func SetOperationLogValues(c *gin.Context, values map[string]string) {
c.Header(header, base64.RawURLEncoding.EncodeToString(data)) c.Header(header, base64.RawURLEncoding.EncodeToString(data))
} }
} }
const errorMetadataKey = "error_response_metadata_policies"
// ErrorMetadataPolicy adds transport metadata before the existing error response is written.
type ErrorMetadataPolicy func(*gin.Context, int, string, error)
func WithErrorMetadata(policies ...ErrorMetadataPolicy) gin.HandlerFunc {
configured := append([]ErrorMetadataPolicy(nil), policies...)
return func(c *gin.Context) {
c.Set(errorMetadataKey, configured)
c.Next()
}
}
func applyErrorMetadata(c *gin.Context, code int, messageKey string, err error) {
value, _ := c.Get(errorMetadataKey)
policies, _ := value.([]ErrorMetadataPolicy)
for _, policy := range policies {
if policy != nil {
policy(c, code, messageKey, err)
}
}
}
+31
View File
@@ -1,8 +1,12 @@
package v2 package v2
import ( import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
_ "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" // Swagger response schema.
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -225,3 +229,30 @@ func (b *BaseApi) ImageLoad(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Container Image
// @Summary Search Docker Hub images
// @Accept json
// @Param request body dto.ImageRemoteSearch true "request"
// @Produce json
// @Success 200 {object} dockerhub.SearchResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/image/remote/search [post]
func (b *BaseApi) SearchRemoteImages(c *gin.Context) {
var req dto.ImageRemoteSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
req.Query = strings.TrimSpace(req.Query)
if req.Query == "" {
helper.BadRequest(c, fmt.Errorf("search query is required"))
return
}
result, err := imageService.SearchRemoteImages(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+390
View File
@@ -0,0 +1,390 @@
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/app/service"
"github.com/gin-gonic/gin"
)
func handleModelDownloaderError(c *gin.Context, err error) {
if service.IsModelDownloaderInputError(err) {
helper.BadRequest(c, err)
return
}
helper.InternalServer(c, err)
}
// @Tags AI
// @Summary Search Hugging Face models
// @Accept json
// @Param request body dto.ModelSearch true "request"
// @Produce json
// @Success 200 {object} dto.PageResult{items=[]dto.ModelItem}
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/hf/search [post]
func (b *BaseApi) SearchHFModels(c *gin.Context) {
var req dto.ModelSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, items, err := modelDownloaderService.SearchHFModels(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Total: total, Items: items})
}
// @Tags AI
// @Summary Get Hugging Face model details
// @Accept json
// @Param request body dto.ModelInfoReq true "request"
// @Produce json
// @Success 200 {object} dto.ModelInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/hf/info [post]
func (b *BaseApi) GetHFModelInfo(c *gin.Context) {
var req dto.ModelInfoReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := modelDownloaderService.GetHFModelInfo(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags AI
// @Summary Download Hugging Face model
// @Description Creates an asynchronous download task. Query /ai/model/downloader/tasks/search for progress.
// @Accept json
// @Param request body dto.ModelDownload true "request"
// @Produce json
// @Success 200 {object} dto.ModelDownloadTaskItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/hf/download [post]
// @x-panel-log {"bodyKeys":["repoID","revision"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 Hugging Face 模型 [repoID]","formatEN":"download Hugging Face model [repoID]"}
func (b *BaseApi) StartHFModelDownload(c *gin.Context) {
var req dto.ModelDownload
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
task, err := modelDownloaderService.StartDownload(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, task)
}
// @Tags AI
// @Summary Search ModelScope models
// @Accept json
// @Param request body dto.ModelSearch true "request"
// @Produce json
// @Success 200 {object} dto.PageResult{items=[]dto.ModelItem}
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/modelscope/search [post]
func (b *BaseApi) SearchModelScopeModels(c *gin.Context) {
var req dto.ModelSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, items, err := modelDownloaderService.SearchModelScopeModels(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Total: total, Items: items})
}
// @Tags AI
// @Summary Get ModelScope model details
// @Accept json
// @Param request body dto.ModelInfoReq true "request"
// @Produce json
// @Success 200 {object} dto.ModelInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/modelscope/info [post]
func (b *BaseApi) GetModelScopeModelInfo(c *gin.Context) {
var req dto.ModelInfoReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := modelDownloaderService.GetModelScopeModelInfo(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags AI
// @Summary Download ModelScope model
// @Description Creates an asynchronous download task. Query /ai/model/downloader/tasks/search for progress.
// @Accept json
// @Param request body dto.ModelDownload true "request"
// @Produce json
// @Success 200 {object} dto.ModelDownloadTaskItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/modelscope/download [post]
// @x-panel-log {"bodyKeys":["repoID","revision"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 ModelScope 模型 [repoID]","formatEN":"download ModelScope model [repoID]"}
func (b *BaseApi) StartModelScopeModelDownload(c *gin.Context) {
var req dto.ModelDownload
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
task, err := modelDownloaderService.StartModelScopeDownload(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, task)
}
// @Tags AI
// @Summary Get model downloader settings
// @Produce json
// @Success 200 {object} dto.ModelDownloaderSetting
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/settings [get]
func (b *BaseApi) GetModelDownloaderSettings(c *gin.Context) {
settings, err := modelDownloaderService.GetSettings()
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, settings)
}
// @Tags AI
// @Summary Update model downloader settings
// @Description Updates the download directory, source endpoints and tokens. The featuredModelsSync field is ignored; use /ai/model/downloader/featured/sync/status to update automatic synchronization.
// @Accept json
// @Param request body dto.ModelDownloaderSetting true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/settings [post]
// @x-panel-log {"bodyKeys":["modelDir"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新模型下载设置 [modelDir]","formatEN":"update model downloader settings [modelDir]"}
func (b *BaseApi) UpdateModelDownloaderSettings(c *gin.Context) {
var req dto.ModelDownloaderSetting
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := modelDownloaderService.UpdateSettings(req); err != nil {
handleModelDownloaderError(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Page model download tasks
// @Description Set includeLocal to true to include locally downloaded models.
// @Accept json
// @Param request body dto.ModelDownloadTaskSearch true "request"
// @Produce json
// @Success 200 {object} dto.PageResult{items=[]dto.ModelDownloadTaskItem}
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/tasks/search [post]
func (b *BaseApi) PageModelDownloadTasks(c *gin.Context) {
var req dto.ModelDownloadTaskSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, items, err := modelDownloaderService.PageTasks(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
}
// @Tags AI
// @Summary Cancel model download task
// @Description Only waiting or downloading tasks can be canceled. The withFile field is ignored.
// @Accept json
// @Param request body dto.ModelDownloadTaskOperate true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/tasks/cancel [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"取消模型下载任务 [repoID]","formatEN":"cancel model download task [repoID]"}
func (b *BaseApi) CancelModelDownloadTask(c *gin.Context) {
var req dto.ModelDownloadTaskOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := modelDownloaderService.CancelTask(req); err != nil {
handleModelDownloaderError(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Retry model download task
// @Description Only failed or canceled tasks can be retried. Returns the task while the download runs asynchronously.
// @Accept json
// @Param request body dto.ModelDownloadTaskRetry true "request"
// @Produce json
// @Success 200 {object} dto.ModelDownloadTaskItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/tasks/retry [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"重试模型下载任务 [repoID]","formatEN":"retry model download task [repoID]"}
func (b *BaseApi) RetryModelDownloadTask(c *gin.Context) {
var req dto.ModelDownloadTaskRetry
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
task, err := modelDownloaderService.RetryTask(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, task)
}
// @Tags AI
// @Summary Remove model download task
// @Description Waiting or downloading tasks cannot be removed. Set withFile to true to also delete downloaded files.
// @Accept json
// @Param request body dto.ModelDownloadTaskOperate true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/tasks/remove [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"model_download_tasks","output_column":"repo_id","output_value":"repoID"}],"formatZH":"移除模型下载任务 [repoID]","formatEN":"remove model download task [repoID]"}
func (b *BaseApi) RemoveModelDownloadTask(c *gin.Context) {
var req dto.ModelDownloadTaskOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := modelDownloaderService.RemoveTask(req); err != nil {
handleModelDownloaderError(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Page locally downloaded models
// @Accept json
// @Param request body dto.LocalModelSearch true "request"
// @Produce json
// @Success 200 {object} dto.PageResult{items=[]dto.LocalModelItem}
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/local/search [post]
func (b *BaseApi) PageLocalDownloadedModels(c *gin.Context) {
var req dto.LocalModelSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, items, err := modelDownloaderService.ListLocalModels(req)
if err != nil {
handleModelDownloaderError(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
}
// @Tags AI
// @Summary Delete locally downloaded model
// @Accept json
// @Param request body dto.LocalModelDelete true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/local/delete [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除本地模型 [name]","formatEN":"delete local model [name]"}
func (b *BaseApi) DeleteLocalDownloadedModel(c *gin.Context) {
var req dto.LocalModelDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := modelDownloaderService.DeleteLocalModel(req); err != nil {
handleModelDownloaderError(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Synchronize featured models
// @Description Synchronizes the featured model catalog immediately, independently of the automatic synchronization setting. Does nothing in offline mode.
// @Accept json
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/featured/sync [post]
func (b *BaseApi) SyncFeaturedModels(c *gin.Context) {
if err := modelDownloaderService.SyncFeaturedModels(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Page featured models
// @Accept json
// @Param request body dto.FeaturedModelSearch true "request"
// @Produce json
// @Success 200 {object} dto.FeaturedModelPage{items=[]dto.FeaturedModel}
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/featured/search [post]
func (b *BaseApi) PageFeaturedModels(c *gin.Context) {
var req dto.FeaturedModelSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
result, err := modelDownloaderService.PageFeaturedModels(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags AI
// @Summary Update featured model automatic synchronization
// @Description Set value to Enable or Disable. This updates the automatic synchronization setting without triggering an immediate synchronization.
// @Accept json
// @Param request body dto.ModelDownloaderSyncSetting true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/model/downloader/featured/sync/status [post]
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新推荐模型自动同步 [value]","formatEN":"update featured model automatic synchronization [value]"}
func (b *BaseApi) UpdateFeaturedModelsSync(c *gin.Context) {
var req dto.ModelDownloaderSyncSetting
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := modelDownloaderService.UpdateFeaturedModelsSync(req); err != nil {
handleModelDownloaderError(c, err)
return
}
helper.Success(c)
}
+6 -2
View File
@@ -19,7 +19,11 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
list := tensorrtLLMService.Page(req) list, err := tensorrtLLMService.Page(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
@@ -81,7 +85,7 @@ func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
err := tensorrtLLMService.Delete(req.ID) err := tensorrtLLMService.Delete(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
File diff suppressed because it is too large Load Diff
+3
View File
@@ -318,6 +318,7 @@ type AgentAccountModelDiscoverReq struct {
} }
type AgentAccountModelCreateReq struct { type AgentAccountModelCreateReq struct {
TaskID string `json:"taskID"`
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"` Model AgentAccountModel `json:"model" validate:"required"`
} }
@@ -328,6 +329,7 @@ type AgentAccountModelUpdateReq struct {
} }
type AgentAccountModelDeleteReq struct { type AgentAccountModelDeleteReq struct {
DeleteFiles bool `json:"deleteFiles"`
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
RecordID uint `json:"recordId" validate:"required"` RecordID uint `json:"recordId" validate:"required"`
} }
@@ -358,6 +360,7 @@ type AgentAccountUpdateReq struct {
ValidateAvailability *bool `json:"validateAvailability"` ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"` Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"` SyncAgents bool `json:"syncAgents"`
Models []AgentAccountModel `json:"models"`
} }
type AgentAccountVerifyReq struct { type AgentAccountVerifyReq struct {
+3
View File
@@ -21,6 +21,9 @@ type InspectReq struct {
} }
type ContainerInfo struct { type ContainerInfo struct {
LatestVersion string `json:"latestVersion"`
TargetImage string `json:"targetImage"`
HasUpdate bool `json:"hasUpdate"`
ContainerID string `json:"containerID"` ContainerID string `json:"containerID"`
Name string `json:"name"` Name string `json:"name"`
ImageId string `json:"imageID"` ImageId string `json:"imageID"`
+6
View File
@@ -2,6 +2,12 @@ package dto
import "time" import "time"
type ImageRemoteSearch struct {
Query string `json:"query" validate:"required,max=256"`
Page int `json:"page" validate:"required,min=1"`
PageSize int `json:"pageSize" validate:"required,min=1,max=100"`
}
type PageImage struct { type PageImage struct {
PageInfo PageInfo
Name string `json:"name"` Name string `json:"name"`
+143
View File
@@ -0,0 +1,143 @@
package dto
type ModelSearch struct {
Query string `json:"query"`
Sort string `json:"sort" validate:"omitempty,oneof=trending downloads updated created likes most_params least_params"`
Page int `json:"page" validate:"omitempty,min=1"`
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=100"`
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
}
type ModelInfoReq struct {
RepoID string `json:"repoID" validate:"required"`
Revision string `json:"revision"`
}
type ModelDownload struct {
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
RepoID string `json:"repoID" validate:"required"`
Revision string `json:"revision"`
}
type ModelDownloaderSyncSetting struct {
Value string `json:"value" validate:"required,oneof=Enable Disable"`
}
type ModelDownloaderSetting struct {
FeaturedModelsSync string `json:"featuredModelsSync"`
ModelDir string `json:"modelDir" validate:"required"`
HFEndpoint string `json:"hfEndpoint"`
HFToken string `json:"hfToken"`
ModelScopeEndpoint string `json:"modelScopeEndpoint"`
ModelScopeToken string `json:"modelScopeToken"`
}
type ModelItem struct {
RepoID string `json:"repoID"`
Name string `json:"name"`
Downloads int64 `json:"downloads"`
Likes int64 `json:"likes"`
Size int64 `json:"size"`
SizeFormatted string `json:"sizeFormatted"`
}
type ModelFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
SizeFormatted string `json:"sizeFormatted"`
}
type ModelInfo struct {
ModelItem
ModelCard string `json:"modelCard"`
Files []ModelFile `json:"files"`
}
type ModelDownloadTaskSearch struct {
IncludeLocal bool `json:"includeLocal"`
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
PageInfo
Info string `json:"info"`
Status string `json:"status"`
}
type ModelDownloadTaskOperate struct {
WithFile bool `json:"withFile"`
ID uint `json:"id" validate:"required"`
}
type ModelDownloadTaskRetry struct {
ID uint `json:"id" validate:"required"`
}
type ModelDownloadTaskItem struct {
LocalModel *LocalModelItem `json:"localModel,omitempty"`
DownloadType string `json:"downloadType"`
ID uint `json:"id"`
Source string `json:"source"`
RepoID string `json:"repoID"`
Revision string `json:"revision"`
ModelName string `json:"modelName"`
TargetDir string `json:"targetDir"`
Status string `json:"status"`
Progress float64 `json:"progress"`
TotalSize int64 `json:"totalSize"`
DownloadedSize int64 `json:"downloadedSize"`
ErrorMessage string `json:"errorMessage"`
CreatedAt string `json:"createdAt"`
StartedAt string `json:"startedAt"`
CompletedAt string `json:"completedAt"`
CanCancel bool `json:"canCancel"`
CanRetry bool `json:"canRetry"`
CanRemoveRecord bool `json:"canRemoveRecord"`
}
type LocalModelSearch struct {
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
PageInfo
Info string `json:"info"`
}
type LocalModelItem struct {
DownloadType string `json:"downloadType"`
RepoID string `json:"repoID"`
Source string `json:"source"`
Revision string `json:"revision"`
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
SizeFormatted string `json:"sizeFormatted"`
CreatedAt string `json:"createdAt"`
}
type LocalModelDelete struct {
WithFile bool `json:"withFile"`
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
Name string `json:"name" validate:"required"`
}
type FeaturedModel struct {
Key string `json:"key" yaml:"key"`
Sort int `json:"sort" yaml:"sort"`
Family string `json:"family" yaml:"category"`
RepoID string `json:"repoID" yaml:"name"`
Sources []string `json:"sources" yaml:"sources"`
SourceModelIDs map[string]string `json:"sourceModelIds" yaml:"sourceModelIds"`
Architecture string `json:"architecture" yaml:"modelType"`
BackboneParams string `json:"backboneParams" yaml:"moeBackboneParameters"`
ActiveParams string `json:"activeParams" yaml:"activeParameters"`
Engram string `json:"engram" yaml:"engramParameters"`
WeightGB float64 `json:"weightGB" yaml:"approximateWeightSizeGB"`
Description map[string]string `json:"description" yaml:"description"`
}
type FeaturedModelSearch struct {
PageInfo
Family string `json:"family"`
Info string `json:"info"`
}
type FeaturedModelPage struct {
PageResult
Families []string `json:"families"`
}
+1
View File
@@ -11,4 +11,5 @@ type MCPWebsiteBinding struct {
Upstream string `json:"upstream"` Upstream string `json:"upstream"`
CoreHost string `json:"coreHost"` CoreHost string `json:"coreHost"`
ProxyKey string `json:"proxyKey"` ProxyKey string `json:"proxyKey"`
DeleteWebsite bool `json:"deleteWebsite,omitempty"`
} }
+4 -1
View File
@@ -8,6 +8,7 @@ type TensorRTLLMSearch struct {
} }
type TensorRTLLMCreate struct { type TensorRTLLMCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
ContainerName string `json:"containerName" validate:"required"` ContainerName string `json:"containerName" validate:"required"`
Version string `json:"version" validate:"required"` Version string `json:"version" validate:"required"`
@@ -27,11 +28,13 @@ type TensorRTLLMUpdate struct {
type TensorRTLLMDelete struct { type TensorRTLLMDelete struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
TaskID string `json:"taskID"`
} }
type TensorRTLLMOperate struct { type TensorRTLLMOperate struct {
TaskID string `json:"taskID"`
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required,oneof=start stop restart"`
} }
type DockerConfig struct { type DockerConfig struct {
+2
View File
@@ -51,6 +51,7 @@ type TagDTO struct {
} }
type AppInstalledCheck struct { type AppInstalledCheck struct {
TaskID string `json:"taskID"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"` ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"` ModelAccountBaseURL string `json:"modelAccountBaseURL"`
IsExist bool `json:"isExist"` IsExist bool `json:"isExist"`
@@ -107,6 +108,7 @@ type AppDetail struct {
} }
type AppInstallDTO struct { type AppInstallDTO struct {
TaskID string `json:"taskID"`
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
+1
View File
@@ -14,4 +14,5 @@ type MCPWebsiteBinding struct {
Code string `json:"code,omitempty"` Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Definitive bool `json:"definitive,omitempty"` Definitive bool `json:"definitive,omitempty"`
WebsiteDeleted bool `json:"websiteDeleted,omitempty"`
} }
+1
View File
@@ -12,6 +12,7 @@ type TensorRTLLMsRes struct {
} }
type TensorRTLLMDTO struct { type TensorRTLLMDTO struct {
TaskID string `json:"taskID"`
model.TensorRTLLM model.TensorRTLLM
Version string `json:"version"` Version string `json:"version"`
Model string `json:"model"` Model string `json:"model"`
+447
View File
@@ -0,0 +1,447 @@
package dto
import (
"time"
)
type VMSearchResult struct {
PageResult
Limit int `json:"limit"` // Maximum VM count for the target node; 0 means unlimited.
}
type VMCheckItem struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Message string `json:"message"`
IsEnable bool `json:"isEnable"`
IsExist bool `json:"isExist"`
IsActive bool `json:"isActive"`
}
type VMEnvironment struct {
Arch string `json:"arch"`
ArchSupported bool `json:"archSupported"`
Ready bool `json:"ready"`
HelperExist bool `json:"helperExist"`
HelperMessage string `json:"helperMessage"`
KVMExist bool `json:"kvmExist"`
KVMMessage string `json:"kvmMessage"`
LibvirtExist bool `json:"libvirtExist"`
LibvirtActive bool `json:"libvirtActive"`
LibvirtMessage string `json:"libvirtMessage"`
StorageExist bool `json:"storageExist"`
StorageMessage string `json:"storageMessage"`
}
type VMHealthCheckItem struct {
Level string `json:"level"`
Check string `json:"check"`
Reason string `json:"reason,omitempty"`
Params map[string]string `json:"params,omitempty"`
Detail string `json:"detail,omitempty"`
FixCommand string `json:"fixCommand,omitempty"`
}
type VMHealthCheckResult struct {
Pass int `json:"pass"`
Warn int `json:"warn"`
Fail int `json:"fail"`
Items []VMHealthCheckItem `json:"items"`
}
type VMCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
CPU int `json:"cpu" validate:"required,min=1"`
Memory int64 `json:"memory" validate:"required,min=1"`
DiskSize int64 `json:"diskSize" validate:"required,min=1"`
OSType string `json:"osType" validate:"omitempty,oneof=windows linux other"`
DiskBus string `json:"diskBus" validate:"omitempty,oneof=sata virtio"`
TemplateID uint `json:"templateID"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths"`
ImagePath string `json:"imagePath"`
Network string `json:"network" validate:"required,vm_common"`
BootOrder string `json:"bootOrder"`
StartAfterCreate bool `json:"startAfterCreate"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMTemplateCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
TemplateName string `json:"templateName" validate:"required,vm_name"`
StoragePool string `json:"storagePool"`
Description string `json:"description"`
}
type VMTemplateDelete struct {
ID uint `json:"id" validate:"required"`
DeleteDisk bool `json:"deleteDisk"`
TaskID string `json:"taskID"`
}
type VMTemplateInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
SourceVM string `json:"sourceVM"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
IsExist bool `json:"isExist"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
Description string `json:"description"`
}
type VMUpdate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
CPU int `json:"cpu" validate:"required,min=1"`
Memory int64 `json:"memory" validate:"required,min=1"`
ISOPaths []string `json:"isoPaths"`
Network string `json:"network" validate:"required,vm_common"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMDiskResize struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
Target string `json:"target" validate:"required,vm_common"`
SizeGiB int64 `json:"sizeGiB" validate:"required,min=1,max=8388607"`
}
type VMRename struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
NewName string `json:"newName" validate:"required,vm_name"`
}
type VMDelete struct {
TaskID string `json:"taskID"`
ID uint `json:"id" validate:"required"`
Force bool `json:"force"`
}
type VMOperate struct {
Name string `json:"name" validate:"required,vm_name"`
Operate string `json:"operate" validate:"required,oneof=start shutdown reboot destroy suspend resume"`
}
type VMInfoReq struct {
Name string `json:"name" validate:"required,vm_name"`
CheckDisks bool `json:"checkDisks"`
}
type VMReq struct {
Name string `json:"name" validate:"required,vm_name"`
}
type VMTaskResp struct {
TaskID string `json:"taskID"`
}
type VMOrphanItem struct {
Type string `json:"type" validate:"required,oneof=orphan_domain_disk orphan_snapshot partial_file"`
Path string `json:"path" validate:"required"`
Size int64 `json:"size"`
ModifiedAt int64 `json:"modifiedAt"`
}
type VMOrphanClean struct {
Items []VMOrphanItem `json:"items" validate:"required,min=1,dive"`
}
type VMSnapshotReq struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
SnapshotName string `json:"snapshotName" validate:"required,vm_name"`
Description string `json:"description"`
}
type VMSnapshotSearch struct {
Name string `json:"name" validate:"required,vm_name"`
}
type VMSnapshotInfo struct {
Name string `json:"name"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
State string `json:"state"`
Parent string `json:"parent"`
Path string `json:"path"`
Size int64 `json:"size"`
Current bool `json:"current"`
Children int `json:"children"`
}
type VMIsoSearch struct {
PageInfo
Type string `json:"type" validate:"required"`
Info string `json:"info"`
}
type VMIsoCreate struct {
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Description string `json:"description"`
}
type VMIsoUpdate struct {
ID uint `json:"id" validate:"required"`
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Description string `json:"description"`
}
type VMIsoImportStart struct {
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Status string `json:"status" validate:"required,oneof=uploading downloading"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"`
Description string `json:"description"`
}
type VMIsoImportUpdate struct {
ID uint `json:"id" validate:"required"`
Status string `json:"status" validate:"required,oneof=uploading downloading ready failed"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"`
}
type VMIsoDelete struct {
ID uint `json:"id" validate:"required"`
DeleteFile bool `json:"deleteFile"`
}
type VMIsoInfo struct {
ID uint `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
IsExist bool `json:"isExist"`
Size int64 `json:"size"`
Status string `json:"status"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType"`
CreatedAt time.Time `json:"createdAt"`
Description string `json:"description"`
}
type VMResourceInfo struct {
Type string `json:"type"`
Name string `json:"name"`
SubType string `json:"subType"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
Path string `json:"path"`
Status string `json:"status"`
AutoStart bool `json:"autoStart"`
Capacity uint64 `json:"capacity"`
Available uint64 `json:"available"`
}
type VMResourceDelete struct {
ID uint `json:"id" validate:"required"`
Force bool `json:"force"`
}
type VMNetworkCreate struct {
Name string `json:"name" validate:"required,vm_common"`
Type string `json:"type" validate:"required,oneof=nat bridge macvtap"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface" validate:"required_if=Type macvtap"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMNetworkUpdate struct {
ID uint `json:"id" validate:"required"`
AutoStart *bool `json:"autoStart"`
Status *string `json:"status"`
Description *string `json:"description"`
}
type VMNetworkItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
Status string `json:"status"`
VMCount int `json:"vmCount"`
VMs []VMResourceRef `json:"vms"`
Registered bool `json:"registered"`
UsedIPs []VMNetworkIPUsage `json:"usedIPs"`
Error string `json:"error"`
}
type VMResourceRef struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type VMNetworkIPUsage struct {
IP string `json:"ip"`
VMName string `json:"vmName"`
MAC string `json:"mac"`
Source string `json:"source"`
}
type VMNetworkInterfaceItem struct {
Name string `json:"name"`
Addresses []string `json:"addresses"`
}
type VMNetworkBridgeItem struct {
Name string `json:"name"`
Recommended bool `json:"recommended"`
Description string `json:"description"`
}
type VMStorageCreate struct {
Name string `json:"name" validate:"required,vm_common"`
Type string `json:"type" validate:"required,oneof=dir"`
Path string `json:"path" validate:"required"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMStorageUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"omitempty,vm_common"`
Path string `json:"path"`
AutoStart *bool `json:"autoStart"`
Status *string `json:"status"`
Description *string `json:"description"`
}
type VMStorageItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
Status string `json:"status"`
Capacity uint64 `json:"capacity"`
Available uint64 `json:"available"`
VMCount int `json:"vmCount"`
VMs []VMResourceRef `json:"vms"`
Error string `json:"error"`
}
type VMInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
UUID string `json:"uuid"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths"`
ImagePath string `json:"imagePath"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Disks []VMDisk `json:"disks"`
Interfaces []VMInterface `json:"interfaces"`
Graphics *VMGraphics `json:"graphics"`
TaskID string `json:"taskID"`
CreateTaskID string `json:"createTaskID"`
Description string `json:"description"`
}
type VMDisk struct {
Capacity int64 `json:"capacity"`
Format string `json:"format"`
ReadOnly bool `json:"readOnly"`
Shareable bool `json:"shareable"`
ResizeBlocked string `json:"resizeBlocked"`
Device string `json:"device"`
Type string `json:"type"`
Source string `json:"source"`
Target string `json:"target"`
Bus string `json:"bus"`
}
type VMInterface struct {
Type string `json:"type"`
Source string `json:"source"`
Model string `json:"model"`
MAC string `json:"mac"`
IP string `json:"ip"`
}
type VMGraphics struct {
Type string `json:"type"`
Port string `json:"port"`
Listen string `json:"listen"`
}
type VMStats struct {
Name string `json:"name"`
Status string `json:"status"`
Timestamp int64 `json:"timestamp"`
CPUTime uint64 `json:"cpuTime"`
VCPU int `json:"vcpu"`
Memory VMMemoryStats `json:"memory"`
Disk VMDiskStats `json:"disk"`
Network VMNetworkStats `json:"network"`
}
type VMMemoryStats struct {
AvailableKiB uint64 `json:"availableKiB"`
UnusedKiB uint64 `json:"unusedKiB"`
UsableKiB uint64 `json:"usableKiB"`
RSSKiB uint64 `json:"rssKiB"`
ActualKiB uint64 `json:"actualKiB"`
}
type VMDiskStats struct {
ReadBytes uint64 `json:"readBytes"`
WriteBytes uint64 `json:"writeBytes"`
ReadReqs uint64 `json:"readReqs"`
WriteReqs uint64 `json:"writeReqs"`
}
type VMNetworkStats struct {
RxBytes uint64 `json:"rxBytes"`
TxBytes uint64 `json:"txBytes"`
RxPackets uint64 `json:"rxPackets"`
TxPackets uint64 `json:"txPackets"`
}
+1
View File
@@ -17,6 +17,7 @@ type AppInstall struct {
Param string `json:"param"` Param string `json:"param"`
Env string `json:"env"` Env string `json:"env"`
DockerCompose string `json:"dockerCompose" ` DockerCompose string `json:"dockerCompose" `
TaskID string `json:"taskID" gorm:"-"`
Status string `json:"status" gorm:"not null"` Status string `json:"status" gorm:"not null"`
Description string `json:"description"` Description string `json:"description"`
Message string `json:"message"` Message string `json:"message"`
@@ -0,0 +1,20 @@
package model
import "time"
// ContainerImageCheckUpdate is a cache tied to the exact image used by a container.
type ContainerImageCheckUpdate struct {
ContainerID string `gorm:"primaryKey" json:"containerID"`
ImageID string `json:"imageID"`
ImageName string `json:"imageName"`
LatestVersion string `json:"latestVersion"`
TargetImage string `json:"targetImage"`
TargetImageID string `json:"-"`
HasUpdate bool `gorm:"not null;default:false" json:"hasUpdate"`
CheckedAt time.Time `json:"checkedAt"`
CheckStatus string `json:"checkStatus"`
CheckError string `json:"checkError"`
}
// TableName preserves the table created by the original migration.
func (ContainerImageCheckUpdate) TableName() string { return "container_image_updates" }
+17
View File
@@ -0,0 +1,17 @@
package model
type FeaturedModel struct {
BaseModel
Key string `json:"key"`
Sort int `json:"sort"`
Family string `json:"family"`
RepoID string `json:"repoID"`
Sources []string `json:"sources" gorm:"serializer:json"`
SourceModelIDs map[string]string `json:"sourceModelIds" gorm:"serializer:json"`
Architecture string `json:"architecture"`
BackboneParams string `json:"backboneParams"`
ActiveParams string `json:"activeParams"`
Engram string `json:"engram"`
WeightGB float64 `json:"weightGB"`
Description string `json:"description"`
}
+20
View File
@@ -0,0 +1,20 @@
package model
import "time"
type ModelDownloadTask struct {
DownloadType string `json:"downloadType"`
BaseModel
Source string `json:"source" gorm:"type:varchar(32);not null;index"`
RepoID string `json:"repoID" gorm:"type:varchar(255);not null;index"`
Revision string `json:"revision" gorm:"type:varchar(128)"`
ModelName string `json:"modelName" gorm:"type:varchar(255);not null;index"`
TargetDir string `json:"targetDir" gorm:"type:text"`
Status string `json:"status" gorm:"type:varchar(32);not null;index"`
Progress float64 `json:"progress"`
TotalSize int64 `json:"totalSize"`
DownloadedSize int64 `json:"downloadedSize"`
ErrorMessage string `json:"errorMessage" gorm:"type:text"`
StartedAt *time.Time `json:"startedAt"`
CompletedAt *time.Time `json:"completedAt"`
}
+81
View File
@@ -0,0 +1,81 @@
package model
type VirtualMachine struct {
BaseModel
Name string `json:"name"`
UUID string `json:"uuid"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths" gorm:"serializer:json"`
ImagePath string `json:"imagePath"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
TaskID string `json:"taskID"`
CreateTaskID string `json:"createTaskID"`
Description string `json:"description"`
}
type VMTemplate struct {
BaseModel
Name string `json:"name"`
SourceVM string `json:"sourceVM"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
Description string `json:"description"`
}
type VMIso struct {
BaseModel
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
Status string `json:"status"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType"`
Description string `json:"description"`
}
type VMNetwork struct {
BaseModel
Name string `json:"name"`
Type string `json:"type"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr" gorm:"column:cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Description string `json:"description"`
Error string `json:"error"`
}
type VMStorage struct {
BaseModel
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Description string `json:"description"`
Error string `json:"error"`
}
+9 -2
View File
@@ -18,8 +18,9 @@ type IAgentAccountRepo interface {
Save(account *model.AgentAccount) error Save(account *model.AgentAccount) error
DeleteByID(id uint) error DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error) List(opts ...DBOption) ([]model.AgentAccount, error)
Count() (int64, error)
CountTextByProviders(providers []string) (map[string]int64, error) CountTextByProviders(providers []string) (map[string]int64, error)
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error
} }
func NewIAgentAccountRepo() IAgentAccountRepo { func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -69,6 +70,12 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
return accounts, nil return accounts, nil
} }
func (a AgentAccountRepo) Count() (int64, error) {
var count int64
err := getDb().Model(&model.AgentAccount{}).Count(&count).Error
return count, err
}
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) { func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
normalizedProviders := normalizeProviders(providers) normalizedProviders := normalizeProviders(providers)
counts := make(map[string]int64, len(normalizedProviders)) counts := make(map[string]int64, len(normalizedProviders))
@@ -99,7 +106,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
return counts, nil return counts, nil
} }
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error { func (a AgentAccountRepo) SaveWithModels(account *model.AgentAccount, models []model.AgentAccountModel) error {
return global.DB.Transaction(func(tx *gorm.DB) error { return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(account).Error; err != nil { if err := tx.Save(account).Error; err != nil {
return err return err
+6
View File
@@ -31,6 +31,7 @@ type IAppInstallRepo interface {
GetFirst(opts ...DBOption) (model.AppInstall, error) GetFirst(opts ...DBOption) (model.AppInstall, error)
Create(ctx context.Context, install *model.AppInstall) error Create(ctx context.Context, install *model.AppInstall) error
Save(ctx context.Context, install *model.AppInstall) error Save(ctx context.Context, install *model.AppInstall) error
UpdateStatus(install *model.AppInstall) error
DeleteBy(opts ...DBOption) error DeleteBy(opts ...DBOption) error
Delete(ctx context.Context, install model.AppInstall) error Delete(ctx context.Context, install model.AppInstall) error
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
@@ -45,6 +46,11 @@ func NewIAppInstallRepo() IAppInstallRepo {
return &AppInstallRepo{} return &AppInstallRepo{}
} }
func (a *AppInstallRepo) UpdateStatus(install *model.AppInstall) error {
return getDb().Model(&model.AppInstall{}).Where("id = ? AND updated_at = ?", install.ID, install.UpdatedAt).
Updates(map[string]interface{}{"status": install.Status, "message": install.Message}).Error
}
func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption { func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("app_detail_id in (?)", detailIds) return g.Where("app_detail_id in (?)", detailIds)
+1 -1
View File
@@ -86,10 +86,10 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
} }
if !withAll && len(quicks) == 0 { if !withAll && len(quicks) == 0 {
return []model.QuickJump{ return []model.QuickJump{
{Name: "ModelServices", Title: "aiTools.model.model", Recommend: 0, IsShow: true, Router: "/ai/model"},
{Name: "Agent", Title: "aiTools.agents.agent", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"}, {Name: "Agent", Title: "aiTools.agents.agent", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"},
{Name: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"}, {Name: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
{Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"}, {Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"},
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: false, Router: "/cronjobs"},
{Name: "AppInstalled", Title: "home.appInstalled", Recommend: 70, IsShow: true, Router: "/apps/installed"}, {Name: "AppInstalled", Title: "home.appInstalled", Recommend: 70, IsShow: true, Router: "/apps/installed"},
} }
} }
+60
View File
@@ -0,0 +1,60 @@
package repo
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type FeaturedModelRepo struct{}
type IFeaturedModelRepo interface {
Page(page, size int, family, info string) (int64, []model.FeaturedModel, error)
Families() ([]string, error)
SyncAll(models []model.FeaturedModel, version string) error
}
func NewIFeaturedModelRepo() IFeaturedModelRepo {
return &FeaturedModelRepo{}
}
func (r *FeaturedModelRepo) Page(page, size int, family, info string) (int64, []model.FeaturedModel, error) {
var total int64
var items []model.FeaturedModel
db := global.DB.Model(&model.FeaturedModel{})
if family != "" {
db = db.Where("family = ?", family)
}
if info != "" {
db = db.Where("LOWER(repo_id) LIKE ?", "%"+strings.ToLower(info)+"%")
}
if err := db.Count(&total).Error; err != nil {
return 0, nil, err
}
if err := db.Order("sort asc").Order("id asc").Limit(size).Offset(size * (page - 1)).Find(&items).Error; err != nil {
return 0, nil, err
}
return total, items, nil
}
func (r *FeaturedModelRepo) Families() ([]string, error) {
families := make([]string, 0)
err := global.DB.Model(&model.FeaturedModel{}).Distinct("family").Order("family asc").Pluck("family", &families).Error
return families, err
}
func (r *FeaturedModelRepo) SyncAll(models []model.FeaturedModel, version string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("1 = 1").Delete(&model.FeaturedModel{}).Error; err != nil {
return err
}
if err := tx.Create(&models).Error; err != nil {
return err
}
return tx.Where("key = ?", "FeaturedModelsVersion").
Assign(model.Setting{Value: version}).
FirstOrCreate(&model.Setting{Key: "FeaturedModelsVersion"}).Error
})
}
+126
View File
@@ -0,0 +1,126 @@
package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type ModelDownloadTaskRepo struct{}
type IModelDownloadTaskRepo interface {
Page(page, size int, info, status string, opts ...DBOption) (int64, []model.ModelDownloadTask, error)
GetFirst(opts ...DBOption) (model.ModelDownloadTask, error)
List(opts ...DBOption) ([]model.ModelDownloadTask, error)
Create(task *model.ModelDownloadTask) error
Update(id uint, values map[string]interface{}) error
UpdateBy(values map[string]interface{}, opts ...DBOption) error
DeleteBy(opts ...DBOption) error
WithByDownloadType(downloadType string) DBOption
WithBySource(source string) DBOption
WithByRepoID(repoID string) DBOption
WithByTargetDir(targetDir string) DBOption
WithByStatusIn(status []string) DBOption
}
func NewIModelDownloadTaskRepo() IModelDownloadTaskRepo {
return &ModelDownloadTaskRepo{}
}
func (m *ModelDownloadTaskRepo) WithBySource(source string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("source = ?", source)
}
}
func (m *ModelDownloadTaskRepo) WithByRepoID(repoID string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("repo_id = ?", repoID)
}
}
func (m *ModelDownloadTaskRepo) WithByTargetDir(targetDir string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("target_dir = ?", targetDir)
}
}
func (m *ModelDownloadTaskRepo) WithByStatusIn(status []string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("status in ?", status)
}
}
func (m *ModelDownloadTaskRepo) Page(page, size int, info, status string, opts ...DBOption) (int64, []model.ModelDownloadTask, error) {
var (
total int64
list []model.ModelDownloadTask
)
db := global.DB.Model(&model.ModelDownloadTask{})
for _, opt := range opts {
db = opt(db)
}
if info != "" {
db = db.Where("repo_id LIKE ? OR model_name LIKE ?", "%"+info+"%", "%"+info+"%")
}
if status != "" {
db = db.Where("status = ?", status)
}
if err := db.Count(&total).Error; err != nil {
return 0, nil, err
}
if err := db.Order("created_at desc").Limit(size).Offset(size * (page - 1)).Find(&list).Error; err != nil {
return 0, nil, err
}
return total, list, nil
}
func (m *ModelDownloadTaskRepo) GetFirst(opts ...DBOption) (model.ModelDownloadTask, error) {
var item model.ModelDownloadTask
db := global.DB.Model(&model.ModelDownloadTask{})
for _, opt := range opts {
db = opt(db)
}
err := db.First(&item).Error
return item, err
}
func (m *ModelDownloadTaskRepo) Create(task *model.ModelDownloadTask) error {
return global.DB.Create(task).Error
}
func (m *ModelDownloadTaskRepo) List(opts ...DBOption) ([]model.ModelDownloadTask, error) {
var items []model.ModelDownloadTask
db := global.DB.Model(&model.ModelDownloadTask{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&items).Error
return items, err
}
func (m *ModelDownloadTaskRepo) Update(id uint, values map[string]interface{}) error {
return global.DB.Model(&model.ModelDownloadTask{}).Where("id = ?", id).Updates(values).Error
}
func (m *ModelDownloadTaskRepo) UpdateBy(values map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.ModelDownloadTask{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(values).Error
}
func (m *ModelDownloadTaskRepo) DeleteBy(opts ...DBOption) error {
db := global.DB.Model(&model.ModelDownloadTask{})
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.ModelDownloadTask{}).Error
}
func (m *ModelDownloadTaskRepo) WithByDownloadType(downloadType string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("download_type = ?", downloadType)
}
}
+7
View File
@@ -17,6 +17,7 @@ type TaskRepo struct {
type ITaskRepo interface { type ITaskRepo interface {
Save(ctx context.Context, task *model.Task) error Save(ctx context.Context, task *model.Task) error
GetFirst(opts ...DBOption) (model.Task, error) GetFirst(opts ...DBOption) (model.Task, error)
GetExecutingTask(scope string, resourceID uint) (model.Task, error)
Page(page, size int, opts ...DBOption) (int64, []model.Task, error) Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
Update(ctx context.Context, task *model.Task) error Update(ctx context.Context, task *model.Task) error
UpdateRunningTaskToFailed(opts ...DBOption) error UpdateRunningTaskToFailed(opts ...DBOption) error
@@ -98,6 +99,12 @@ func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
return task, nil return task, nil
} }
func (t TaskRepo) GetExecutingTask(scope string, resourceID uint) (model.Task, error) {
var item model.Task
err := getTaskDb(WithByType(scope), t.WithResourceID(resourceID), t.WithByStatus(constant.StatusExecuting)).Order("created_at DESC").Limit(1).Find(&item).Error
return item, err
}
func (t TaskRepo) Page(page, size int, opts ...DBOption) (int64, []model.Task, error) { func (t TaskRepo) Page(page, size int, opts ...DBOption) (int64, []model.Task, error) {
var tasks []model.Task var tasks []model.Task
db := getTaskDb(opts...).Model(&model.Task{}) db := getTaskDb(opts...).Model(&model.Task{})
+315
View File
@@ -0,0 +1,315 @@
package repo
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VMRepo struct{}
type IVMRepo interface {
Get(opts ...DBOption) (model.VirtualMachine, error)
List() ([]model.VirtualMachine, error)
Count() (int64, error)
Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error)
Create(vm *model.VirtualMachine) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
WithByNetwork(network string) DBOption
WithByStorage(storage string) DBOption
GetTemplate(opts ...DBOption) (model.VMTemplate, error)
ListTemplates(opts ...DBOption) ([]model.VMTemplate, error)
CreateTemplate(item *model.VMTemplate) error
UpdateTemplateSource(oldName string, newName string) error
DeleteTemplate(opts ...DBOption) error
GetIso(opts ...DBOption) (model.VMIso, error)
PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error)
ListIsos(opts ...DBOption) ([]model.VMIso, error)
CreateIso(iso *model.VMIso) error
UpdateIso(id uint, vars map[string]interface{}) error
DeleteIso(opts ...DBOption) error
GetNetwork(opts ...DBOption) (model.VMNetwork, error)
PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error)
ListNetworks(opts ...DBOption) ([]model.VMNetwork, error)
CreateNetwork(network *model.VMNetwork) error
UpdateNetwork(id uint, vars map[string]interface{}) error
DeleteNetwork(opts ...DBOption) error
GetStorage(opts ...DBOption) (model.VMStorage, error)
PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error)
ListStorages(opts ...DBOption) ([]model.VMStorage, error)
CreateStorage(storage *model.VMStorage) error
UpdateStorage(id uint, vars map[string]interface{}) error
DeleteStorage(opts ...DBOption) error
}
func NewIVMRepo() IVMRepo {
return &VMRepo{}
}
func (v *VMRepo) Get(opts ...DBOption) (model.VirtualMachine, error) {
var vm model.VirtualMachine
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&vm).Error
return vm, err
}
func (v *VMRepo) List() ([]model.VirtualMachine, error) {
var vms []model.VirtualMachine
err := global.DB.Order("created_at desc").Find(&vms).Error
return vms, err
}
func (v *VMRepo) Count() (int64, error) {
var count int64
err := global.DB.Model(&model.VirtualMachine{}).Count(&count).Error
return count, err
}
func (v *VMRepo) Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error) {
var vms []model.VirtualMachine
var total int64
db := global.DB.Model(&model.VirtualMachine{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&vms).Error
return total, vms, err
}
func (v *VMRepo) Create(vm *model.VirtualMachine) error {
return global.DB.Create(vm).Error
}
func (v *VMRepo) Update(id uint, vars map[string]interface{}) error {
if paths, ok := vars["iso_paths"].([]string); ok {
data, err := json.Marshal(paths)
if err != nil {
return err
}
vars["iso_paths"] = string(data)
}
return global.DB.Model(&model.VirtualMachine{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VirtualMachine{}).Error
}
func (v *VMRepo) WithByNetwork(network string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("network = ?", network)
}
}
func (v *VMRepo) WithByStorage(storage string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("storage_pool = ?", storage)
}
}
func (v *VMRepo) GetTemplate(opts ...DBOption) (model.VMTemplate, error) {
var item model.VMTemplate
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&item).Error
return item, err
}
func (v *VMRepo) ListTemplates(opts ...DBOption) ([]model.VMTemplate, error) {
var items []model.VMTemplate
db := global.DB.Model(&model.VMTemplate{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&items).Error
return items, err
}
func (v *VMRepo) CreateTemplate(item *model.VMTemplate) error {
return global.DB.Create(item).Error
}
func (v *VMRepo) UpdateTemplateSource(oldName string, newName string) error {
return global.DB.Model(&model.VMTemplate{}).Where("source_vm = ?", oldName).Update("source_vm", newName).Error
}
func (v *VMRepo) DeleteTemplate(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMTemplate{}).Error
}
func (v *VMRepo) GetIso(opts ...DBOption) (model.VMIso, error) {
var iso model.VMIso
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&iso).Error
return iso, err
}
func (v *VMRepo) PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error) {
var isos []model.VMIso
var total int64
db := global.DB.Model(&model.VMIso{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&isos).Error
return total, isos, err
}
func (v *VMRepo) ListIsos(opts ...DBOption) ([]model.VMIso, error) {
var isos []model.VMIso
db := global.DB.Model(&model.VMIso{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&isos).Error
return isos, err
}
func (v *VMRepo) CreateIso(iso *model.VMIso) error {
return global.DB.Create(iso).Error
}
func (v *VMRepo) UpdateIso(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMIso{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteIso(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMIso{}).Error
}
func (v *VMRepo) GetNetwork(opts ...DBOption) (model.VMNetwork, error) {
var network model.VMNetwork
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&network).Error
return network, err
}
func (v *VMRepo) PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error) {
var networks []model.VMNetwork
var total int64
db := global.DB.Model(&model.VMNetwork{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&networks).Error
return total, networks, err
}
func (v *VMRepo) ListNetworks(opts ...DBOption) ([]model.VMNetwork, error) {
var networks []model.VMNetwork
db := global.DB.Model(&model.VMNetwork{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&networks).Error
return networks, err
}
func (v *VMRepo) CreateNetwork(network *model.VMNetwork) error {
return global.DB.Create(network).Error
}
func (v *VMRepo) UpdateNetwork(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMNetwork{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteNetwork(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMNetwork{}).Error
}
func (v *VMRepo) GetStorage(opts ...DBOption) (model.VMStorage, error) {
var storage model.VMStorage
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&storage).Error
return storage, err
}
func (v *VMRepo) PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error) {
var storages []model.VMStorage
var total int64
db := global.DB.Model(&model.VMStorage{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&storages).Error
return total, storages, err
}
func (v *VMRepo) ListStorages(opts ...DBOption) ([]model.VMStorage, error) {
var storages []model.VMStorage
db := global.DB.Model(&model.VMStorage{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&storages).Error
return storages, err
}
func (v *VMRepo) CreateStorage(storage *model.VMStorage) error {
return global.DB.Create(storage).Error
}
func (v *VMRepo) UpdateStorage(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMStorage{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteStorage(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMStorage{}).Error
}
+100 -1
View File
@@ -1105,10 +1105,24 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if err != nil { if err != nil {
return err return err
} }
if req.Models != nil {
if account.AppInstallID != 0 {
return buserr.WithDetail("ErrInvalidParams", "local model pool is managed by the deployment", nil)
}
models, err = normalizeAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := ensureAccountModelsNotBound(account, models); err != nil {
return err
}
}
requestedVerifyModel := req.VerifyModel requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" { if strings.TrimSpace(requestedVerifyModel) == "" {
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
requestedVerifyModel = account.VerifyModel requestedVerifyModel = account.VerifyModel
} }
}
verifyModel := "" verifyModel := ""
if account.AppInstallID == 0 || len(models) > 0 { if account.AppInstallID == 0 || len(models) > 0 {
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models) verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
@@ -1131,7 +1145,16 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.Remark = req.Remark account.Remark = req.Remark
account.Verified = true account.Verified = true
if err := agentAccountRepo.Save(account); err != nil { if req.Models != nil {
rows := make([]model.AgentAccountModel, 0, len(models))
for _, item := range models {
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
}
err = agentAccountRepo.SaveWithModels(account, rows)
} else {
err = agentAccountRepo.Save(account)
}
if err != nil {
return err return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
@@ -1275,6 +1298,31 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
} }
if account.Provider == "ollama" {
if account.AppInstallID != 0 {
install, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "ollama" || !install.SyncModelAccount {
return buserr.New("ErrAgentAccountUnavailable")
}
localModels, err := aiRepo.List()
if err != nil {
return err
}
for _, item := range localModels {
if sameProviderModelID("ollama", item.Name, nextModel.ID) {
return (&AIToolService{}).Recreate(dto.OllamaModelName{Name: item.Name, TaskID: req.TaskID})
}
}
return (&AIToolService{}).Create(dto.OllamaModelName{Name: nextModel.ID, TaskID: req.TaskID})
}
nextModel.ID, err = resolveExistingOllamaModel(account, nextModel.ID)
if err != nil {
return err
}
}
sortOrder := len(models) + 1 sortOrder := len(models) + 1
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: account.ID, AccountID: account.ID,
@@ -1305,6 +1353,15 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil { if err != nil {
return err return err
} }
if account.Provider == "ollama" && !sameProviderModelID("ollama", record.Model, nextModel.ID) {
if account.AppInstallID != 0 {
return buserr.New("ErrInvalidParams")
}
nextModel.ID, err = resolveExistingOllamaModel(account, nextModel.ID)
if err != nil {
return err
}
}
nextModel.RecordID = req.Model.RecordID nextModel.RecordID = req.Model.RecordID
for _, item := range models { for _, item := range models {
if item.RecordID == req.Model.RecordID { if item.RecordID == req.Model.RecordID {
@@ -1343,6 +1400,10 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
} }
func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error { func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error {
ollamaModelMu.Lock()
defer ollamaModelMu.Unlock()
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil { if err != nil {
return err return err
@@ -1368,6 +1429,44 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil { if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
return err return err
} }
if req.DeleteFiles && account.Provider != "ollama" {
return buserr.New("ErrAgentProviderNotSupported")
}
if req.DeleteFiles {
if account.AppInstallID != 0 {
install, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "ollama" {
return buserr.New("ErrAgentAccountUnavailable")
}
localModels, err := aiRepo.List()
if err != nil {
return err
}
localIDs := make([]uint, 0)
for _, item := range localModels {
if !sameProviderModelID("ollama", item.Name, record.Model) {
continue
}
if item.Status == constant.StatusWaiting {
return buserr.New("TaskIsExecuting")
}
localIDs = append(localIDs, item.ID)
}
if err := cmd.NewCommandMgr().Run("docker", "exec", install.ContainerName, "ollama", "rm", record.Model); err != nil {
return err
}
if len(localIDs) > 0 {
if err := aiRepo.Delete(repo.WithByIDs(localIDs)); err != nil {
return err
}
}
} else if err := deleteRemoteOllamaModel(account, record.Model); err != nil {
return err
}
}
if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil { if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil {
return err return err
} }
+99 -1
View File
@@ -1,13 +1,19 @@
package service package service
import ( import (
"bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/app/repo" "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/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai" terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
@@ -88,7 +94,7 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
} }
} }
if modelsChanged { if modelsChanged {
err = agentAccountRepo.SaveLocal(account, rows) err = agentAccountRepo.SaveWithModels(account, rows)
} else if accountChanged { } else if accountChanged {
err = agentAccountRepo.Save(account) err = agentAccountRepo.Save(account)
} else { } else {
@@ -136,6 +142,62 @@ func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBase
return err return err
} }
func updateOllamaModelPool(modelName string, remove bool) error {
install, err := appInstallRepo.LoadInstallAppByKeyAndName("ollama", "")
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if !install.SyncModelAccount {
return nil
}
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
}
if remove {
for _, item := range models {
if !sameProviderModelID("ollama", item.ID, modelName) {
continue
}
if err := agentAccountModelRepo.DeleteByID(item.RecordID); err != nil {
return err
}
}
if sameProviderModelID("ollama", account.VerifyModel, modelName) {
account.VerifyModel = ""
if err := agentAccountRepo.Save(account); err != nil {
return err
}
}
if err := compactPersistedAgentAccountModelSortOrder(account.ID); err != nil {
return err
}
} else {
if _, exists := findAgentAccountModelForProvider("ollama", models, modelName); exists {
return nil
}
record := &model.AgentAccountModel{AccountID: account.ID, Model: modelName, Name: modelName, SortOrder: len(models) + 1}
if err := agentAccountModelRepo.Create(record); err != nil {
return err
}
}
terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache()
return nil
}
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) { func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
addressType := install.ModelAccountBaseURLType addressType := install.ModelAccountBaseURLType
if addressType == "" { if addressType == "" {
@@ -200,3 +262,39 @@ func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, cont
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
} }
} }
func deleteRemoteOllamaModel(account *model.AgentAccount, modelName string) error {
body, err := json.Marshal(map[string]string{"model": modelName})
if err != nil {
return err
}
baseURL := strings.TrimSuffix(strings.TrimRight(account.BaseURL, "/"), "/v1")
req, err := http.NewRequest(http.MethodDelete, baseURL+"/api/delete", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+account.APIKey)
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("delete Ollama model failed: %s", resp.Status)
}
return nil
}
func resolveExistingOllamaModel(account *model.AgentAccount, modelID string) (string, error) {
available, err := providercatalog.DiscoverModels(account.BaseURL, account.APIKey)
if err != nil {
return "", buserr.WithErr("ErrAgentAccountUnavailable", err)
}
for _, id := range available {
if sameProviderModelID("ollama", id, modelID) {
return id, nil
}
}
return "", buserr.New("ErrRecordNotFound")
}
+9 -1
View File
@@ -1328,7 +1328,7 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
} }
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { if (account.Provider == "vllm" || account.Provider == "llmman") && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel") return nil, buserr.New("ErrAgentAccountSingleInitialModel")
} }
if len(requested) > 0 { if len(requested) > 0 {
@@ -1517,6 +1517,14 @@ func sameProviderModelID(provider, left, right string) bool {
} }
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed) leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed) rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
if provider == "ollama" {
if leftComparable != "" && !strings.Contains(path.Base(leftComparable), ":") {
leftComparable += ":latest"
}
if rightComparable != "" && !strings.Contains(path.Base(rightComparable), ":") {
rightComparable += ":latest"
}
}
return leftComparable != "" && leftComparable == rightComparable return leftComparable != "" && leftComparable == rightComparable
} }
+24 -18
View File
@@ -117,6 +117,7 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
} }
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, info.ID) taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, info.ID)
if err != nil { if err != nil {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("new task for exec shell failed, err: %v", err) global.LOG.Errorf("new task for exec shell failed, err: %v", err)
return err return err
} }
@@ -126,19 +127,17 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
}, nil) }, nil)
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
itemSize, err := loadModelSize(info.Name, containerName) itemSize, err := loadModelSize(info.Name, containerName)
if len(itemSize) != 0 { if err != nil {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize}) return err
} else {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
} }
return nil return aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error {
return updateOllamaModelPool(info.Name, false)
}, nil) }, nil)
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) _ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
} }
if err := SyncOllamaModelAccount(false); err != nil {
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
}
}() }()
return nil return nil
} }
@@ -167,6 +166,9 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
if modelInfo.ID == 0 { if modelInfo.ID == 0 {
return buserr.New("ErrRecordNotFound") return buserr.New("ErrRecordNotFound")
} }
if modelInfo.Status == constant.StatusWaiting {
return buserr.New("TaskIsExecuting")
}
containerName, err := LoadContainerName() containerName, err := LoadContainerName()
if err != nil { if err != nil {
return err return err
@@ -176,6 +178,7 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
} }
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, modelInfo.ID) taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, modelInfo.ID)
if err != nil { if err != nil {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("new task for exec shell failed, err: %v", err) global.LOG.Errorf("new task for exec shell failed, err: %v", err)
return err return err
} }
@@ -185,19 +188,17 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
}, nil) }, nil)
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
itemSize, err := loadModelSize(modelInfo.Name, containerName) itemSize, err := loadModelSize(modelInfo.Name, containerName)
if len(itemSize) != 0 { if err != nil {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize}) return err
} else {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
} }
return nil return aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("TaskSync"), func(t *task.Task) error {
return updateOllamaModelPool(modelInfo.Name, false)
}, nil) }, nil)
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) _ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
} }
if err := SyncOllamaModelAccount(false); err != nil {
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
}
}() }()
return nil return nil
} }
@@ -219,11 +220,16 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
return fmt.Errorf("handle ollama rm %s failed, %v", item.Name, err) return fmt.Errorf("handle ollama rm %s failed, %v", item.Name, err)
} }
} }
_ = aiRepo.Delete(repo.WithByID(item.ID)) if err := aiRepo.Delete(repo.WithByID(item.ID)); err != nil {
return err
}
if err := updateOllamaModelPool(item.Name, true); err != nil {
return err
}
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name) logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
_ = os.Remove(logItem) _ = os.Remove(logItem)
} }
return SyncOllamaModelAccount(false) return nil
} }
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error { func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
+5 -1
View File
@@ -642,7 +642,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
_ = appInstallRepo.Save(context.Background(), appInstall) _ = appInstallRepo.Save(context.Background(), appInstall)
} }
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour) installTimeout := time.Hour
if app.Key == "ollama" || app.Key == "vllm" {
installTimeout = 0
}
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, installTimeout)
go func() { go func() {
if taskErr := installTask.Execute(); taskErr != nil { if taskErr := installTask.Execute(); taskErr != nil {
@@ -223,6 +223,79 @@ func TestExplicitConfigComposeDetectsCustomAndReusesCurrentCache(t *testing.T) {
} }
} }
func TestRemovedCurrentComposeDoesNotBlockUpgrade(t *testing.T) {
for _, custom := range []bool{false, true} {
t.Run(fmt.Sprintf("custom=%v", custom), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/demo/1.0/docker-compose.yml" {
w.WriteHeader(http.StatusNotFound)
return
}
version := strings.Split(r.URL.Path, "/")[2]
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
}))
t.Cleanup(server.Close)
db, _, install, details := composeUpgradeDB(t, server.URL)
if err := db.Model(&details[1]).UpdateColumn("status", constant.AppTakeDown).Error; err != nil {
t.Fatal(err)
}
if custom {
install.DockerCompose += " environment:\n USER_SETTING: preserve\n"
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
t.Fatal(err)
}
}
service := NewIAppInstalledService()
config, err := service.GetParams(install.ID)
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || !config.IsEdit {
t.Fatalf("removed template must preserve the instance and warn about custom configuration: %+v, %v", config, err)
}
var current model.AppDetail
if err := db.First(&current, details[1].ID).Error; err != nil || current.DockerCompose != "" {
t.Fatalf("instance Compose was cached as the official template: %+v, %v", current, err)
}
noCompose := false
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
if err != nil || len(versions) != 2 {
t.Fatalf("removed current version blocked available versions: %+v, %v", versions, err)
}
for _, version := range versions {
previews, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: version.Version})
if err != nil {
t.Fatal(err)
}
for _, preview := range previews {
if preview.Version == version.Version && !strings.Contains(preview.DockerCompose, "demo:"+version.Version) {
t.Fatalf("upgrade did not use the selected new template: %+v", preview)
}
}
}
var stored model.AppInstall
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Version != install.Version || stored.Status != install.Status {
t.Fatalf("upgrade preview modified the running instance: %+v, %v", stored, err)
}
})
}
}
func TestRemovedCurrentComposeRejectsInvalidInstanceCompose(t *testing.T) {
for _, compose := range []string{"", "services: []", "services: ["} {
t.Run(compose, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(server.Close)
db, _, install, _ := composeUpgradeDB(t, server.URL)
if err := db.Model(&install).UpdateColumn("docker_compose", compose).Error; err != nil {
t.Fatal(err)
}
if config, err := NewIAppInstalledService().GetParams(install.ID); err == nil || config != nil {
t.Fatalf("invalid instance Compose was accepted: %+v, %v", config, err)
}
})
}
}
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) { func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
var calls atomic.Int32 var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+94 -32
View File
@@ -12,7 +12,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -123,7 +122,10 @@ func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []respo
} }
} }
installDTOs, _ := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate) installDTOs, err := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate)
if err != nil {
return 0, nil, err
}
if req.Update { if req.Update {
total = int64(len(installDTOs)) total = int64(len(installDTOs))
} }
@@ -168,6 +170,7 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
res.Version = appInstall.Version res.Version = appInstall.Version
res.CreatedAt = appInstall.CreatedAt res.CreatedAt = appInstall.CreatedAt
res.Status = appInstall.Status res.Status = appInstall.Status
res.TaskID = appInstall.TaskID
res.AppInstallID = appInstall.ID res.AppInstallID = appInstall.ID
res.IsExist = true res.IsExist = true
res.InstallPath = path.Join(global.Dir.AppInstallDir, appInstall.App.Key, appInstall.Name) res.InstallPath = path.Join(global.Dir.AppInstallDir, appInstall.App.Key, appInstall.Name)
@@ -256,6 +259,21 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
if err != nil { if err != nil {
return err return err
} }
if install.App.Key == "ollama" || install.App.Key == "vllm" {
if req.Operate != constant.Upgrade {
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting")
}
defer appUpgradeLocks.Delete(install.ID)
}
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, install.ID)
if err != nil {
return err
}
if operationTask.ID != "" && req.Operate != constant.Sync {
return buserr.New("TaskIsExecuting")
}
}
if install.Status == constant.StatusRestarting && (req.Operate == constant.Stop || req.Operate == constant.Delete) && !req.ForceDelete { if install.Status == constant.StatusRestarting && (req.Operate == constant.Stop || req.Operate == constant.Delete) && !req.ForceDelete {
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart)) count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
if err != nil { if err != nil {
@@ -269,8 +287,8 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return buserr.New("ErrInstallDirNotFound") return buserr.New("ErrInstallDirNotFound")
} }
dockerComposePath := install.GetComposePath() dockerComposePath := install.GetComposePath()
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) { if (req.UseLifecycleScripts || install.App.Key == "ollama" || install.App.Key == "vllm") && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
return operateAppWithLifecycleScripts(install, req, nil) return operateAppTask(install, req, nil)
} }
switch req.Operate { switch req.Operate {
case constant.Rebuild: case constant.Rebuild:
@@ -303,10 +321,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
TaskID: req.TaskID, TaskID: req.TaskID,
UseLifecycleScripts: req.UseLifecycleScripts, UseLifecycleScripts: req.UseLifecycleScripts,
} }
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete { return deleteAppInstall(deleteReq)
return err
}
return nil
case constant.Sync: case constant.Sync:
return syncAppInstallStatus(&install, true) return syncAppInstallStatus(&install, true)
case constant.Upgrade: case constant.Upgrade:
@@ -334,20 +349,25 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
} }
} }
func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error { func operateAppTask(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
taskType := task.TaskUpdate var taskType string
switch req.Operate { switch req.Operate {
case constant.Start: case constant.Start:
taskType = "Run"
install.Status = constant.StatusStarting install.Status = constant.StatusStarting
case constant.Restart: case constant.Restart:
taskType = task.TaskRestart taskType = task.TaskRestart
install.Status = constant.StatusRestarting install.Status = constant.StatusRestarting
case constant.Stop: case constant.Stop:
taskType = "Stop"
install.Status = constant.StatusWaiting install.Status = constant.StatusWaiting
default: default:
return errors.New("lifecycle script operation not supported") return errors.New("lifecycle script operation not supported")
} }
install.Message = "" install.Message = ""
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID) operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil { if err != nil {
@@ -358,20 +378,32 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
} }
return err return err
} }
ready := make(chan error, 1)
started := make(chan struct{})
operation := string(req.Operate) operation := string(req.Operate)
operationTask.AddSubTaskWithOps( operationTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, taskType, task.TaskScopeApp), task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
func(t *task.Task) error { func(t *task.Task) error {
if err := appInstallRepo.Save(context.Background(), &install); err != nil { if req.UseLifecycleScripts {
return err
}
close(started)
ready <- nil
if err := runScript(t, &install, operation); err != nil { if err := runScript(t, &install, operation); err != nil {
return err return err
} }
} else {
var out string
var err error
switch req.Operate {
case constant.Start:
out, err = compose.Up(install.GetComposePath())
case constant.Stop:
out, err = compose.Stop(install.GetComposePath())
case constant.Restart:
out, err = compose.Restart(install.GetComposePath())
}
if strings.TrimSpace(out) != "" {
t.Log(out)
}
if err != nil {
return err
}
}
if req.Operate == constant.Stop { if req.Operate == constant.Stop {
install.Status = constant.StatusStopped install.Status = constant.StatusStopped
install.Message = "" install.Message = ""
@@ -391,14 +423,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
}, },
nil, nil,
0, 0,
time.Hour, 0,
) )
go func() { go func() {
if taskErr := operationTask.Execute(); taskErr != nil { taskErr := operationTask.Execute()
select { if taskErr == nil {
case <-started:
default:
ready <- taskErr
return return
} }
if onFailure != nil { if onFailure != nil {
@@ -408,9 +437,8 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
install.Status = constant.StatusUpErr install.Status = constant.StatusUpErr
install.Message = taskErr.Error() install.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &install) _ = appInstallRepo.Save(context.Background(), &install)
}
}() }()
return <-ready return nil
} }
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error { func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
@@ -439,6 +467,19 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if err != nil { if err != nil {
return err return err
} }
if installed.App.Key == "ollama" || installed.App.Key == "vllm" {
if _, loaded := appUpgradeLocks.LoadOrStore(installed.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting")
}
defer appUpgradeLocks.Delete(installed.ID)
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, installed.ID)
if err != nil {
return err
}
if operationTask.ID != "" {
return buserr.New("TaskIsExecuting")
}
}
oldInstalled := installed oldInstalled := installed
port, ok := req.Params["PANEL_APP_PORT_HTTP"] port, ok := req.Params["PANEL_APP_PORT_HTTP"]
if ok { if ok {
@@ -578,7 +619,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
_ = appInstallRepo.Save(context.Background(), &failed) _ = appInstallRepo.Save(context.Background(), &failed)
} }
if req.UseLifecycleScripts { if req.UseLifecycleScripts {
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{ err = operateAppTask(installed, request.AppInstalledOperate{
InstallId: installed.ID, InstallId: installed.ID,
Operate: constant.Restart, Operate: constant.Restart,
TaskID: req.TaskID, TaskID: req.TaskID,
@@ -591,10 +632,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
restoreConfig(err) restoreConfig(err)
return err return err
} }
if !req.UseLifecycleScripts {
installed.Status = constant.StatusRunning
_ = appInstallRepo.Save(context.Background(), &installed)
}
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed) proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed) currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
@@ -1062,8 +1099,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
rawCompose := install.DockerCompose rawCompose := install.DockerCompose
if install.App.Key != vllmAppKeyForUpgrade { if install.App.Key != vllmAppKeyForUpgrade {
if err := ensureAppDetailCompose(install.App, &detail); err != nil { if err := ensureAppDetailCompose(install.App, &detail); err != nil {
var businessErr buserr.BusinessError
if !errors.As(err, &businessErr) || businessErr.Msg != "ErrAppVersionUnavailable" {
return nil, err return nil, err
} }
if err := validateAppDetailCompose([]byte(install.DockerCompose)); err != nil {
return nil, err
}
// A removed template cannot prove the instance is unmodified.
res.RawCompose = install.DockerCompose
res.IsEdit = true
return &res, nil
}
rawCompose, err = getUpgradeCompose(install, detail) rawCompose, err = getUpgradeCompose(install, detail)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1079,6 +1126,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
} }
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error { func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, appInstall.ID)
if err != nil {
return err
}
appInstall.TaskID = operationTask.ID
if operationTask.ID != "" {
appInstall.Status = localEngineTaskStatus(operationTask.Operate, appInstall.Status)
return nil
}
force = true
} else {
operation := "" operation := ""
switch appInstall.Status { switch appInstall.Status {
case constant.StatusInstalling: case constant.StatusInstalling:
@@ -1089,8 +1148,10 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
operation = task.TaskBuild operation = task.TaskBuild
case constant.StatusUninstalling: case constant.StatusUninstalling:
operation = task.TaskUninstall operation = task.TaskUninstall
case constant.StatusStarting, constant.StatusWaiting: case constant.StatusStarting:
operation = task.TaskUpdate operation = "Run"
case constant.StatusWaiting:
operation = "Stop"
case constant.StatusRestarting: case constant.StatusRestarting:
operation = task.TaskRestart operation = task.TaskRestart
} }
@@ -1116,6 +1177,7 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
return nil return nil
} }
} }
}
cli, err := docker.NewClient() cli, err := docker.NewClient()
if err != nil { if err != nil {
return err return err
+27
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -74,3 +75,29 @@ func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string,
} }
return nil return nil
} }
func localEngineTaskStatus(operation, status string) string {
switch operation {
case task.TaskInstall, task.TaskCreate:
return constant.StatusInstalling
case task.TaskUpgrade:
return constant.StatusUpgrading
case task.TaskUninstall, task.TaskDelete:
return constant.StatusUninstalling
case task.TaskBuild:
return constant.StatusRebuilding
case task.TaskRestart:
return constant.StatusRestarting
case "Run":
return constant.StatusStarting
case "Stop":
return constant.StatusWaiting
case task.TaskUpdate:
if status == constant.StatusWaiting {
return constant.StatusWaiting
}
return constant.StatusStarting
default:
return constant.StatusWaiting
}
}
+5 -5
View File
@@ -92,12 +92,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err return err
} }
if install.Status == constant.StatusUpgrading { if install.Status == constant.StatusUpgrading && install.App.Key != "ollama" && install.App.Key != "vllm" {
return buserr.New("TaskIsExecuting") return buserr.New("TaskIsExecuting")
} }
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded { if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting") return buserr.New("TaskIsExecuting")
} }
@@ -107,6 +104,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
appUpgradeLocks.Delete(install.ID) appUpgradeLocks.Delete(install.ID)
} }
}() }()
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
feishuLockPath := "" feishuLockPath := ""
if install.App.Key == constant.AppOpenclaw { if install.App.Key == constant.AppOpenclaw {
@@ -185,7 +185,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if ctx.rollbackErr != nil { if ctx.rollbackErr != nil {
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr) taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
upgradeTask.Task.ErrorMsg = taskErr.Error() upgradeTask.Task.ErrorMsg = taskErr.Error()
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task) _ = taskRepo.Update(context.Background(), upgradeTask.Task)
} }
if !ctx.stopAttempted || ctx.rollbackErr == nil { if !ctx.stopAttempted || ctx.rollbackErr == nil {
restored := ctx.original restored := ctx.original
+62 -20
View File
@@ -498,7 +498,11 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
} }
return nil return nil
} }
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil) uninstallTimeout := 30 * time.Minute
if install.App.Key == "ollama" || install.App.Key == "vllm" {
uninstallTimeout = 0
}
uninstallTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil, 0, uninstallTimeout)
go func() { go func() {
if err := uninstallTask.Execute(); err != nil { if err := uninstallTask.Execute(); err != nil {
install.Status = constant.StatusError install.Status = constant.StatusError
@@ -1226,28 +1230,36 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecyc
func rebuildApp(appInstall model.AppInstall) error { func rebuildApp(appInstall model.AppInstall) error {
appInstall.Status = constant.StatusRebuilding appInstall.Status = constant.StatusRebuilding
_ = appInstallRepo.Save(context.Background(), &appInstall) if err := appInstallRepo.Save(context.Background(), &appInstall); err != nil {
go func() { return err
dockerComposePath := appInstall.GetComposePath() }
out, err := compose.Down(dockerComposePath) buildTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskBuild, task.TaskScopeApp, "", appInstall.ID)
if err != nil { if err != nil {
_ = handleErr(appInstall, err, out) return handleErr(appInstall, err, "")
return }
buildTask.AddSubTaskWithOps(buildTask.Name, func(t *task.Task) error {
out, err := compose.Down(appInstall.GetComposePath())
t.Log(out)
if err != nil {
return handleErr(appInstall, err, out)
} }
out, err = compose.Up(appInstall.GetComposePath()) out, err = compose.Up(appInstall.GetComposePath())
t.Log(out)
if err != nil { if err != nil {
_ = handleErr(appInstall, err, out) return handleErr(appInstall, err, out)
return
} }
containerNames, err := getContainerNames(appInstall) containerNames, err := getContainerNames(appInstall)
if err != nil { if err != nil {
_ = handleErr(appInstall, err, out) return handleErr(appInstall, err, out)
return
} }
appInstall.ContainerName = strings.Join(containerNames, ",") appInstall.ContainerName = strings.Join(containerNames, ",")
appInstall.Status = constant.StatusRunning appInstall.Status = constant.StatusRunning
_ = appInstallRepo.Save(context.Background(), &appInstall) return appInstallRepo.Save(context.Background(), &appInstall)
}, nil, 0, 0)
go func() {
if err := buildTask.Execute(); err != nil {
_ = handleErr(appInstall, err, "")
}
}() }()
return nil return nil
} }
@@ -1477,7 +1489,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
} }
appInstall.Status = constant.StatusError appInstall.Status = constant.StatusError
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error() appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
_ = appInstallRepo.UpdateStatus(appInstall)
} else {
_ = appInstallRepo.Save(context.Background(), appInstall) _ = appInstallRepo.Save(context.Background(), appInstall)
}
return return
} }
notFoundNames := make([]string, 0) notFoundNames := make([]string, 0)
@@ -1536,30 +1552,55 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
appInstall.Message = msg appInstall.Message = msg
appInstall.Status = constant.StatusUnHealthy appInstall.Status = constant.StatusUnHealthy
} }
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
_ = appInstallRepo.UpdateStatus(appInstall)
} else {
_ = appInstallRepo.Save(context.Background(), appInstall) _ = appInstallRepo.Save(context.Background(), appInstall)
} }
}
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) { func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) {
var ( var (
res []response.AppInstallDTO res []response.AppInstallDTO
containersMap map[string]container.Summary containersMap map[string]container.Summary
) )
if sync { syncContainers := sync
for _, installed := range appInstallList {
if installed.App.Key == "ollama" || installed.App.Key == "vllm" {
syncContainers = true
break
}
}
if syncContainers {
cli, err := docker.NewClient() cli, err := docker.NewClient()
if err == nil { if err != nil {
return nil, err
}
defer cli.Close() defer cli.Close()
containers, err := cli.ListAllContainers() containers, err := cli.ListAllContainers()
if err == nil { if err != nil {
return nil, err
}
containersMap = make(map[string]container.Summary, len(containers)) containersMap = make(map[string]container.Summary, len(containers))
for _, contain := range containers { for _, contain := range containers {
containersMap[contain.Names[0]] = contain containersMap[contain.Names[0]] = contain
} }
} }
}
}
for _, installed := range appInstallList { for _, installed := range appInstallList {
if sync && appInstallOperationPending(installed.Status) { localEngine := installed.App.Key == "ollama" || installed.App.Key == "vllm"
if localEngine {
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, installed.ID)
if err != nil {
return nil, err
}
installed.TaskID = operationTask.ID
if operationTask.ID != "" {
installed.Status = localEngineTaskStatus(operationTask.Operate, installed.Status)
} else {
synAppInstall(containersMap, &installed, true)
}
} else if sync && appInstallOperationPending(installed.Status) {
if err := syncAppInstallStatus(&installed, false); err != nil { if err := syncAppInstallStatus(&installed, false); err != nil {
return nil, err return nil, err
} }
@@ -1568,7 +1609,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
continue continue
} }
if sync && !doNotNeedSync(installed) { if sync && !localEngine && !doNotNeedSync(installed) {
synAppInstall(containersMap, &installed, false) synAppInstall(containersMap, &installed, false)
} }
@@ -1582,6 +1623,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
AppDetailID: installed.AppDetailId, AppDetailID: installed.AppDetailId,
Version: installed.Version, Version: installed.Version,
Status: installed.Status, Status: installed.Status,
TaskID: installed.TaskID,
Message: installed.Message, Message: installed.Message,
HttpPort: installed.HttpPort, HttpPort: installed.HttpPort,
HttpsPort: installed.HttpsPort, HttpsPort: installed.HttpsPort,
+23
View File
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
backData = records[start:end] backData = records[start:end]
} }
// Batch-load persisted checks; never contact Docker Hub while rendering the list.
ids := make([]string, 0, len(backData))
for _, row := range backData {
ids = append(ids, row.ContainerID)
}
if len(ids) > 0 {
var checks []model.ContainerImageCheckUpdate
if err := global.DB.Where("container_id IN ?", ids).Find(&checks).Error; err != nil {
return 0, nil, err
}
byID := make(map[string]model.ContainerImageCheckUpdate, len(checks))
for _, check := range checks {
byID[check.ContainerID] = check
}
for i := range backData {
check, ok := byID[backData[i].ContainerID]
if ok && sameContainerImage(check.ImageID, backData[i].ImageId) && check.ImageName == backData[i].ImageName {
backData[i].LatestVersion = check.LatestVersion
backData[i].TargetImage = check.TargetImage
backData[i].HasUpdate = check.HasUpdate
}
}
}
for i := 0; i < len(backData); i++ { for i := 0; i < len(backData); i++ {
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name)) install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
if install.ID > 0 { if install.ID > 0 {
@@ -0,0 +1,402 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/dockerhub"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const imageCheckUpdateSetting = "ContainerImageUpdateEnabled"
type ImageCheckUpdateStatus struct {
TaskID string `json:"taskID"`
Enabled bool `json:"enabled"`
Running bool `json:"running"`
Checked int `json:"checked"`
Updates int `json:"updates"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Error string `json:"error"`
FinishedAt int64 `json:"finishedAt"`
}
var imageCheckUpdateState struct {
sync.Mutex
status ImageCheckUpdateStatus
}
func GetContainerImageCheckUpdateStatus() (ImageCheckUpdateStatus, error) {
var setting model.Setting
if err := global.DB.Where("key = ?", imageCheckUpdateSetting).First(&setting).Error; err != nil {
return ImageCheckUpdateStatus{}, err
}
imageCheckUpdateState.Lock()
defer imageCheckUpdateState.Unlock()
status := imageCheckUpdateState.status
status.Enabled = setting.Value == "Enable"
return status, nil
}
func SetContainerImageCheckUpdateSchedule(enabled bool) error {
value := "Disable"
if enabled {
_, err := req_helper.LoadRuntimeProxyURL()
if errors.Is(err, req_helper.ErrProxyRequired) {
return buserr.New("ErrImageCheckProxyRequired")
}
if err != nil {
return err
}
value = "Enable"
}
return global.DB.Model(&model.Setting{}).Where("key = ?", imageCheckUpdateSetting).Update("value", value).Error
}
// StartContainerImageCheckUpdate coalesces manual and scheduled runs on this node.
func StartContainerImageCheckUpdate() (string, error) {
imageCheckUpdateState.Lock()
defer imageCheckUpdateState.Unlock()
if imageCheckUpdateState.status.Running {
return imageCheckUpdateState.status.TaskID, nil
}
taskItem, err := task.NewTask(i18n.GetMsgByKey("ContainerImageCheckUpdate"), task.TaskScan, task.TaskScopeImage, "", 0)
if err != nil {
return "", err
}
// Persist before returning the ID so the log drawer can immediately find it.
if err := global.TaskDB.Save(taskItem.Task).Error; err != nil {
_ = taskItem.Logger.Out.(io.Closer).Close()
taskItem.DeleteLogFile()
if cancel, ok := global.LoadTaskCancel(taskItem.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(taskItem.TaskID)
return "", err
}
imageCheckUpdateState.status = ImageCheckUpdateStatus{Running: true, TaskID: taskItem.TaskID}
go func() {
result := ImageCheckUpdateStatus{TaskID: taskItem.TaskID}
defer func() {
result.FinishedAt = time.Now().UnixMilli()
imageCheckUpdateState.Lock()
imageCheckUpdateState.status = result
imageCheckUpdateState.Unlock()
}()
// The action owns its deadline so the task runner waits for cleanup and logs.
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerImageCheckUpdate"), func(t *task.Task) (err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("image update check failed: %v", recovered)
}
t.Log(i18n.GetMsgWithMap("ContainerImageCheckSummary", map[string]interface{}{
"checked": result.Checked, "updates": result.Updates, "failed": result.Failed, "skipped": result.Skipped,
}))
}()
ctx, cancel := context.WithTimeout(t.TaskCtx, 30*time.Minute)
defer cancel()
if err = runContainerImageCheckUpdate(ctx, &result, t.Log); err != nil {
return err
}
if result.Failed > 0 {
return fmt.Errorf("%s", i18n.GetMsgWithMap("ContainerImageCheckFailures", map[string]interface{}{"count": result.Failed}))
}
return nil
}, nil, 0, 0)
if err := taskItem.Execute(); err != nil {
result.Error = err.Error()
global.LOG.Errorf("container image check: %s", err)
}
}()
return taskItem.TaskID, nil
}
// These caches live only for one check. Remote results do not depend on a
// container's local image ID, which is compared separately for every baseline.
type imageCheckUpdateRepository struct {
client imageCheckUpdateHubClient
err error
tags []string
tagsLoaded bool
tagsErr error
}
type imageCheckUpdateTargetKey struct {
repository, tag string
platform dockerhub.Platform
}
type imageCheckUpdateTargetResult struct {
target dockerhub.Target
compatible bool
err error
}
type imageCheckUpdateResult struct {
record model.ContainerImageCheckUpdate
err error
}
type imageCheckUpdateCache struct {
username, password string
httpClient *http.Client
repositories map[string]*imageCheckUpdateRepository
targets map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult
}
func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error {
httpClient, err := req_helper.NewRuntimeHTTPClient()
if errors.Is(err, req_helper.ErrProxyRequired) {
return buserr.New("ErrImageCheckProxyRequired")
}
if err != nil {
return err
}
if httpClient.Transport != nil {
defer httpClient.CloseIdleConnections()
}
log := func(key string, values map[string]interface{}) {
if logger != nil {
logger(i18n.GetMsgWithMap(key, values))
}
}
cli, err := newImageCheckUpdateDockerClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return err
}
log("ContainerImageCheckScope", map[string]interface{}{"count": len(containers)})
// Load old results once, rather than issuing one SELECT for every failed check.
var previousRows []model.ContainerImageCheckUpdate
if err := global.DB.WithContext(ctx).Find(&previousRows).Error; err != nil {
return err
}
previous := make(map[string]model.ContainerImageCheckUpdate, len(previousRows))
for _, row := range previousRows {
previous[row.ContainerID] = row
}
cache := imageCheckUpdateCache{httpClient: httpClient, repositories: map[string]*imageCheckUpdateRepository{}, targets: map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult{}}
cfg := config.LoadDefaultConfigFile(io.Discard)
if cfg != nil {
for _, host := range []string{"https://index.docker.io/v1/", "docker.io", "registry-1.docker.io"} {
auth, e := cfg.GetAuthConfig(host)
if e == nil && auth.Username != "" {
cache.username, cache.password = auth.Username, auth.Password
break
}
}
}
checks := map[string]imageCheckUpdateResult{}
ids := make([]string, 0, len(containers))
names := make(map[string]string, len(containers))
pending := make([]model.ContainerImageCheckUpdate, 0, 100)
for _, item := range containers {
if err := ctx.Err(); err != nil {
return err
}
ids = append(ids, item.ID)
name := item.ID
if len(item.Names) > 0 {
name = strings.TrimPrefix(item.Names[0], "/")
}
names[item.ID] = name
log("ContainerImageCheckItem", map[string]interface{}{"name": name, "image": item.Image})
repository, tag, supported := dockerhub.Reference(item.Image)
key := repository + ":" + tag + "@" + item.ImageID
entry, found := checks[key]
if !found {
entry.record, entry.err = checkContainerImageUpdate(ctx, cli, item, repository, tag, supported, &cache)
checks[key] = entry
}
record := entry.record
record.ContainerID, record.ImageName = item.ID, item.Image
if entry.err != nil {
record.CheckStatus, record.CheckError = "failed", entry.err.Error()
old := previous[item.ID]
if old.ImageID == item.ImageID && old.ImageName == item.Image {
record.LatestVersion, record.TargetImage, record.TargetImageID, record.HasUpdate = old.LatestVersion, old.TargetImage, old.TargetImageID, old.HasUpdate
}
}
pending = append(pending, record)
if len(pending) == 100 {
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
return err
}
pending = pending[:0]
}
}
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
return err
}
query := global.DB.WithContext(ctx).Where("1 = 1")
if len(ids) > 0 {
query = query.Where("container_id NOT IN ?", ids)
}
return query.Delete(&model.ContainerImageCheckUpdate{}).Error
}
func checkContainerImageUpdate(ctx context.Context, cli imageCheckUpdateDockerClient, item container.Summary, repository, tag string, supported bool, cache *imageCheckUpdateCache) (model.ContainerImageCheckUpdate, error) {
record := model.ContainerImageCheckUpdate{ImageID: item.ImageID, ImageName: item.Image, CheckStatus: "success", CheckedAt: time.Now()}
if !supported {
record.CheckStatus = "skipped"
return record, nil
}
inspected, err := cli.ImageInspect(ctx, item.ImageID)
if err != nil {
return record, err
}
repo, loaded := cache.repositories[repository]
if !loaded {
repo = &imageCheckUpdateRepository{}
repo.client, repo.err = newImageCheckUpdateHubClient(ctx, repository, cache.username, cache.password, cache.httpClient)
cache.repositories[repository] = repo
}
if repo.err != nil {
return record, repo.err
}
if dockerhub.NeedsTags(tag) {
if !repo.tagsLoaded {
repo.tags, repo.tagsErr = repo.client.Tags(ctx)
repo.tagsLoaded = true
}
if repo.tagsErr != nil {
return record, repo.tagsErr
}
}
platform := dockerhub.Platform{OS: inspected.Os, Architecture: inspected.Architecture, Variant: inspected.Variant}
for _, candidate := range dockerhub.Candidates(tag, repo.tags) {
key := imageCheckUpdateTargetKey{repository: repository, tag: candidate, platform: platform}
remote, loaded := cache.targets[key]
if !loaded {
remote.target, remote.compatible, remote.err = repo.client.Resolve(ctx, candidate, platform)
cache.targets[key] = remote
}
if remote.err != nil {
return record, remote.err
}
if !remote.compatible {
continue
}
record.LatestVersion = remote.target.Tag
record.TargetImage = "docker.io/" + repository + ":" + remote.target.Tag
record.TargetImageID = remote.target.ConfigDigest
// Both versioned and floating tags can be republished with new image content.
record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID
return record, nil
}
return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture)
}
// Revalidate immediately before each small write batch. Never hold a database
// transaction open while waiting for Docker or Docker Hub.
func saveContainerImageCheckUpdates(ctx context.Context, cli imageCheckUpdateDockerClient, pending []model.ContainerImageCheckUpdate, names map[string]string, result *ImageCheckUpdateStatus, log func(string, map[string]interface{})) error {
records := make([]model.ContainerImageCheckUpdate, 0, len(pending))
for _, record := range pending {
current, err := cli.ContainerInspect(ctx, record.ContainerID)
if err != nil && !errdefs.IsNotFound(err) {
return err
}
if err != nil || current.Image != record.ImageID {
result.Skipped++
log("ContainerImageCheckChanged", map[string]interface{}{"name": names[record.ContainerID]})
continue
}
records = append(records, record)
}
if len(records) == 0 {
return nil
}
if err := global.DB.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&records, 100).Error; err != nil {
return err
}
// Count and announce only results that were successfully persisted.
for _, record := range records {
values := map[string]interface{}{"name": names[record.ContainerID], "image": record.ImageName}
switch record.CheckStatus {
case "failed":
result.Failed++
values["error"] = record.CheckError
log("ContainerImageCheckFailed", values)
case "skipped":
result.Skipped++
log("ContainerImageCheckSkipped", values)
default:
result.Checked++
if record.HasUpdate {
result.Updates++
values["target"] = record.TargetImage
log("ContainerImageCheckAvailable", values)
} else {
log("ContainerImageCheckCurrent", values)
}
}
}
return nil
}
// sameContainerImage guards cached results after external image changes.
func sameContainerImage(stored, displayed string) bool {
return strings.TrimPrefix(stored, "sha256:") == strings.TrimPrefix(displayed, "sha256:")
}
// Reconcile only after Docker confirms the replacement container uses the target.
func reconcileContainerImageCheckUpdate(oldID, newID, imageID, imageName string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var previous model.ContainerImageCheckUpdate
if err := tx.Where("container_id = ?", oldID).Find(&previous).Error; err != nil {
return err
}
if previous.ContainerID == "" {
return nil
}
if err := tx.Where("container_id = ?", oldID).Delete(&model.ContainerImageCheckUpdate{}).Error; err != nil {
return err
}
if previous.TargetImageID != imageID {
return nil
}
previous.ContainerID = newID
previous.ImageID = imageID
previous.ImageName = imageName
previous.HasUpdate = false
previous.CheckStatus = "success"
previous.CheckError = ""
previous.CheckedAt = time.Now()
return tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(&previous).Error
})
}
type imageCheckUpdateDockerClient interface {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error)
Close() error
}
type imageCheckUpdateHubClient interface {
Tags(context.Context) ([]string, error)
Resolve(context.Context, string, dockerhub.Platform) (dockerhub.Target, bool, error)
}
var newImageCheckUpdateDockerClient = func() (imageCheckUpdateDockerClient, error) { return docker.NewDockerClient() }
var newImageCheckUpdateHubClient = func(ctx context.Context, repository, username, password string, httpClient *http.Client) (imageCheckUpdateHubClient, error) {
return dockerhub.New(ctx, repository, username, password, httpClient)
}
+5 -1
View File
@@ -163,6 +163,11 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
upgradeErrors = append(upgradeErrors, upgradeErr) upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr return upgradeErr
} }
if updated, inspectErr := client.ContainerInspect(ctx, item); inspectErr == nil {
if cacheErr := reconcileContainerImageCheckUpdate(oldContainer.ID, updated.ID, updated.Image, updated.Config.Image); cacheErr != nil {
global.LOG.Warnf("refresh container image update state: %v", cacheErr)
}
}
if cleanupErr != nil { if cleanupErr != nil {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
} }
@@ -307,7 +312,6 @@ func switchContainer(
return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false) return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false)
} }
const ( const (
containerUpgradePullTimeout = 30 * time.Minute containerUpgradePullTimeout = 30 * time.Minute
containerUpgradeReplaceTimeout = 15 * time.Minute containerUpgradeReplaceTimeout = 15 * time.Minute
+7 -1
View File
@@ -414,7 +414,7 @@ func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
if showCount == 0 { if showCount == 0 {
return buserr.New("ErrMinQuickJump") return buserr.New("ErrMinQuickJump")
} }
if showCount > 4 { if showCount > 5 {
return buserr.New("ErrMaxQuickJump") return buserr.New("ErrMaxQuickJump")
} }
@@ -709,6 +709,12 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false) quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ { for i := 0; i < len(quicks); i++ {
switch quicks[i].Name { switch quicks[i].Name {
case "ModelServices":
count, err := agentAccountRepo.Count()
if err != nil {
global.LOG.Errorf("load model service count: %v", err)
}
quicks[i].Detail = fmt.Sprintf("%d", count)
case "File": case "File":
count, err := launcherRepo.CountQuickDirectories() count, err := launcherRepo.CountQuickDirectories()
if err != nil { if err != nil {
+3
View File
@@ -3,6 +3,9 @@ package service
import "github.com/1Panel-dev/1Panel/agent/app/repo" import "github.com/1Panel-dev/1Panel/agent/app/repo"
var ( var (
vmRepo = repo.NewIVMRepo()
modelDownloadTaskRepo = repo.NewIModelDownloadTaskRepo()
featuredModelRepo = repo.NewIFeaturedModelRepo()
appRepo = repo.NewIAppRepo() appRepo = repo.NewIAppRepo()
appTagRepo = repo.NewIAppTagRepo() appTagRepo = repo.NewIAppTagRepo()
appDetailRepo = repo.NewIAppDetailRepo() appDetailRepo = repo.NewIAppDetailRepo()
+122
View File
@@ -0,0 +1,122 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/req_helper"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
)
var featuredModelsSyncMu sync.Mutex
func (m *ModelDownloaderService) PageFeaturedModels(req dto.FeaturedModelSearch) (*dto.FeaturedModelPage, error) {
total, models, err := featuredModelRepo.Page(req.Page, req.PageSize, strings.TrimSpace(req.Family), strings.TrimSpace(req.Info))
if err != nil {
return nil, err
}
items := make([]dto.FeaturedModel, 0, len(models))
for _, item := range models {
var description map[string]string
if err := json.Unmarshal([]byte(item.Description), &description); err != nil {
return nil, fmt.Errorf("parse featured model %s description failed: %w", item.Key, err)
}
items = append(items, dto.FeaturedModel{
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
SourceModelIDs: item.SourceModelIDs,
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
Description: description,
})
}
families, err := featuredModelRepo.Families()
if err != nil {
return nil, err
}
return &dto.FeaturedModelPage{PageResult: dto.PageResult{Total: total, Items: items}, Families: families}, nil
}
func (m *ModelDownloaderService) SyncFeaturedModels() error {
if global.CONF.Base.IsOffline {
return nil
}
featuredModelsSyncMu.Lock()
defer featuredModelsSyncMu.Unlock()
localVersion, err := settingRepo.GetValueByKey("FeaturedModelsVersion")
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
data, version, err := req_helper.LoadAIResource("featured-models", localVersion)
if err != nil || data == nil {
return err
}
var remote struct {
Models []dto.FeaturedModel `yaml:"models"`
}
if err := yaml.Unmarshal(data, &remote); err != nil {
return fmt.Errorf("parse featured models failed: %w", err)
}
if len(remote.Models) == 0 {
return errors.New("featured models data is empty")
}
keys := make(map[string]bool, len(remote.Models))
models := make([]model.FeaturedModel, 0, len(remote.Models))
for i := range remote.Models {
item := &remote.Models[i]
item.Key = strings.TrimSpace(item.Key)
item.Family = strings.TrimSpace(item.Family)
item.RepoID = strings.TrimSpace(item.RepoID)
if item.Key == "" || item.Family == "" || item.RepoID == "" || (item.Architecture != "MoE" && item.Architecture != "Dense") || item.WeightGB < 0 {
return fmt.Errorf("invalid featured model: %s", item.Key)
}
if keys[item.Key] {
return fmt.Errorf("duplicate featured model key: %s", item.Key)
}
keys[item.Key] = true
if len(item.Sources) == 0 {
item.Sources = []string{"modelscope"}
}
for _, source := range item.Sources {
if source != "modelscope" && source != "huggingface" {
return fmt.Errorf("invalid featured model %s source: %s", item.Key, source)
}
}
description, err := json.Marshal(item.Description)
if err != nil {
return err
}
models = append(models, model.FeaturedModel{
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
SourceModelIDs: item.SourceModelIDs,
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
Description: string(description),
})
}
return featuredModelRepo.SyncAll(models, version)
}
func SyncFeaturedModels() {
if global.CONF.Base.IsOffline {
return
}
status, err := settingRepo.GetValueByKey("FeaturedModelsSync")
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
global.LOG.Errorf("load featured model sync setting failed: %v", err)
return
}
if err == nil && status != constant.StatusEnable {
return
}
if err := NewIModelDownloaderService().SyncFeaturedModels(); err != nil {
global.LOG.Errorf("sync featured models failed: %v", err)
}
}
+17
View File
@@ -24,6 +24,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/dockerhub"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
@@ -35,6 +37,7 @@ import (
type ImageService struct{} type ImageService struct{}
type IImageService interface { type IImageService interface {
SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error)
Page(req dto.PageImage) (int64, interface{}, error) Page(req dto.PageImage) (int64, interface{}, error)
List() ([]dto.Options, error) List() ([]dto.Options, error)
ListAll() ([]dto.ImageInfo, error) ListAll() ([]dto.ImageInfo, error)
@@ -560,3 +563,17 @@ func checkUsed(imageID string, containers []container.Summary) bool {
} }
return false return false
} }
func (u *ImageService) SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error) {
client, err := req_helper.NewRuntimeHTTPClient()
if err != nil {
if errors.Is(err, req_helper.ErrProxyRequired) {
return dockerhub.SearchResult{Items: []dockerhub.SearchItem{}, ProxyRequired: true}, nil
}
return dockerhub.SearchResult{}, err
}
if client.Transport != nil {
defer client.CloseIdleConnections()
}
return dockerhub.Search(ctx, client, req.Query, req.Page, req.PageSize)
}
+282 -11
View File
@@ -17,14 +17,17 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "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/constant"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx" "github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -52,6 +55,7 @@ func init() {
type MCPWebsiteBindingService struct { type MCPWebsiteBindingService struct {
createWebsite func(request.WebsiteCreate) error createWebsite func(request.WebsiteCreate) error
deleteWebsite func(request.WebsiteDelete) error
updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error
applyInclude func(model.Website) error applyInclude func(model.Website) error
removeInclude func(model.Website) error removeInclude func(model.Website) error
@@ -63,6 +67,7 @@ type MCPWebsiteBindingService struct {
func NewMCPWebsiteBindingService() *MCPWebsiteBindingService { func NewMCPWebsiteBindingService() *MCPWebsiteBindingService {
s := &MCPWebsiteBindingService{ s := &MCPWebsiteBindingService{
createWebsite: func(req request.WebsiteCreate) error { return NewIWebsiteService().CreateWebsite(req) }, createWebsite: func(req request.WebsiteCreate) error { return NewIWebsiteService().CreateWebsite(req) },
deleteWebsite: func(req request.WebsiteDelete) error { return NewIWebsiteService().DeleteWebsite(req) },
updateHTTPS: func(ctx context.Context, req request.WebsiteHTTPSOp) error { updateHTTPS: func(ctx context.Context, req request.WebsiteHTTPSOp) error {
if req.Enable { if req.Enable {
current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID) current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID)
@@ -148,6 +153,14 @@ type mcpWebsiteOwner struct {
RequestID string `json:"requestId"` RequestID string `json:"requestId"`
RequestHash string `json:"requestHash"` RequestHash string `json:"requestHash"`
Operation string `json:"operation"` Operation string `json:"operation"`
DeleteRoot string `json:"deleteRoot,omitempty"`
DeletePaths []mcpWebsiteDeletePath `json:"deletePaths,omitempty"`
}
type mcpWebsiteDeletePath struct {
Kind string `json:"kind"`
Device uint64 `json:"device"`
Inode uint64 `json:"inode"`
} }
func mcpWebsiteHash(value string) string { func mcpWebsiteHash(value string) string {
@@ -236,6 +249,9 @@ func mcpWebsiteFailure(reason string, definitive, conflict bool) response.MCPWeb
if conflict { if conflict {
code, message = "MCP_WEBSITE_CONFLICT", "MCP website configuration changed" code, message = "MCP_WEBSITE_CONFLICT", "MCP website configuration changed"
} }
if definitive && reason == "http_port_requires_default" {
code, message = "MCP_WEBSITE_HTTP_PORT_REQUIRED", "Use the OpenResty HTTP port before enabling HTTPS"
}
return response.MCPWebsiteBinding{State: "unavailable", Reason: reason, Code: code, Error: message, Definitive: definitive} return response.MCPWebsiteBinding{State: "unavailable", Reason: reason, Code: code, Error: message, Definitive: definitive}
} }
@@ -461,13 +477,190 @@ func mcpWebsiteReadProxy(path string) ([]byte, error) {
} }
func mcpWebsitePayloadHash(req request.MCPWebsiteBinding) string { func mcpWebsitePayloadHash(req request.MCPWebsiteBinding) string {
return mcpWebsiteHash(fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID)) payload := fmt.Sprintf("%s\n%s\n%t\n%d", req.Operation, req.Domain, req.EnableSSL, req.SSLID)
if req.DeleteWebsite {
payload += "\ndeleteWebsite=true"
}
return mcpWebsiteHash(payload)
} }
func mcpWebsiteBindHash(req request.MCPWebsiteBinding) string { func mcpWebsiteBindHash(req request.MCPWebsiteBinding) string {
return mcpWebsiteHash(fmt.Sprintf("%s\n%t\n%d", req.Domain, req.EnableSSL, req.SSLID)) return mcpWebsiteHash(fmt.Sprintf("%s\n%t\n%d", req.Domain, req.EnableSSL, req.SSLID))
} }
func mcpWebsiteDeleteLocations(site model.Website) map[string]string {
return map[string]string{"site": GetSitePath(site, SiteDir), "config": GetSitePath(site, SiteConf)}
}
func mcpWebsiteDeleteParents() error {
root := filepath.Clean(GetWebSiteRootDir())
if !filepath.IsAbs(root) {
return errors.New("invalid managed website root")
}
for _, directory := range []string{root, filepath.Join(root, "sites"), filepath.Join(root, "conf.d")} {
info, err := os.Lstat(directory)
if os.IsNotExist(err) {
continue
}
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("managed website directory changed")
}
}
return nil
}
func mcpWebsiteDeleteIdentity(kind, location string) (mcpWebsiteDeletePath, bool, error) {
info, err := os.Lstat(location)
if os.IsNotExist(err) {
return mcpWebsiteDeletePath{}, false, nil
}
if err != nil || info.Mode()&os.ModeSymlink != 0 || kind == "site" && !info.IsDir() || kind == "config" && !info.Mode().IsRegular() {
return mcpWebsiteDeletePath{}, false, errors.New("managed website path changed")
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return mcpWebsiteDeletePath{}, false, errors.New("managed website path identity is unavailable")
}
return mcpWebsiteDeletePath{Kind: kind, Device: uint64(stat.Dev), Inode: uint64(stat.Ino)}, true, nil
}
func mcpWebsiteRecordDeletePaths(site model.Website, owner *mcpWebsiteOwner) error {
if err := mcpWebsiteDeleteParents(); err != nil {
return err
}
owner.DeleteRoot, owner.DeletePaths = filepath.Clean(GetWebSiteRootDir()), nil
for kind, location := range mcpWebsiteDeleteLocations(site) {
identity, exists, err := mcpWebsiteDeleteIdentity(kind, location)
if err != nil {
return err
}
if exists {
owner.DeletePaths = append(owner.DeletePaths, identity)
}
}
return nil
}
func mcpWebsiteVerifyDeletePaths(site model.Website, owner mcpWebsiteOwner) (map[string]string, error) {
if owner.DeleteRoot == "" || owner.DeleteRoot != filepath.Clean(GetWebSiteRootDir()) {
return nil, errors.New("managed website root changed")
}
if err := mcpWebsiteDeleteParents(); err != nil {
return nil, err
}
expected := make(map[string]mcpWebsiteDeletePath)
for _, identity := range owner.DeletePaths {
if identity.Kind != "site" && identity.Kind != "config" || expected[identity.Kind].Kind != "" {
return nil, errors.New("invalid managed website deletion journal")
}
expected[identity.Kind] = identity
}
remaining := make(map[string]string)
for kind, location := range mcpWebsiteDeleteLocations(site) {
actual, exists, err := mcpWebsiteDeleteIdentity(kind, location)
if err != nil {
return nil, err
}
if exists {
if expected[kind] != actual {
return nil, errors.New("managed website path was replaced")
}
remaining[kind] = location
}
}
return remaining, nil
}
func mcpWebsiteCleanupDeletedPrivate(site model.Website) error {
if err := mcpWebsitePrivateDirectory(site, false); err != nil {
if _, statErr := os.Lstat(filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName))); os.IsNotExist(statErr) {
return nil
}
return err
}
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) {
return errors.New("managed proxy remains after website deletion")
}
if err := os.Remove(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); err != nil && !os.IsNotExist(err) {
return err
}
directory := filepath.Dir(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName))
entries, err := os.ReadDir(directory)
if err != nil {
return err
}
if len(entries) == 0 {
return os.Remove(directory)
}
return nil
}
func (s *MCPWebsiteBindingService) deletedWebsite(req request.MCPWebsiteBinding, site model.Website, owner mcpWebsiteOwner, recoverFiles bool) response.MCPWebsiteBinding {
if replacement, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
if err == nil && replacement.ID == site.ID {
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
}
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
remaining, err := mcpWebsiteVerifyDeletePaths(site, owner)
if err != nil {
return mcpWebsiteFailure("website_directory_changed", false, true)
}
if len(remaining) > 0 && !recoverFiles {
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
}
for kind, location := range remaining {
fileOp := files.NewFileOp()
if kind == "site" {
err = fileOp.DeleteDir(location)
} else {
err = fileOp.DeleteFile(location)
}
if err != nil {
return mcpWebsiteFailure("website_delete_failed", false, false)
}
}
remaining, err = mcpWebsiteVerifyDeletePaths(site, owner)
if err != nil || len(remaining) != 0 {
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
}
if err = mcpWebsiteCleanupDeletedPrivate(site); err != nil {
return mcpWebsiteFailure("ownership_cleanup_failed", false, false)
}
return response.MCPWebsiteBinding{State: "unbound", WebsiteID: site.ID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true}
}
func (s *MCPWebsiteBindingService) missingDeletedWebsite(req request.MCPWebsiteBinding) response.MCPWebsiteBinding {
site := model.Website{BaseModel: model.BaseModel{ID: req.WebsiteID}, Alias: mcpWebsiteAlias(req.BindingID)}
if _, err := websiteRepo.GetFirst(websiteRepo.WithAlias(site.Alias)); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
owner, ownerErr := mcpWebsiteReadOwner(site)
if ownerErr == nil && owner.BindingID == req.BindingID && (req.WebsiteID == 0 || owner.WebsiteID == req.WebsiteID) && owner.KeyHash == mcpWebsiteHash(req.ProxyKey) &&
owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == mcpWebsitePayloadHash(req) {
site.ID = owner.WebsiteID
if _, err := websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) {
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
return s.deletedWebsite(req, site, owner, true)
}
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteOwnerName)); !os.IsNotExist(err) {
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
if _, err := os.Lstat(mcpWebsitePrivatePath(site, mcpWebsiteProxyName)); !os.IsNotExist(err) {
return mcpWebsiteFailure("proxy_modified", false, true)
}
if err := mcpWebsiteDeleteParents(); err != nil {
return mcpWebsiteFailure("website_directory_changed", false, true)
}
for _, location := range mcpWebsiteDeleteLocations(site) {
if _, err := os.Lstat(location); !os.IsNotExist(err) {
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
}
}
return response.MCPWebsiteBinding{State: "unbound", WebsiteID: req.WebsiteID, BindingID: req.BindingID, RequestID: req.RequestID, WebsiteDeleted: true}
}
func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBinding, site model.Website) bool { func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBinding, site model.Website) bool {
host, port, err := mcpWebsiteDomain(req.Domain) host, port, err := mcpWebsiteDomain(req.Domain)
if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID { if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID {
@@ -479,12 +672,9 @@ func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBind
return false return false
} }
port = installed.HttpPort port = installed.HttpPort
if req.EnableSSL {
port = installed.HttpsPort
}
} }
for _, domain := range site.Domains { for _, domain := range site.Domains {
if domain.Domain == host && domain.Port == port { if domain.Domain == host && domain.Port == port && !domain.SSL {
return true return true
} }
} }
@@ -617,6 +807,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
if req.Operation != "status" && req.Operation != "bind" && req.Operation != "update" && req.Operation != "unbind" && req.Operation != "sync" { if req.Operation != "status" && req.Operation != "bind" && req.Operation != "update" && req.Operation != "unbind" && req.Operation != "sync" {
return mcpWebsiteFailure("invalid_operation", true, false) return mcpWebsiteFailure("invalid_operation", true, false)
} }
if req.DeleteWebsite && req.Operation != "unbind" {
return mcpWebsiteFailure("invalid_operation", true, false)
}
if req.BindingID == "" && req.WebsiteID == 0 && req.Operation == "status" { if req.BindingID == "" && req.WebsiteID == 0 && req.Operation == "status" {
return response.MCPWebsiteBinding{State: "unbound"} return response.MCPWebsiteBinding{State: "unbound"}
} }
@@ -639,6 +832,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
site, err := s.load(req) site, err := s.load(req)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
if req.Operation == "status" || req.Operation == "unbind" { if req.Operation == "status" || req.Operation == "unbind" {
if req.DeleteWebsite {
return s.missingDeletedWebsite(req)
}
if req.WebsiteID == 0 || req.Operation == "unbind" { if req.WebsiteID == 0 || req.Operation == "unbind" {
return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID} return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID}
} }
@@ -658,19 +854,31 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
domain, port, _ := mcpWebsiteDomain(req.Domain) domain, port, _ := mcpWebsiteDomain(req.Domain)
if port == 0 { if port == 0 {
port = installed.HttpPort port = installed.HttpPort
}
if req.EnableSSL && port != installed.HttpPort {
return mcpWebsiteFailure("http_port_requires_default", true, false)
}
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port}}}
if req.EnableSSL { if req.EnableSSL {
port = installed.HttpsPort candidate.Protocol = constant.ProtocolHTTPS
} }
} if conflictErr := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); conflictErr != nil {
if existing, conflictErr := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain), websiteDomainRepo.WithPort(port)); conflictErr == nil && existing.ID > 0 { var business buserr.BusinessError
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
return mcpWebsiteFailure("domain_conflict", true, true) return mcpWebsiteFailure("domain_conflict", true, true)
} else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) { }
return mcpWebsiteFailure("website_unavailable", false, false) return mcpWebsiteFailure("website_unavailable", false, false)
} }
create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID, create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID,
Domains: []request.WebsiteDomain{{Domain: domain, Port: port, SSL: req.EnableSSL}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}} Domains: []request.WebsiteDomain{{Domain: domain, Port: port}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}}
previousMutation := mutationStarted
mutationStarted = true mutationStarted = true
if createErr := s.createWebsite(create); createErr != nil { if createErr := s.createWebsite(create); createErr != nil {
var business buserr.BusinessError
if errors.As(createErr, &business) && business.Msg == "ErrDomainIsUsed" {
mutationStarted = previousMutation
return mcpWebsiteFailure("domain_conflict", true, true)
}
return mcpWebsiteFailure("website_create_failed", false, false) return mcpWebsiteFailure("website_create_failed", false, false)
} }
site, err = s.load(req) site, err = s.load(req)
@@ -701,6 +909,9 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
return mcpWebsiteFailure("website_unavailable", false, false) return mcpWebsiteFailure("website_unavailable", false, false)
} }
if ownerErr != nil { if ownerErr != nil {
if req.DeleteWebsite {
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) { if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) {
return mcpWebsiteFailure("website_ownership_changed", true, true) return mcpWebsiteFailure("website_ownership_changed", true, true)
} }
@@ -711,15 +922,44 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
} else if owner.BindingID != req.BindingID || owner.WebsiteID != site.ID || owner.KeyHash != mcpWebsiteHash(req.ProxyKey) { } else if owner.BindingID != req.BindingID || owner.WebsiteID != site.ID || owner.KeyHash != mcpWebsiteHash(req.ProxyKey) {
return mcpWebsiteFailure("website_ownership_changed", true, true) return mcpWebsiteFailure("website_ownership_changed", true, true)
} }
if req.DeleteWebsite {
if site.Type != constant.Static || site.AppInstallID != 0 || site.RuntimeID != 0 || site.DbID != 0 || site.DbType != "" || site.ParentWebsiteID != 0 {
return mcpWebsiteFailure("website_modified", true, true)
}
if err = mcpWebsiteDeleteParents(); err != nil {
return mcpWebsiteFailure("website_directory_changed", true, true)
}
for kind, location := range mcpWebsiteDeleteLocations(site) {
if _, _, err = mcpWebsiteDeleteIdentity(kind, location); err != nil {
return mcpWebsiteFailure("website_directory_changed", true, true)
}
}
children, childErr := websiteRepo.GetBy(websiteRepo.WithParentID(site.ID))
if childErr != nil {
return mcpWebsiteFailure("website_unavailable", false, false)
}
if len(children) != 0 {
return mcpWebsiteFailure("website_modified", true, true)
}
}
if req.Operation == "status" { if req.Operation == "status" {
return s.status(req, site, owner) return s.status(req, site, owner)
} }
operationHash := mcpWebsitePayloadHash(req) operationHash := mcpWebsitePayloadHash(req)
deleteRetry := req.DeleteWebsite && owner.Operation == "unbind" && owner.RequestID == req.RequestID && owner.RequestHash == operationHash && owner.DeleteRoot != ""
if owner.DeleteRoot != "" && req.Operation != "status" && (owner.RequestID != req.RequestID || owner.RequestHash != operationHash) {
return mcpWebsiteFailure("operation_request_changed", false, true)
}
if deleteRetry {
if _, err = mcpWebsiteVerifyDeletePaths(site, owner); err != nil {
return mcpWebsiteFailure("website_directory_changed", false, true)
}
}
if owner.RequestID == req.RequestID && req.RequestID != "" { if owner.RequestID == req.RequestID && req.RequestID != "" {
if owner.RequestHash != operationHash { if owner.RequestHash != operationHash {
return mcpWebsiteFailure("operation_request_changed", true, true) return mcpWebsiteFailure("operation_request_changed", true, true)
} }
if owner.Operation == "unbind" || owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) { if owner.Operation == "unbind" && !req.DeleteWebsite || owner.Operation != "unbind" && owner.ProxyHash == mcpWebsiteHash(string(mcpWebsiteProxy(req))) {
return s.status(req, site, owner) return s.status(req, site, owner)
} }
} }
@@ -774,17 +1014,48 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
return mcpWebsiteFailure("proxy_remove_failed", false, false) return mcpWebsiteFailure("proxy_remove_failed", false, false)
} }
} }
if req.DeleteWebsite && !deleteRetry {
if err = mcpWebsiteRecordDeletePaths(site, &owner); err != nil {
return mcpWebsiteFailure("website_directory_changed", false, true)
}
}
owner.Operation, owner.RequestID, owner.RequestHash = "unbind", req.RequestID, operationHash owner.Operation, owner.RequestID, owner.RequestHash = "unbind", req.RequestID, operationHash
mutationStarted = true mutationStarted = true
if err = mcpWebsiteWriteOwner(site, owner); err != nil { if err = mcpWebsiteWriteOwner(site, owner); err != nil {
return mcpWebsiteFailure("ownership_write_failed", false, false) return mcpWebsiteFailure("ownership_write_failed", false, false)
} }
if req.DeleteWebsite {
if err = s.deleteWebsite(request.WebsiteDelete{ID: site.ID}); err != nil {
return mcpWebsiteFailure("website_delete_failed", false, false)
}
if _, err = websiteRepo.GetFirst(repo.WithByID(site.ID)); !errors.Is(err, gorm.ErrRecordNotFound) {
return mcpWebsiteFailure("website_delete_unconfirmed", false, false)
}
return s.deletedWebsite(req, site, owner, false)
}
return s.status(req, site, owner) return s.status(req, site, owner)
} }
if req.Operation == "update" { if req.Operation == "update" {
if req.Domain != site.PrimaryDomain { if req.Domain != site.PrimaryDomain {
return mcpWebsiteFailure("domain_changed", true, true) return mcpWebsiteFailure("domain_changed", true, true)
} }
if req.EnableSSL {
installed, installErr := s.openresty()
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
return mcpWebsiteFailure("openresty_unavailable", true, false)
}
host, _, _ := mcpWebsiteDomain(site.PrimaryDomain)
usesHTTPPort := false
for _, domain := range site.Domains {
if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL {
usesHTTPPort = true
break
}
}
if !usesHTTPPort {
return mcpWebsiteFailure("http_port_requires_default", true, false)
}
}
if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID { if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID {
mutationStarted = true mutationStarted = true
if err = s.updateHTTPS(context.WithoutCancel(ctx), request.WebsiteHTTPSOp{WebsiteID: site.ID, Enable: req.EnableSSL, Type: constant.SSLExisted, WebsiteSSLID: req.SSLID, HttpConfig: constant.HTTPSOnly}); err != nil { if err = s.updateHTTPS(context.WithoutCancel(ctx), request.WebsiteHTTPSOp{WebsiteID: site.ID, Enable: req.EnableSSL, Type: constant.SSLExisted, WebsiteSSLID: req.SSLID, HttpConfig: constant.HTTPSOnly}); err != nil {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,149 @@
package service
import (
"encoding/json"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/files"
)
const tokenizerDownloadMaxSize = 256 << 20
type tokenizerDownloadMetadata struct {
Source string `json:"source"`
RepoID string `json:"repoID"`
Revision string `json:"revision"`
}
func filterTokenizerDownloadFiles(items []dto.ModelFile) []dto.ModelFile {
result := make([]dto.ModelFile, 0)
for _, item := range items {
name := item.Name
if !filepath.IsLocal(name) || strings.ContainsAny(name, "\\\x00\r\n") || filepath.ToSlash(filepath.Clean(name)) != name {
continue
}
if strings.Contains(name, "/") {
if strings.HasPrefix(name, "chat_templates/") && strings.HasSuffix(name, ".jinja") {
result = append(result, item)
}
continue
}
switch name {
case "config.json", "tokenizer_config.json", "tokenizer.json", "special_tokens_map.json", "added_tokens.json", "vocab.json", "vocab.txt", "merges.txt", "tekken.json":
result = append(result, item)
default:
if strings.HasSuffix(name, ".model") || strings.HasPrefix(name, "tokenizer.model") || strings.HasSuffix(name, ".tiktoken") || strings.HasSuffix(name, ".py") || strings.HasSuffix(name, ".jinja") {
result = append(result, item)
}
}
}
return result
}
func validateTokenizerDownloadFiles(items []dto.ModelFile) error {
if len(items) == 0 || len(items) > 512 {
return newModelDownloaderInputError("tokenizer must contain between 1 and 512 files")
}
names := make(map[string]bool, len(items))
var size int64
vocabulary := false
for _, item := range items {
if names[item.Name] || item.Size < 0 || item.Size > tokenizerDownloadMaxSize-size {
return newModelDownloaderInputError("invalid tokenizer files or total size exceeds 256 MiB")
}
size += item.Size
names[item.Name] = true
if item.Name == "tokenizer.json" || item.Name == "tekken.json" || item.Name == "vocab.txt" || strings.HasSuffix(item.Name, ".model") || strings.HasPrefix(item.Name, "tokenizer.model") || strings.HasSuffix(item.Name, ".tiktoken") {
vocabulary = true
}
}
if !vocabulary && !(names["vocab.json"] && names["merges.txt"]) {
return newModelDownloaderInputError("repository has no supported tokenizer vocabulary in its root directory")
}
return nil
}
func validateDownloadedTokenizer(root string) error {
var items []dto.ModelFile
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
stat, err := entry.Info()
if err != nil {
return err
}
if !stat.Mode().IsRegular() || stat.Size() > tokenizerDownloadMaxSize {
return newModelDownloaderInputError("invalid tokenizer file")
}
name, err := filepath.Rel(root, path)
if err != nil {
return err
}
items = append(items, dto.ModelFile{Name: filepath.ToSlash(name), Size: stat.Size()})
if strings.HasSuffix(name, ".json") {
content, err := os.ReadFile(path)
if err != nil {
return err
}
if !json.Valid(content) {
return newModelDownloaderInputError("invalid tokenizer JSON: %s", name)
}
}
return nil
})
if err != nil {
return err
}
return validateTokenizerDownloadFiles(items)
}
func listDownloadedTokenizers(info string) ([]dto.LocalModelItem, error) {
items := make([]dto.LocalModelItem, 0)
root := filepath.Join(global.CONF.Base.InstallDir, "1panel/ai/tokenizer")
directories, err := listModelDownloadDirectories(root)
if err != nil {
return nil, err
}
for _, item := range directories {
if _, err := normalizeModelDownloaderRepoID(item.Name); err != nil {
continue
}
path, err := filepath.EvalSymlinks(item.Path)
if err != nil {
return nil, err
}
content, err := os.ReadFile(filepath.Join(path, ".1panel-tokenizer"))
if os.IsNotExist(err) {
continue
}
if err != nil {
return nil, err
}
var metadata tokenizerDownloadMetadata
if err := json.Unmarshal(content, &metadata); err != nil {
return nil, err
}
if info != "" && !strings.Contains(strings.ToLower(metadata.RepoID), strings.ToLower(info)) {
continue
}
stat, err := os.Stat(path)
if err != nil {
return nil, err
}
size, err := files.NewFileOp().GetDirSize(path)
if err != nil {
return nil, err
}
items = append(items, dto.LocalModelItem{Name: item.Name, Path: path, RepoID: metadata.RepoID, Source: metadata.Source, Revision: metadata.Revision, DownloadType: "tokenizer", Size: size, SizeFormatted: formatBytes(size), CreatedAt: stat.ModTime().Format("2006-01-02 15:04:05")})
}
return items, nil
}
+65 -59
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -9,6 +10,7 @@ import (
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
"github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/compose-spec/compose-go/v2/interpolation" "github.com/compose-spec/compose-go/v2/interpolation"
"github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/template"
@@ -16,77 +18,73 @@ import (
func LoadModelGPUDevices() dto.ModelGPUDevices { func LoadModelGPUDevices() dto.ModelGPUDevices {
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}} result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern) ok, client := accelerator.New()
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern) if !ok {
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern) return result
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} { }
snapshot, err := client.Collect(context.Background())
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
return result
}
result.Warnings = append(result.Warnings, snapshot.Info.Warnings...)
for _, monitored := range snapshot.Devices {
if monitored.ParentID != "" {
continue
}
device := dto.ModelGPUDevice{Available: true}
devicePath := ""
switch {
case monitored.GPU != nil:
if monitored.Vendor == "nvidia" && !re.GetRegex(re.NvidiaGPUUUIDPattern).MatchString(monitored.GPU.UUID) {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID from nvidia-smi: %s", monitored.GPU.UUID))
continue
}
device.ID = monitored.ID
device.Name = fmt.Sprintf("GPU %d · %s", monitored.Index, monitored.Name)
device.Type = monitored.Vendor
device.UUID = monitored.GPU.UUID
case monitored.XPU != nil:
devicePath = monitored.XPU.Basic.DrmDevice
if !re.GetRegex(re.IntelGPUDevicePathPattern).MatchString(devicePath) {
directory := filepath.Join("/sys/bus/pci/devices", filepath.Base(monitored.BusID), "drm")
entries, err := os.ReadDir(directory) entries, err := os.ReadDir(directory)
if err != nil { if err != nil {
if !os.IsNotExist(err) { result.Warnings = append(result.Warnings, fmt.Sprintf("resolve Intel GPU %d render device: %v", monitored.Index, err))
result.Warnings = append(result.Warnings, err.Error())
}
continue continue
} }
devicePath = ""
for _, entry := range entries { for _, entry := range entries {
var device dto.ModelGPUDevice if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
var devicePath string
switch directory {
case "/proc/driver/nvidia/gpus":
informationPath := filepath.Join(directory, entry.Name(), "information")
content, err := os.ReadFile(informationPath)
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
fields := map[string]string{}
for _, line := range strings.Split(string(content), "\n") {
key, value, found := strings.Cut(line, ":")
if found {
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
uuid := fields["GPU UUID"]
minor, err := strconv.Atoi(fields["Device Minor"])
if !uuidPattern.MatchString(uuid) || err != nil || minor < 0 {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID or device minor in %s", informationPath))
continue
}
name := fields["Model"]
if name == "" {
name = "NVIDIA"
}
device = dto.ModelGPUDevice{ID: "nvidia:" + uuid, Name: name + " · " + entry.Name(), Type: "nvidia", UUID: uuid}
devicePath = filepath.Join("/dev", "nvidia"+strconv.Itoa(minor))
case "/sys/class/drm":
if !renderPattern.MatchString(entry.Name()) {
continue
}
vendor, err := os.ReadFile(filepath.Join(directory, entry.Name(), "device/vendor"))
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
if strings.TrimSpace(string(vendor)) != "0x8086" {
continue
}
devicePath = filepath.Join("/dev/dri", entry.Name()) devicePath = filepath.Join("/dev/dri", entry.Name())
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"} break
case "/dev": }
if !ascendPattern.MatchString(entry.Name()) { }
if devicePath == "" {
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
continue continue
} }
devicePath = filepath.Join(directory, entry.Name())
device = dto.ModelGPUDevice{ID: "ascend:" + strings.TrimPrefix(entry.Name(), "davinci"), Name: "Ascend · " + entry.Name(), Type: "ascend"}
} }
device.ID = "intel:" + devicePath
device.Name = fmt.Sprintf("XPU %d · %s", monitored.Index, monitored.Name)
device.Type = "intel"
case monitored.NPU != nil:
device.ID = fmt.Sprintf("ascend:%d", monitored.Index)
device.Name = fmt.Sprintf("NPU %d · Chip %d · %s", monitored.NPUIndex, monitored.ChipIndex, monitored.Name)
device.Type = "ascend"
devicePath = fmt.Sprintf("/dev/davinci%d", monitored.Index)
default:
continue
}
if devicePath != "" {
info, err := os.Stat(devicePath) info, err := os.Stat(devicePath)
device.Available = err == nil && info.Mode()&os.ModeCharDevice != 0
if err != nil { if err != nil {
result.Warnings = append(result.Warnings, err.Error()) result.Warnings = append(result.Warnings, err.Error())
} else { }
device.Available = info.Mode()&os.ModeCharDevice != 0
} }
result.Devices = append(result.Devices, device) result.Devices = append(result.Devices, device)
} }
}
return result return result
} }
@@ -649,9 +647,17 @@ func resolveModelGPUValue(value string, params map[string]interface{}) (string,
func loadModelAscendDeviceIDs() []string { func loadModelAscendDeviceIDs() []string {
ids := []string{} ids := []string{}
for _, device := range LoadModelGPUDevices().Devices { entries, err := os.ReadDir("/dev")
if device.Type == "ascend" && device.Available { if err != nil {
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:")) return ids
}
for _, entry := range entries {
if !re.GetRegex(re.AscendGPUDeviceNamePattern).MatchString(entry.Name()) {
continue
}
info, err := os.Stat(filepath.Join("/dev", entry.Name()))
if err == nil && info.Mode()&os.ModeCharDevice != 0 {
ids = append(ids, strings.TrimPrefix(entry.Name(), "davinci"))
} }
} }
slices.SortFunc(ids, func(a, b string) int { slices.SortFunc(ids, func(a, b string) int {
+141 -65
View File
@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -15,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai" "github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -25,13 +27,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
) )
var tensorRTLLMMu sync.Mutex
type TensorRTLLMService struct{} type TensorRTLLMService struct{}
type ITensorRTLLMService interface { type ITensorRTLLMService interface {
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error)
Create(create request.TensorRTLLMCreate) error Create(create request.TensorRTLLMCreate) error
Update(req request.TensorRTLLMUpdate) error Update(req request.TensorRTLLMUpdate) error
Delete(id uint) error Delete(req request.TensorRTLLMDelete) error
Operate(req request.TensorRTLLMOperate) error Operate(req request.TensorRTLLMOperate) error
} }
@@ -39,17 +43,29 @@ func NewITensorRTLLMService() ITensorRTLLMService {
return &TensorRTLLMService{} return &TensorRTLLMService{}
} }
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes { func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) (response.TensorRTLLMsRes, error) {
var ( var (
res response.TensorRTLLMsRes res response.TensorRTLLMsRes
items []response.TensorRTLLMDTO items []response.TensorRTLLMDTO
) )
total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize) total, data, err := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
if err != nil {
return res, err
}
for _, item := range data { for _, item := range data {
_ = syncTensorRTLLMContainerStatus(&item) operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, item.ID)
if err != nil {
return res, err
}
if operationTask.ID != "" {
item.Status = localEngineTaskStatus(operationTask.Operate, item.Status)
} else if err := syncTensorRTLLMContainerStatus(&item); err != nil {
return res, err
}
serverDTO := response.TensorRTLLMDTO{ serverDTO := response.TensorRTLLMDTO{
TensorRTLLM: item, TensorRTLLM: item,
TaskID: operationTask.ID,
} }
envs, _ := gotenv.Unmarshal(item.Env) envs, _ := gotenv.Unmarshal(item.Env)
serverDTO.Version = envs["VERSION"] serverDTO.Version = envs["VERSION"]
@@ -105,7 +121,7 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
} }
res.Total = total res.Total = total
res.Items = items res.Items = items
return res return res, nil
} }
func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) error { func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) error {
@@ -213,6 +229,8 @@ func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gote
} }
func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error { func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
tensorRTLLMMu.Lock()
defer tensorRTLLMMu.Unlock()
servers, _ := tensorrtLLMRepo.List() servers, _ := tensorrtLLMRepo.List()
for _, server := range servers { for _, server := range servers {
if server.ContainerName == create.ContainerName { if server.ContainerName == create.ContainerName {
@@ -236,11 +254,6 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
if !filesOp.Stat(tensorrtLLMDir) { if !filesOp.Stat(tensorrtLLMDir) {
_ = filesOp.CreateDir(tensorrtLLMDir, 0644) _ = filesOp.CreateDir(tensorrtLLMDir, 0644)
} }
if create.ModelSpeedup {
if err := handleModelArchive(create.ModelType, create.ModelDir); err != nil {
return err
}
}
tensorrtLLM := &model.TensorRTLLM{ tensorrtLLM := &model.TensorRTLLM{
Name: create.Name, Name: create.Name,
ContainerName: create.ContainerName, ContainerName: create.ContainerName,
@@ -267,15 +280,23 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil { if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
return err return err
} }
go startTensorRTLLM(tensorrtLLM) return runTensorRTLLMTask(tensorrtLLM, task.TaskCreate, "create", create.TaskID)
return nil
} }
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error { func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
tensorRTLLMMu.Lock()
defer tensorRTLLMMu.Unlock()
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID)) tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
} }
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, tensorrtLLM.ID)
if err != nil {
return err
}
if operationTask.ID != "" {
return buserr.New("TaskIsExecuting")
}
if tensorrtLLM.ContainerName != req.ContainerName { if tensorrtLLM.ContainerName != req.ContainerName {
if err := checkContainerName(req.ContainerName); err != nil { if err := checkContainerName(req.ContainerName); err != nil {
return err return err
@@ -305,59 +326,118 @@ func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil { if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
return err return err
} }
go startTensorRTLLM(tensorrtLLM) return runTensorRTLLMTask(tensorrtLLM, task.TaskUpdate, "update", req.TaskID)
return nil
} }
func (t TensorRTLLMService) Delete(id uint) error { func (t TensorRTLLMService) Delete(req request.TensorRTLLMDelete) error {
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(id)) tensorRTLLMMu.Lock()
if err != nil { defer tensorRTLLMMu.Unlock()
return err
}
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name))
return tensorrtLLMRepo.DeleteBy(repo.WithByID(id))
}
func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID)) tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
} }
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml") return runTensorRTLLMTask(tensorrtLLM, task.TaskDelete, "delete", req.TaskID)
var out string
switch req.Operate {
case "start":
out, err = compose.Up(composePath)
tensorrtLLM.Status = constant.StatusRunning
case "stop":
out, err = compose.Down(composePath)
tensorrtLLM.Status = constant.StatusStopped
case "restart":
out, err = compose.Restart(composePath)
tensorrtLLM.Status = constant.StatusRunning
}
if err != nil {
tensorrtLLM.Status = constant.StatusError
tensorrtLLM.Message = out
}
return tensorrtLLMRepo.Save(tensorrtLLM)
} }
func startTensorRTLLM(tensorrtLLM *model.TensorRTLLM) { func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml") tensorRTLLMMu.Lock()
if tensorrtLLM.Status != constant.StatusNormal { defer tensorRTLLMMu.Unlock()
_, _ = compose.Down(composePath) tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
} }
if out, err := compose.Up(composePath); err != nil { var operation string
tensorrtLLM.Status = constant.StatusError switch req.Operate {
tensorrtLLM.Message = out case "start":
operation = "Run"
case "stop":
operation = "Stop"
case "restart":
operation = task.TaskRestart
default:
return fmt.Errorf("unsupported TensorRT operation: %s", req.Operate)
}
return runTensorRTLLMTask(tensorrtLLM, operation, req.Operate, req.TaskID)
}
func runTensorRTLLMTask(llm *model.TensorRTLLM, operation, action, taskID string) error {
currentTask, err := taskRepo.GetExecutingTask(task.TaskScopeTensorRT, llm.ID)
if err != nil {
return err
}
if currentTask.ID != "" {
return buserr.New("TaskIsExecuting")
}
operationTask, err := task.NewTaskWithOps(llm.Name, operation, task.TaskScopeTensorRT, taskID, llm.ID)
if err != nil {
return err
}
operationTask.AddSubTaskWithOps(operationTask.Name, func(t *task.Task) error {
if action == "stop" {
llm.Status = constant.StatusWaiting
} else { } else {
tensorrtLLM.Status = constant.StatusRunning llm.Status = localEngineTaskStatus(operation, constant.StatusStarting)
tensorrtLLM.Message = ""
} }
_ = syncTensorRTLLMContainerStatus(tensorrtLLM) if err := tensorrtLLMRepo.Save(llm); err != nil {
return err
}
composePath := path.Join(global.Dir.TensorRTLLMDir, llm.Name, "docker-compose.yml")
var out string
var err error
switch action {
case "create":
if llm.ModelSpeedup {
envs, err := gotenv.Unmarshal(llm.Env)
if err != nil {
return err
}
if err := handleModelArchive(llm.ModelType, envs["MODEL_PATH"]); err != nil {
return err
}
}
out, err = compose.Up(composePath)
case "start":
out, err = compose.Up(composePath)
case "update":
out, err = compose.Down(composePath)
if strings.TrimSpace(out) != "" {
t.Log(out)
}
if err != nil {
return err
}
out, err = compose.Up(composePath)
case "stop":
out, err = compose.Stop(composePath)
case "restart":
out, err = compose.Restart(composePath)
case "delete":
out, err = compose.Down(composePath)
default:
return fmt.Errorf("unsupported TensorRT operation: %s", action)
}
if strings.TrimSpace(out) != "" {
t.Log(out)
}
if err != nil {
return err
}
if action == "delete" {
if err := files.NewFileOp().DeleteDir(path.Join(global.Dir.TensorRTLLMDir, llm.Name)); err != nil {
return err
}
return tensorrtLLMRepo.DeleteBy(repo.WithByID(llm.ID))
}
if err := syncTensorRTLLMContainerStatus(llm); err != nil {
return err
}
llm.Message = ""
return tensorrtLLMRepo.Save(llm)
}, nil, 0, 0)
go func() {
_ = operationTask.Execute()
}()
return nil
} }
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error { func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
@@ -372,28 +452,24 @@ func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
return err return err
} }
if len(containers) == 0 { if len(containers) == 0 {
if tensorrtLLM.Status == constant.StatusStarting { tensorrtLLM.Status = constant.StatusError
tensorrtLLM.Message = buserr.WithName("ErrContainerNotFound", tensorrtLLM.ContainerName).Error()
return nil return nil
} }
tensorrtLLM.Status = constant.StatusStopped
return tensorrtLLMRepo.Save(tensorrtLLM)
}
container := containers[0] container := containers[0]
switch container.State { switch container.State {
case "exited": case "exited":
tensorrtLLM.Status = constant.StatusError tensorrtLLM.Status = constant.StatusStopped
case "running": case "running":
tensorrtLLM.Status = constant.StatusRunning tensorrtLLM.Status = constant.StatusRunning
case "paused": case "paused":
tensorrtLLM.Status = constant.StatusStopped tensorrtLLM.Status = constant.StatusPaused
case "restarting": case "restarting":
tensorrtLLM.Status = constant.StatusRestarting tensorrtLLM.Status = constant.StatusRestarting
default: default:
if tensorrtLLM.Status != constant.StatusStarting { tensorrtLLM.Status = constant.StatusUnHealthy
tensorrtLLM.Status = constant.StatusStopped
} }
} return nil
return tensorrtLLMRepo.Save(tensorrtLLM)
} }
func findModelArchive(modelType string) (string, error) { func findModelArchive(modelType string) (string, error) {
+171
View File
@@ -0,0 +1,171 @@
package service
import (
"os"
"path/filepath"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/jinzhu/copier"
)
func (v *VMService) SearchIso(req dto.SearchPageWithType) (int64, interface{}, error) {
options := []repo.DBOption{repo.WithOrderDesc("created_at"), repo.WithOrderDesc("id")}
if req.Type != "" {
options = append(options, repo.WithByType(req.Type))
}
if req.Info != "" {
options = append(options, repo.WithByLikeName(req.Info))
}
total, isos, err := vmRepo.PageIso(req.Page, req.PageSize, options...)
if err != nil {
return 0, nil, err
}
dtoIsos := make([]dto.VMIsoInfo, 0, len(isos))
for _, iso := range isos {
var item dto.VMIsoInfo
if err := copier.Copy(&item, &iso); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
}
item.IsExist, item.Size = loadVMResourceFileInfo(item.Path)
dtoIsos = append(dtoIsos, item)
}
return total, dtoIsos, err
}
func (v *VMService) ListIso() ([]dto.VMIsoInfo, error) {
isos, err := vmRepo.ListIsos()
if err != nil {
return nil, err
}
dtoIsos := make([]dto.VMIsoInfo, 0, len(isos))
for _, iso := range isos {
if iso.Status == "uploading" || iso.Status == "downloading" || iso.Status == "failed" {
continue
}
var item dto.VMIsoInfo
if err := copier.Copy(&item, &iso); err != nil {
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
}
item.IsExist, item.Size = loadVMResourceFileInfo(item.Path)
dtoIsos = append(dtoIsos, item)
}
return dtoIsos, nil
}
func (v *VMService) CreateIso(req dto.VMIsoCreate) error {
isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name))
if isoItem.ID != 0 {
return buserr.New("ErrRecordExist")
}
exists, _ := loadVMResourceFileInfo(req.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
return vmRepo.CreateIso(&model.VMIso{
Type: req.Type, Name: req.Name, Path: req.Path, Status: "ready", Description: req.Description,
})
}
func (v *VMService) StartIsoImport(req dto.VMIsoImportStart) (uint, error) {
isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name))
if isoItem.ID != 0 {
return 0, buserr.New("ErrRecordExist")
}
item := model.VMIso{Type: req.Type, Name: req.Name, Path: req.Path, Status: req.Status, TempFileID: req.TempFileID, TempFileType: req.TempFileType, Description: req.Description}
if err := vmRepo.CreateIso(&item); err != nil {
return 0, err
}
return item.ID, nil
}
func (v *VMService) UpdateIsoImport(req dto.VMIsoImportUpdate) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil || isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if isoItem.Status != "uploading" && isoItem.Status != "downloading" {
return buserr.WithDetail("ErrVMInvalidInput", "ISO import is not active", nil)
}
if (isoItem.Status == "uploading" && req.Status == "downloading") ||
(isoItem.Status == "downloading" && req.Status == "uploading") {
return buserr.WithDetail("ErrVMInvalidInput", "ISO import type cannot be changed", nil)
}
vars := map[string]interface{}{"status": req.Status, "error": req.Error}
if req.TempFileID != "" {
vars["temp_file_id"] = req.TempFileID
}
if req.TempFileType != "" {
vars["temp_file_type"] = req.TempFileType
}
if req.Status == "ready" {
exists, _ := loadVMResourceFileInfo(isoItem.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
vars["error"] = ""
}
if req.Status == "ready" {
vars["temp_file_id"] = ""
vars["temp_file_type"] = ""
}
return vmRepo.UpdateIso(req.ID, vars)
}
func (v *VMService) UpdateIso(req dto.VMIsoUpdate) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil {
return err
}
if isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
exists, _ := loadVMResourceFileInfo(req.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
return vmRepo.UpdateIso(req.ID, map[string]interface{}{
"name": req.Name, "type": req.Type, "path": req.Path, "description": req.Description,
})
}
func (v *VMService) DeleteIso(req dto.VMIsoDelete) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil {
return err
}
if isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if req.DeleteFile && isoItem.Path != "" {
exists, _ := loadVMResourceFileInfo(isoItem.Path)
if exists {
if err := os.Remove(isoItem.Path); err != nil && !os.IsNotExist(err) {
return err
}
}
}
if isoItem.TempFileType == "upload" && validTempFileID(isoItem.TempFileID) {
tmpPath := filepath.Join(global.CONF.Base.InstallDir, "1panel", "tmp", "upload", isoItem.TempFileID)
if err := os.RemoveAll(tmpPath); err != nil {
return err
}
}
return vmRepo.DeleteIso(repo.WithByID(req.ID))
}
func loadVMResourceFileInfo(filePath string) (bool, int64) {
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
return false, 0
}
return true, info.Size()
}
func validTempFileID(value string) bool {
return value != "" && filepath.Base(value) == value && !strings.ContainsAny(value, `/\\`)
}
+464
View File
@@ -0,0 +1,464 @@
package service
import (
"context"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/vm/types"
"gorm.io/gorm"
)
func (v *VMService) ListNetworks() ([]dto.VMNetworkItem, error) {
records, err := vmRepo.ListNetworks()
if err != nil {
return nil, err
}
items := v.networkRecordsToItems(records)
return items, nil
}
func (v *VMService) ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) {
return listNetworkBridges("/sys/class/net")
}
func (v *VMService) ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
return listMacvtapInterfaces("/sys/class/net", interfaces)
}
func (v *VMService) CreateNetwork(req dto.VMNetworkCreate) error {
if req.Type == "macvtap" {
interfaces, err := v.ListNetworkInterfaces()
if err != nil {
return err
}
found := false
for _, item := range interfaces {
if item.Name == req.SourceInterface {
found = true
break
}
}
if !found {
return fmt.Errorf("unavailable macvtap source interface: %s", req.SourceInterface)
}
} else {
req.SourceInterface = ""
}
if req.Type != "nat" {
req.CIDR, req.Gateway, req.DHCPStart, req.DHCPEnd = "", "", "", ""
}
if req.Type == "macvtap" {
req.BridgeName = ""
}
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
unlock, err := vmResourceLocks.lock(ctx, "network", req.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByName(req.Name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if itemNetwork.ID != 0 {
return buserr.New("ErrRecordExist")
}
if req.Type == "bridge" && isManagedNetworkBridge(req.BridgeName) {
return buserr.New("ErrVMBridgeManaged")
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
if _, exists, err := v.findVMResourceWithContext(ctx, "network", req.Name); err != nil {
return err
} else if exists {
return buserr.New("ErrRecordExist")
}
record := model.VMNetwork{
Name: req.Name, Type: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description,
}
if err := vmRepo.CreateNetwork(&record); err != nil {
return err
}
if err := v.provider.CreateResource(ctx, types.VMResourceCreate{
Type: "network", Name: req.Name, SubType: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart,
}); err != nil {
if updateErr := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusFailed, "error": err.Error(),
}); updateErr != nil {
return errors.Join(err, fmt.Errorf("save network creation failure: %w", updateErr))
}
return err
}
return vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusRunning, "error": "",
})
}
func (v *VMService) SyncNetworks() error {
revision, idle := vmResourceLocks.snapshot("network")
if !idle {
return nil
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(context.Background(), "network")
if err != nil {
return err
}
return vmResourceLocks.applySnapshot("network", revision, func() error {
records, err := vmRepo.ListNetworks()
if err != nil {
return err
}
liveByName := map[string]dto.VMResourceInfo{}
for _, live := range liveItems {
if live.Name == "" {
continue
}
liveByName[live.Name] = live
}
recordsByName := map[string]model.VMNetwork{}
for _, record := range records {
if vmResourceStatusEquals(record.Status, vmResourceCreating) {
record.Status = constant.StatusFailed
record.Error = "Network creation was interrupted before its result was saved"
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": record.Status, "error": record.Error,
}); err != nil {
return err
}
}
recordsByName[record.Name] = record
}
for name, live := range liveByName {
vars := map[string]interface{}{
"type": live.SubType,
"bridge_name": live.BridgeName,
"source_interface": live.SourceInterface,
"cidr": live.CIDR,
"gateway": live.Gateway,
"dhcp_start": live.DHCPStart,
"dhcp_end": live.DHCPEnd,
"auto_start": live.AutoStart,
"status": normalizeVMResourceStatus(live.Status),
}
if record, ok := recordsByName[name]; ok {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if err := vmRepo.UpdateNetwork(record.ID, vars); err != nil {
return err
}
continue
}
if err := vmRepo.CreateNetwork(&model.VMNetwork{
Name: live.Name, Type: live.SubType, BridgeName: live.BridgeName, SourceInterface: live.SourceInterface,
CIDR: live.CIDR, Gateway: live.Gateway, DHCPStart: live.DHCPStart, DHCPEnd: live.DHCPEnd,
AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status),
}); err != nil {
return err
}
}
for _, record := range recordsByName {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if _, ok := liveByName[record.Name]; !ok {
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusDeleted,
}); err != nil {
return err
}
}
}
return nil
})
}
func (v *VMService) UpdateNetwork(req dto.VMNetworkUpdate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
unlock, err := vmResourceLocks.lock(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if itemNetwork.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
vars := map[string]interface{}{}
if req.Description != nil {
vars["description"] = *req.Description
}
if req.AutoStart != nil || req.Status != nil {
if vmResourceCreationIncomplete(itemNetwork.Status) {
return buserr.WithDetail("ErrInvalidParams", "network creation has not succeeded", nil)
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
live, ok, err := v.findVMResourceWithContext(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
if !ok {
_ = vmRepo.UpdateNetwork(req.ID, map[string]interface{}{"status": constant.StatusDeleted})
return buserr.WithDetail("ErrResourceNotExists", "Network does not exist", nil)
}
if live.Status != "" {
vars["status"] = normalizeVMResourceStatus(live.Status)
}
}
if req.AutoStart != nil {
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, AutoStart: req.AutoStart,
}); err != nil {
return err
}
vars["auto_start"] = *req.AutoStart
}
if req.Status != nil {
status := normalizeVMResourceStatus(*req.Status)
if !validVMResourceOperateStatus(status) {
return buserr.WithDetail("ErrInvalidParams", status, nil)
}
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, Status: &status,
}); err != nil {
return err
}
vars["status"] = status
}
if len(vars) == 0 {
return nil
}
return vmRepo.UpdateNetwork(req.ID, vars)
}
func (v *VMService) DeleteNetwork(req dto.VMResourceDelete) error {
ctx, cancel := vmResourceOperationContext(req.Force)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrRecordNotFound")
}
return err
}
unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "network", itemNetwork.Name, req.Force)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if len(loadVMResourceRefs("network")[itemNetwork.Name]) > 0 {
return buserr.New("ErrVMNetworkInUse")
}
if cleanup {
if err := v.deleteVMResource(ctx, "network", itemNetwork.Name, req.Force); err != nil {
return err
}
}
return vmRepo.DeleteNetwork(repo.WithByID(req.ID))
}
func (v *VMService) networkRecordsToItems(records []model.VMNetwork) []dto.VMNetworkItem {
items := make([]dto.VMNetworkItem, 0, len(records))
vmRefs := loadVMResourceRefs("network")
usedIPs := v.loadVMNetworkIPUsages(records)
for _, record := range records {
status := normalizeVMResourceStatus(record.Status)
items = append(items, dto.VMNetworkItem{
ID: record.ID, Name: record.Name, Type: record.Type,
BridgeName: record.BridgeName, SourceInterface: record.SourceInterface, CIDR: record.CIDR, Gateway: record.Gateway,
DHCPStart: record.DHCPStart, DHCPEnd: record.DHCPEnd, AutoStart: record.AutoStart,
Description: record.Description, CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
Status: status, Error: record.Error, VMCount: len(vmRefs[record.Name]), VMs: vmRefs[record.Name],
Registered: true, UsedIPs: usedIPs[record.Name],
})
}
return items
}
func (v *VMService) loadVMNetworkIPUsages(records []model.VMNetwork) map[string][]dto.VMNetworkIPUsage {
usages := map[string][]dto.VMNetworkIPUsage{}
if !v.helperExists() {
return usages
}
domains, err := v.provider.List(context.Background())
if err != nil {
return usages
}
vmRecords, err := vmRepo.List()
if err != nil {
return usages
}
vmRecordsByName := map[string]model.VirtualMachine{}
for _, record := range vmRecords {
vmRecordsByName[record.Name] = record
}
networkNames := map[string]struct{}{}
bridgeToNetwork := map[string]string{}
for _, record := range records {
networkNames[record.Name] = struct{}{}
if record.BridgeName != "" {
bridgeToNetwork[record.BridgeName] = record.Name
}
}
for _, domain := range domains {
if record, ok := vmRecordsByName[domain.Name]; ok {
mergeVMMetadata(&domain, record)
}
for _, item := range domain.Interfaces {
ip := strings.TrimSpace(item.IP)
if ip == "" {
continue
}
networkName := resolveVMInterfaceNetwork(item.Source, domain.Network, networkNames, bridgeToNetwork)
if networkName == "" {
continue
}
usages[networkName] = append(usages[networkName], dto.VMNetworkIPUsage{
IP: ip, VMName: domain.Name, MAC: item.MAC, Source: item.Source,
})
}
}
for name := range usages {
sort.Slice(usages[name], func(i, j int) bool {
if usages[name][i].IP != usages[name][j].IP {
return usages[name][i].IP < usages[name][j].IP
}
return strings.ToLower(usages[name][i].VMName) < strings.ToLower(usages[name][j].VMName)
})
}
return usages
}
func resolveVMInterfaceNetwork(source, vmNetwork string, networkNames map[string]struct{}, bridgeToNetwork map[string]string) string {
if _, ok := networkNames[source]; ok {
return source
}
if _, ok := networkNames[vmNetwork]; ok {
return vmNetwork
}
return bridgeToNetwork[source]
}
func listNetworkBridges(netClassPath string) ([]dto.VMNetworkBridgeItem, error) {
entries, err := os.ReadDir(netClassPath)
if err != nil {
if os.IsNotExist(err) {
return []dto.VMNetworkBridgeItem{}, nil
}
return nil, err
}
items := make([]dto.VMNetworkBridgeItem, 0)
for _, entry := range entries {
name := entry.Name()
netPath := netClassPath + "/" + name
info, err := os.Stat(netPath)
if err != nil || !info.IsDir() {
continue
}
if _, err := os.Stat(netPath + "/bridge"); err != nil {
continue
}
item := dto.VMNetworkBridgeItem{
Name: name,
Recommended: true,
}
switch {
case name == "docker0" || strings.HasPrefix(name, "br-"):
item.Recommended = false
item.Description = "docker"
case name == "virbr0" || strings.HasPrefix(name, "virbr"):
item.Recommended = false
item.Description = "libvirt"
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Recommended != items[j].Recommended {
return items[i].Recommended
}
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
})
return items, nil
}
func isManagedNetworkBridge(name string) bool {
return name == "docker0" || strings.HasPrefix(name, "br-") || name == "virbr0" || strings.HasPrefix(name, "virbr")
}
func listMacvtapInterfaces(netClassPath string, interfaces []net.Interface) ([]dto.VMNetworkInterfaceItem, error) {
items := make([]dto.VMNetworkInterfaceItem, 0)
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) != 6 {
continue
}
netPath := filepath.Join(netClassPath, iface.Name)
if _, err := os.Stat(filepath.Join(netPath, "device")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
continue
}
excluded := false
for _, name := range []string{"master", "wireless", "bridge"} {
if _, err := os.Stat(filepath.Join(netPath, name)); err == nil {
excluded = true
break
} else if !os.IsNotExist(err) {
return nil, err
}
}
if excluded {
continue
}
addresses, err := iface.Addrs()
if err != nil {
return nil, err
}
item := dto.VMNetworkInterfaceItem{Name: iface.Name, Addresses: []string{}}
for _, address := range addresses {
item.Addresses = append(item.Addresses, address.String())
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
return items, nil
}
+36
View File
@@ -0,0 +1,36 @@
package service
import (
"context"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
)
func (v *VMService) deleteVMResource(ctx context.Context, resourceType, name string, force bool) error {
if err := v.deleteVMResourceWithContext(ctx, resourceType, name); err != nil {
if !force {
return err
}
if global.LOG != nil {
global.LOG.Warnf("[xpack] force delete VM %s %q metadata after resource cleanup failed: %v", resourceType, name, err)
}
}
return nil
}
func (v *VMService) deleteVMResourceWithContext(ctx context.Context, resourceType, name string) error {
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
items, err := v.provider.ListResources(ctx, resourceType)
if err != nil {
return err
}
for _, item := range items {
if item.Name == name {
return v.provider.DeleteResource(ctx, resourceType, name)
}
}
return nil
}
+111
View File
@@ -0,0 +1,111 @@
package service
import (
"context"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
)
type vmResourceKey struct{ kind, name string }
type vmResourceGate struct {
token chan struct{}
refs int
}
type vmResourceLockSet struct {
mu sync.Mutex
gates map[vmResourceKey]*vmResourceGate
active map[string]int
revision map[string]uint64
}
var vmResourceLocks = newVMResourceLockSet()
func newVMResourceLockSet() *vmResourceLockSet {
return &vmResourceLockSet{gates: make(map[vmResourceKey]*vmResourceGate), active: make(map[string]int), revision: make(map[string]uint64)}
}
func vmResourceOperationContext(force bool) (context.Context, context.CancelFunc) {
timeout := 2 * time.Minute
if force {
timeout = 10 * time.Second
}
return context.WithTimeout(context.Background(), timeout)
}
func (s *vmResourceLockSet) lock(ctx context.Context, kind, name string) (func(), error) {
key := vmResourceKey{kind, name}
s.mu.Lock()
gate := s.gates[key]
if gate == nil {
gate = &vmResourceGate{token: make(chan struct{}, 1)}
gate.token <- struct{}{}
s.gates[key] = gate
}
gate.refs++
s.mu.Unlock()
select {
case <-ctx.Done():
s.mu.Lock()
s.dropRef(key, gate)
s.mu.Unlock()
return nil, ctx.Err()
case <-gate.token:
}
s.mu.Lock()
if err := ctx.Err(); err != nil {
gate.token <- struct{}{}
s.dropRef(key, gate)
s.mu.Unlock()
return nil, err
}
s.active[kind]++
s.revision[kind]++
s.mu.Unlock()
return func() {
s.mu.Lock()
s.active[kind]--
s.revision[kind]++
gate.token <- struct{}{}
s.dropRef(key, gate)
s.mu.Unlock()
}, nil
}
func (s *vmResourceLockSet) dropRef(key vmResourceKey, gate *vmResourceGate) {
gate.refs--
if gate.refs == 0 {
delete(s.gates, key)
}
}
func (s *vmResourceLockSet) lockForDelete(ctx context.Context, kind, name string, force bool) (func(), bool, error) {
unlock, err := s.lock(ctx, kind, name)
if err == nil {
return unlock, true, nil
}
if !force {
return nil, false, err
}
if global.LOG != nil {
global.LOG.Warnf("[xpack] force delete VM %s %q metadata after waiting for resource lock failed: %v", kind, name, err)
}
s.mu.Lock()
s.revision[kind]++
return func() { s.revision[kind]++; s.mu.Unlock() }, false, nil
}
func (s *vmResourceLockSet) snapshot(kind string) (uint64, bool) {
s.mu.Lock()
defer s.mu.Unlock()
return s.revision[kind], s.active[kind] == 0
}
func (s *vmResourceLockSet) applySnapshot(kind string, revision uint64, apply func() error) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.active[kind] != 0 || s.revision[kind] != revision {
return nil
}
defer func() { s.revision[kind]++ }()
return apply()
}
+55
View File
@@ -0,0 +1,55 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
)
const vmResourceCreating = "creating"
func vmResourceCreationIncomplete(status string) bool {
return vmResourceStatusEquals(status, vmResourceCreating) || vmResourceStatusEquals(status, constant.StatusFailed)
}
func normalizeVMResourceStatus(status string) string {
status = strings.TrimSpace(status)
if status == "" {
return constant.StatusUnknown
}
switch strings.ToLower(status) {
case vmResourceCreating:
return vmResourceCreating
case strings.ToLower(constant.StatusFailed):
return constant.StatusFailed
case strings.ToLower(constant.StatusRunning):
return constant.StatusRunning
case strings.ToLower(constant.StatusInactive):
return constant.StatusInactive
case strings.ToLower(constant.StatusDeleted):
return constant.StatusDeleted
case strings.ToLower(constant.StatusUnknown):
return constant.StatusUnknown
case strings.ToLower(constant.StatusBuilding):
return constant.StatusBuilding
case strings.ToLower(constant.StatusDegraded):
return constant.StatusDegraded
case strings.ToLower(constant.StatusInaccessible):
return constant.StatusInaccessible
default:
return status
}
}
func vmResourceStatusEquals(status string, target string) bool {
return strings.EqualFold(strings.TrimSpace(status), target)
}
func vmResourceExists(status string) bool {
status = normalizeVMResourceStatus(status)
return !vmResourceStatusEquals(status, constant.StatusDeleted)
}
func validVMResourceOperateStatus(status string) bool {
return vmResourceStatusEquals(status, constant.StatusRunning) || vmResourceStatusEquals(status, constant.StatusInactive)
}
+403
View File
@@ -0,0 +1,403 @@
package service
import (
"context"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/vm/types"
"gorm.io/gorm"
)
func (v *VMService) ListStorages() ([]dto.VMStorageItem, error) {
records, err := vmRepo.ListStorages()
if err != nil {
return nil, err
}
items := v.mergeStorageItems(records)
return items, nil
}
func (v *VMService) CreateStorage(req dto.VMStorageCreate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
unlock, err := vmResourceLocks.lock(ctx, "storagePool", req.Name)
if err != nil {
return err
}
defer unlock()
req.Path = strings.TrimSpace(req.Path)
unlockPath, err := vmResourceLocks.lock(ctx, "storagePath", normalizeStoragePath(req.Path))
if err != nil {
return err
}
defer unlockPath()
itemStorage, err := vmRepo.GetStorage(repo.WithByName(req.Name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if itemStorage.ID != 0 {
return buserr.New("ErrRecordExist")
}
records, err := vmRepo.ListStorages()
if err != nil {
return err
}
for _, record := range records {
if sameStoragePath(record.Path, req.Path) {
return buserr.WithName("ErrVMStoragePathInUse", record.Name)
}
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(ctx, "storagePool")
if err != nil {
return err
}
for _, live := range liveItems {
if live.Name == req.Name {
return buserr.New("ErrRecordExist")
}
if sameStoragePath(live.Path, req.Path) {
return buserr.WithName("ErrVMStoragePathInUse", live.Name)
}
}
record := model.VMStorage{
Name: req.Name, Type: req.Type, Path: req.Path,
AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description,
}
if err := vmRepo.CreateStorage(&record); err != nil {
return err
}
if err := v.provider.CreateResource(ctx, types.VMResourceCreate{
Type: "storagePool", Name: req.Name, SubType: req.Type, Path: req.Path, AutoStart: req.AutoStart,
}); err != nil {
if updateErr := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusFailed, "error": err.Error(),
}); updateErr != nil {
return errors.Join(err, fmt.Errorf("save storage pool creation failure: %w", updateErr))
}
return err
}
return vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusRunning, "error": "",
})
}
func sameStoragePath(left, right string) bool {
left = normalizeStoragePath(left)
right = normalizeStoragePath(right)
return left != "" && left == right
}
func normalizeStoragePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
return filepath.Clean(path)
}
func (v *VMService) SyncStorages() error {
revision, idle := vmResourceLocks.snapshot("storagePool")
if !idle {
return nil
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(context.Background(), "storagePool")
if err != nil {
return err
}
return vmResourceLocks.applySnapshot("storagePool", revision, func() error {
records, err := vmRepo.ListStorages()
if err != nil {
return err
}
liveByName := map[string]dto.VMResourceInfo{}
for _, live := range liveItems {
if live.Name == "" {
continue
}
liveByName[live.Name] = live
}
recordsByName := map[string]model.VMStorage{}
for _, record := range records {
if vmResourceStatusEquals(record.Status, vmResourceCreating) {
record.Status = constant.StatusFailed
record.Error = "Storage pool creation was interrupted before its result was saved"
if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": record.Status, "error": record.Error,
}); err != nil {
return err
}
}
recordsByName[record.Name] = record
}
for name, live := range liveByName {
vars := map[string]interface{}{
"type": live.SubType,
"path": live.Path,
"auto_start": live.AutoStart,
"status": normalizeVMResourceStatus(live.Status),
}
if record, ok := recordsByName[name]; ok {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if err := vmRepo.UpdateStorage(record.ID, vars); err != nil {
return err
}
continue
}
if err := vmRepo.CreateStorage(&model.VMStorage{
Name: live.Name, Type: live.SubType, Path: live.Path,
AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status),
}); err != nil {
return err
}
}
for _, record := range recordsByName {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if _, ok := liveByName[record.Name]; !ok {
if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusDeleted,
}); err != nil {
return err
}
}
}
return nil
})
}
func (v *VMService) UpdateStorage(req dto.VMStorageUpdate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
unlock, err := vmResourceLocks.lock(ctx, "storagePool", itemStorage.Name)
if err != nil {
return err
}
defer unlock()
itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
if itemStorage.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
vars := map[string]interface{}{}
if req.Name != "" && req.Name != itemStorage.Name {
return buserr.New("ErrVMStorageNameCannotChange")
}
if req.Path != "" && req.Path != itemStorage.Path {
return buserr.New("ErrVMStoragePathCannotChange")
}
if req.Description != nil {
vars["description"] = *req.Description
}
if req.AutoStart != nil || req.Status != nil {
if vmResourceCreationIncomplete(itemStorage.Status) {
return buserr.WithDetail("ErrInvalidParams", "storage pool creation has not succeeded", nil)
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
live, ok, err := v.findVMResourceWithContext(ctx, "storagePool", itemStorage.Name)
if err != nil {
return err
}
if !ok {
_ = vmRepo.UpdateStorage(req.ID, map[string]interface{}{"status": constant.StatusDeleted})
return buserr.WithDetail("ErrResourceNotExists", "Storage pool does not exist", nil)
}
if live.Status != "" {
vars["status"] = normalizeVMResourceStatus(live.Status)
}
}
if req.AutoStart != nil {
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "storagePool", Name: itemStorage.Name, AutoStart: req.AutoStart,
}); err != nil {
return err
}
vars["auto_start"] = *req.AutoStart
}
if req.Status != nil {
status := normalizeVMResourceStatus(*req.Status)
if !validVMResourceOperateStatus(status) {
return buserr.WithDetail("ErrInvalidParams", status, nil)
}
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "storagePool", Name: itemStorage.Name, Status: &status,
}); err != nil {
return err
}
vars["status"] = status
}
if len(vars) == 0 {
return nil
}
return vmRepo.UpdateStorage(req.ID, vars)
}
func (v *VMService) DeleteStorage(req dto.VMResourceDelete) error {
ctx, cancel := vmResourceOperationContext(req.Force)
defer cancel()
itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrRecordNotFound")
}
return err
}
unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "storagePool", itemStorage.Name, req.Force)
if err != nil {
return err
}
defer unlock()
itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
if len(loadVMResourceRefs("storagePool")[itemStorage.Name]) > 0 {
return buserr.New("ErrVMStorageInUse")
}
if cleanup {
if err := v.deleteVMResource(ctx, "storagePool", itemStorage.Name, req.Force); err != nil {
return err
}
}
return vmRepo.DeleteStorage(repo.WithByID(req.ID))
}
func (v *VMService) mergeStorageItems(records []model.VMStorage) []dto.VMStorageItem {
itemsByValue := map[string]dto.VMStorageItem{}
for _, record := range records {
itemsByValue[record.Name] = dto.VMStorageItem{
ID: record.ID, Name: record.Name, Type: record.Type,
Path: record.Path, AutoStart: record.AutoStart, Description: record.Description,
CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
Status: normalizeVMResourceStatus(record.Status),
Error: record.Error,
}
}
if v.helperExists() {
if liveItems, err := v.provider.ListResources(context.Background(), "storagePool"); err == nil {
for _, live := range liveItems {
item, ok := itemsByValue[live.Name]
if !ok || vmResourceCreationIncomplete(item.Status) {
continue
}
item.Type = live.SubType
item.Status = normalizeVMResourceStatus(live.Status)
item.AutoStart = live.AutoStart
item.Capacity = live.Capacity
item.Available = live.Available
itemsByValue[live.Name] = item
}
}
}
vmRefs := loadVMResourceRefs("storagePool")
items := make([]dto.VMStorageItem, 0, len(records))
for _, record := range records {
item := itemsByValue[record.Name]
if item.Status == "" {
item.Status = constant.StatusDeleted
}
item.VMs = vmRefs[record.Name]
item.VMCount = len(item.VMs)
items = append(items, item)
}
return items
}
func loadVMResourceRefs(resourceType string) map[string][]dto.VMResourceRef {
records, err := vmRepo.List()
if err != nil {
return map[string][]dto.VMResourceRef{}
}
refs := map[string][]dto.VMResourceRef{}
for _, record := range records {
ref := dto.VMResourceRef{ID: record.ID, Name: record.Name}
switch resourceType {
case "network":
if record.Network != "" {
refs[record.Network] = append(refs[record.Network], ref)
}
case "storagePool":
if record.StoragePool != "" {
refs[record.StoragePool] = append(refs[record.StoragePool], ref)
}
}
}
for name := range refs {
sort.Slice(refs[name], func(i, j int) bool {
return strings.ToLower(refs[name][i].Name) < strings.ToLower(refs[name][j].Name)
})
}
return refs
}
func (v *VMService) fillVMStoragePools(items []dto.VMInfo) {
needsPools := false
for _, item := range items {
if item.StoragePool == "" && item.DiskPath != "" {
needsPools = true
break
}
}
if !needsPools {
return
}
pools, err := v.provider.ListResources(context.Background(), "storagePool")
if err != nil {
global.LOG.Warnf("[xpack] resolve VM storage pools failed, retaining metadata: %v", err)
return
}
for i := range items {
if items[i].StoragePool == "" {
items[i].StoragePool = storagePoolForDisk(items[i].DiskPath, pools)
}
}
}
func storagePoolForDisk(diskPath string, pools []dto.VMResourceInfo) string {
if !filepath.IsAbs(diskPath) {
return ""
}
poolName, bestLength := "", 0
for _, pool := range pools {
poolPath := normalizeStoragePath(pool.Path)
if pool.Name == "" || !filepath.IsAbs(poolPath) {
continue
}
rel, err := filepath.Rel(poolPath, diskPath)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
continue
}
if len(poolPath) > bestLength {
poolName, bestLength = pool.Name, len(poolPath)
} else if len(poolPath) == bestLength && poolName != pool.Name {
poolName = ""
}
}
return poolName
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -280,6 +280,8 @@ func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp {
} }
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) { func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
alias := create.Alias alias := create.Alias
if alias == "default" { if alias == "default" {
return buserr.New("ErrDefaultAlias") return buserr.New("ErrDefaultAlias")
@@ -377,6 +379,11 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
website.HttpConfig = sslReq.HttpConfig website.HttpConfig = sslReq.HttpConfig
initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq} initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq}
} }
candidate := *website
candidate.Domains = domains
if err = checkWebsiteListenConflict(candidate, defaultHttpPort, nginxInstall.HttpsPort); err != nil {
return err
}
} }
createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0) createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0)
@@ -1004,6 +1011,8 @@ func applyWebsiteHTTPSParams(res *response.WebsiteHTTPS, params []response.Nginx
} }
func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) { func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID)) website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1012,6 +1021,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH
res response.WebsiteHTTPS res response.WebsiteHTTPS
websiteSSL model.WebsiteSSL websiteSSL model.WebsiteSSL
) )
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
candidate := website
candidate.Protocol, candidate.HttpConfig = constant.ProtocolHTTP, req.HttpConfig
if req.Enable {
candidate.Protocol = constant.ProtocolHTTPS
}
if err = checkWebsiteListenConflict(candidate, nginxInstall.HttpPort, nginxInstall.HttpsPort); err != nil {
return nil, err
}
if err = ChangeHSTSConfig(req.Hsts, req.HstsIncludeSubDomains, req.Http3, website); err != nil { if err = ChangeHSTSConfig(req.Hsts, req.HstsIncludeSubDomains, req.Http3, website); err != nil {
return nil, err return nil, err
} }
+11
View File
@@ -14,6 +14,8 @@ import (
) )
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) { func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
var ( var (
domainModels []model.WebsiteDomain domainModels []model.WebsiteDomain
addPorts []int addPorts []int
@@ -31,6 +33,15 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ports := websiteListenPorts(website, httpPort, httpsPort)
candidate := website
candidate.Domains = append(append([]model.WebsiteDomain(nil), website.Domains...), domainModels...)
for _, domain := range domainModels {
ports[domain.Port] = struct{}{}
}
if err = checkWebsiteListenPorts(candidate, ports, httpPort, httpsPort); err != nil {
return nil, err
}
go func() { go func() {
if err := ensureFirewallPorts(addPorts); err != nil { if err := ensureFirewallPorts(addPorts); err != nil {
global.LOG.Errorf("allow website firewall ports failed: %v", err) global.LOG.Errorf("allow website firewall ports failed: %v", err)
+74
View File
@@ -0,0 +1,74 @@
package service
import (
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
var websiteListenMutation sync.Mutex
func websiteListenPorts(website model.Website, httpPort, httpsPort int) map[int]struct{} {
ports := make(map[int]struct{})
for _, domain := range website.Domains {
if domain.Port > 0 {
ports[domain.Port] = struct{}{}
}
}
if website.Protocol == constant.ProtocolHTTPS {
plan := buildWebsiteTLSPlan(website.Domains, httpPort, httpsPort)
for _, port := range plan.httpsPorts {
if port > 0 {
ports[port] = struct{}{}
}
}
}
return ports
}
func checkWebsiteListenConflict(website model.Website, httpPort, httpsPort int) error {
return checkWebsiteListenPorts(website, websiteListenPorts(website, httpPort, httpsPort), httpPort, httpsPort)
}
func checkWebsiteListenPorts(website model.Website, ports map[int]struct{}, httpPort, httpsPort int) error {
if len(website.Domains) == 0 || len(ports) == 0 {
return nil
}
names := make(map[string]struct{}, len(website.Domains))
for _, domain := range website.Domains {
names[strings.ToLower(domain.Domain)] = struct{}{}
}
existing, err := websiteRepo.GetBy()
if err != nil {
return err
}
domains, err := websiteDomainRepo.GetBy()
if err != nil {
return err
}
byWebsite := make(map[uint][]model.WebsiteDomain)
for _, domain := range domains {
byWebsite[domain.WebsiteID] = append(byWebsite[domain.WebsiteID], domain)
}
for _, other := range existing {
if website.ID != 0 && other.ID == website.ID || other.Type == constant.Stream {
continue
}
other.Domains = byWebsite[other.ID]
for _, domain := range other.Domains {
if _, match := names[strings.ToLower(domain.Domain)]; !match {
continue
}
for port := range websiteListenPorts(other, httpPort, httpsPort) {
if _, occupied := ports[port]; occupied {
return buserr.WithName("ErrDomainIsUsed", other.PrimaryDomain)
}
}
break
}
}
return nil
}
+1
View File
@@ -81,6 +81,7 @@ const (
const ( const (
TaskScopeWebsite = "Website" TaskScopeWebsite = "Website"
TaskScopeAI = "AI" TaskScopeAI = "AI"
TaskScopeTensorRT = "TensorRT"
TaskScopeApp = "App" TaskScopeApp = "App"
TaskScopeRuntime = "Runtime" TaskScopeRuntime = "Runtime"
TaskScopeDatabase = "Database" TaskScopeDatabase = "Database"
+130 -121
View File
@@ -8,6 +8,107 @@
"formatZH": "更新 Ollama 绑定域名 [domain]", "formatZH": "更新 Ollama 绑定域名 [domain]",
"formatEN": "update Ollama bind domain [domain]" "formatEN": "update Ollama bind domain [domain]"
}, },
"/ai/model/downloader/featured/sync/status": {
"bodyKeys": [
"value"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新推荐模型自动同步 [value]",
"formatEN": "update featured model automatic synchronization [value]"
},
"/ai/model/downloader/hf/download": {
"bodyKeys": [
"repoID",
"revision"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载 Hugging Face 模型 [repoID]",
"formatEN": "download Hugging Face model [repoID]"
},
"/ai/model/downloader/local/delete": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除本地模型 [name]",
"formatEN": "delete local model [name]"
},
"/ai/model/downloader/modelscope/download": {
"bodyKeys": [
"repoID",
"revision"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载 ModelScope 模型 [repoID]",
"formatEN": "download ModelScope model [repoID]"
},
"/ai/model/downloader/settings": {
"bodyKeys": [
"modelDir"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新模型下载设置 [modelDir]",
"formatEN": "update model downloader settings [modelDir]"
},
"/ai/model/downloader/tasks/cancel": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "取消模型下载任务 [repoID]",
"formatEN": "cancel model download task [repoID]"
},
"/ai/model/downloader/tasks/remove": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "移除模型下载任务 [repoID]",
"formatEN": "remove model download task [repoID]"
},
"/ai/model/downloader/tasks/retry": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "重试模型下载任务 [repoID]",
"formatEN": "retry model download task [repoID]"
},
"/ai/ollama/close": { "/ai/ollama/close": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
@@ -103,27 +204,27 @@
"/alert/config/del": { "/alert/config/del": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [name]", "formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]", "formatEN": "delete alert config [name]"
"beforeFunctions": []
}, },
"/alert/config/status": { "/alert/config/status": {
"bodyKeys": [ "bodyKeys": [
"status" "status"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [name][status]", "formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]", "formatEN": "update alert config status [name][status]"
"beforeFunctions": []
}, },
"/alert/config/update": { "/alert/config/update": {
"bodyKeys": [ "bodyKeys": [
"displayName" "displayName"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [displayName]", "formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]", "formatEN": "update alert config [displayName]"
"beforeFunctions": []
}, },
"/alert/del": { "/alert/del": {
"bodyKeys": [ "bodyKeys": [
@@ -4971,98 +5072,6 @@
"formatZH": "更新离线告警日志 [id]", "formatZH": "更新离线告警日志 [id]",
"formatEN": "update offline alert log [id]" "formatEN": "update offline alert log [id]"
}, },
"/xpack/model/downloader/hf/download": {
"bodyKeys": [
"repoID",
"revision"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载 Hugging Face 模型 [repoID]",
"formatEN": "download Hugging Face model [repoID]"
},
"/xpack/model/downloader/local/delete": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除本地模型 [name]",
"formatEN": "delete local model [name]"
},
"/xpack/model/downloader/modelscope/download": {
"bodyKeys": [
"repoID",
"revision"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载 ModelScope 模型 [repoID]",
"formatEN": "download ModelScope model [repoID]"
},
"/xpack/model/downloader/settings": {
"bodyKeys": [
"modelDir"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新模型下载设置 [modelDir]",
"formatEN": "update model downloader settings [modelDir]"
},
"/xpack/model/downloader/tasks/cancel": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "取消模型下载任务 [repoID]",
"formatEN": "cancel model download task [repoID]"
},
"/xpack/model/downloader/tasks/remove": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "移除模型下载任务 [repoID]",
"formatEN": "remove model download task [repoID]"
},
"/xpack/model/downloader/tasks/retry": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "model_download_tasks",
"output_column": "repo_id",
"output_value": "repoID"
}
],
"formatZH": "重试模型下载任务 [repoID]",
"formatEN": "retry model download task [repoID]"
},
"/xpack/monitor/config/global": { "/xpack/monitor/config/global": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -5211,7 +5220,7 @@
"formatZH": "更新 vLLM [name]", "formatZH": "更新 vLLM [name]",
"formatEN": "update vLLM [name]" "formatEN": "update vLLM [name]"
}, },
"/xpack/vms": { "/vms": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
@@ -5220,7 +5229,7 @@
"formatZH": "创建虚拟机 [name]", "formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]" "formatEN": "create VM [name]"
}, },
"/xpack/vms/del": { "/vms/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5238,7 +5247,7 @@
"formatZH": "删除虚拟机 [name]", "formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]" "formatEN": "delete VM [name]"
}, },
"/xpack/vms/disks/resize": { "/vms/disks/resize": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"target", "target",
@@ -5249,14 +5258,14 @@
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB", "formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB" "formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
}, },
"/xpack/vms/environment/enable": { "/vms/environment/enable": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "启用虚拟机运行环境", "formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment" "formatEN": "enable VM runtime environment"
}, },
"/xpack/vms/iso": { "/vms/iso": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -5266,7 +5275,7 @@
"formatZH": "创建虚拟机镜像 [name] [type]", "formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]" "formatEN": "create VM ISO [name] [type]"
}, },
"/xpack/vms/iso/del": { "/vms/iso/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5284,7 +5293,7 @@
"formatZH": "删除虚拟机镜像 [name]", "formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]" "formatEN": "delete VM ISO [name]"
}, },
"/xpack/vms/iso/update": { "/vms/iso/update": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -5294,7 +5303,7 @@
"formatZH": "更新虚拟机镜像 [name] [type]", "formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]" "formatEN": "update VM ISO [name] [type]"
}, },
"/xpack/vms/networks": { "/vms/networks": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -5304,7 +5313,7 @@
"formatZH": "创建虚拟机网络 [name] [type]", "formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]" "formatEN": "create VM network [name] [type]"
}, },
"/xpack/vms/networks/del": { "/vms/networks/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5322,7 +5331,7 @@
"formatZH": "删除虚拟机网络 [name]", "formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]" "formatEN": "delete VM network [name]"
}, },
"/xpack/vms/networks/update": { "/vms/networks/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5340,7 +5349,7 @@
"formatZH": "更新虚拟机网络 [name]", "formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]" "formatEN": "update VM network [name]"
}, },
"/xpack/vms/operate": { "/vms/operate": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"operate" "operate"
@@ -5350,14 +5359,14 @@
"formatZH": "操作虚拟机 [name] [operate]", "formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]" "formatEN": "operate VM [name] [operate]"
}, },
"/xpack/vms/orphans/clean": { "/vms/orphans/clean": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据", "formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data" "formatEN": "clean VM orphaned data"
}, },
"/xpack/vms/rename": { "/vms/rename": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"newName" "newName"
@@ -5367,7 +5376,7 @@
"formatZH": "重命名虚拟机 [name] 为 [newName]", "formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]" "formatEN": "rename VM [name] to [newName]"
}, },
"/xpack/vms/snapshots": { "/vms/snapshots": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -5377,7 +5386,7 @@
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]", "formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]" "formatEN": "create VM [name] snapshot [snapshotName]"
}, },
"/xpack/vms/snapshots/del": { "/vms/snapshots/del": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -5387,7 +5396,7 @@
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]", "formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]" "formatEN": "delete VM [name] snapshot [snapshotName]"
}, },
"/xpack/vms/snapshots/recover": { "/vms/snapshots/recover": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"snapshotName" "snapshotName"
@@ -5397,7 +5406,7 @@
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]", "formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]" "formatEN": "recover VM [name] snapshot [snapshotName]"
}, },
"/xpack/vms/storages": { "/vms/storages": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"type" "type"
@@ -5407,7 +5416,7 @@
"formatZH": "创建虚拟机存储 [name] [type]", "formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]" "formatEN": "create VM storage [name] [type]"
}, },
"/xpack/vms/storages/del": { "/vms/storages/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5425,7 +5434,7 @@
"formatZH": "删除虚拟机存储 [name]", "formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]" "formatEN": "delete VM storage [name]"
}, },
"/xpack/vms/storages/update": { "/vms/storages/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5443,14 +5452,14 @@
"formatZH": "更新虚拟机存储 [name]", "formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]" "formatEN": "update VM storage [name]"
}, },
"/xpack/vms/sync": { "/vms/sync": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "同步虚拟机", "formatZH": "同步虚拟机",
"formatEN": "sync virtual machines" "formatEN": "sync virtual machines"
}, },
"/xpack/vms/templates": { "/vms/templates": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
"templateName" "templateName"
@@ -5460,7 +5469,7 @@
"formatZH": "创建虚拟机 [name] 模板 [templateName]", "formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]" "formatEN": "create VM [name] template [templateName]"
}, },
"/xpack/vms/templates/del": { "/vms/templates/del": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
], ],
@@ -5478,7 +5487,7 @@
"formatZH": "删除虚拟机模板 [name]", "formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]" "formatEN": "delete VM template [name]"
}, },
"/xpack/vms/update": { "/vms/update": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
], ],
+11 -1
View File
@@ -26,6 +26,12 @@ func Run() {
interval model.Setting interval model.Setting
status model.Setting status model.Setting
) )
if !global.CONF.Base.IsOffline {
if _, err := global.Cron.AddFunc("@every 24h", service.SyncFeaturedModels); err != nil {
global.LOG.Errorf("add featured model sync job failed: %v", err)
}
go service.SyncFeaturedModels()
}
go syncBeforeStart() go syncBeforeStart()
if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil { if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil {
global.LOG.Errorf("load monitor status from db failed, err: %v", err) global.LOG.Errorf("load monitor status from db failed, err: %v", err)
@@ -77,7 +83,11 @@ func Run() {
global.LOG.Errorf("generate random hour failed: %v", err) global.LOG.Errorf("generate random hour failed: %v", err)
hourRand = big.NewInt(0) hourRand = big.NewInt(0)
} }
if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), job.NewAppStoreJob()); err != nil { appSyncSchedule := fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64())
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewContainerImageCheckUpdateJob()); err != nil {
global.LOG.Errorf("cannot add container image update cron job: %v", err)
}
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewAppStoreJob()); err != nil {
global.LOG.Errorf("can not add appstore corn job: %s", err.Error()) global.LOG.Errorf("can not add appstore corn job: %s", err.Error())
} }
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil { if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
@@ -0,0 +1,24 @@
package job
import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
)
type containerImageCheckUpdate struct{}
func NewContainerImageCheckUpdateJob() *containerImageCheckUpdate {
return &containerImageCheckUpdate{}
}
func (j *containerImageCheckUpdate) Run() {
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
global.LOG.Errorf("load container image update schedule: %v", err)
return
}
if status.Enabled {
if _, err := service.StartContainerImageCheckUpdate(); err != nil {
global.LOG.Errorf("start container image update check: %v", err)
}
}
}
+2
View File
@@ -2,6 +2,8 @@ module github.com/1Panel-dev/1Panel/agent
go 1.27.3 go 1.27.3
godebug multipathtcp=0
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1 replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require ( require (
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Requested resource not found: {{ .err }}'
# quick jump # quick jump
ErrMinQuickJump: 'Set at least one quick jump entry' ErrMinQuickJump: 'Set at least one quick jump entry'
ErrMaxQuickJump: 'At most 4 quick jump entries' ErrMaxQuickJump: 'At most 5 quick jump entries'
# common # common
ErrShutDown: 'Command stopped manually' ErrShutDown: 'Command stopped manually'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first' ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified' ErrAgentAccountNotVerified: 'Agent account not verified'
ErrAgentAccountModelsRequired: 'Please configure at least one model' ErrAgentAccountModelsRequired: 'Please configure at least one model'
ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation' ErrAgentAccountSingleInitialModel: 'This provider supports configuring at most one model'
ErrAgentModelNotInAccount: 'Model is not configured in the selected account' ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it' ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it'
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated' ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'The certificate file does not exist'
ErrSSLKeyFormat: 'Private key file verification failed' ErrSSLKeyFormat: 'Private key file verification failed'
ErrSSLCertificateFormat: 'Invalid certificate format; use PEM' ErrSSLCertificateFormat: 'Invalid certificate format; use PEM'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank'
ErrOpenrestyInstall: "Please install OpenResty from the App Store first"
ErrOpenrestyNotFound: 'HTTP mode requires OpenResty' ErrOpenrestyNotFound: 'HTTP mode requires OpenResty'
ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]' ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]'
dnsAccount: 'DNS Auto' dnsAccount: 'DNS Auto'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "The SSH public and private keys do not match"
# virtual machine # virtual machine
ErrVMNotFound: "Virtual machine does not exist" ErrVMNotFound: "Virtual machine does not exist"
ErrVMAlreadyExists: "Virtual machine already exists" ErrVMAlreadyExists: "Virtual machine already exists"
ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more." ErrVMCountLimit: "The current edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade your edition and try again."
ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}" ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]" ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm not found" ErrVMHelperNotFound: "1panel-kvm not found"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use
AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained." AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "Check for image updates"
ContainerImageCheckScope: "Checking all {{ .count }} containers on this node, regardless of pagination or filters"
ContainerImageCheckItem: "Checking container {{ .name }}, image {{ .image }}"
ContainerImageCheckFailed: "Container {{ .name }} check failed: {{ .error }}; retaining previous results matching the current image"
ContainerImageCheckSkipped: "Skipping {{ .name }}: not a tagged Docker Hub image, a local image, or pinned digest"
ContainerImageCheckAvailable: "Container {{ .name }} has an update: {{ .image }} -> {{ .target }}"
ContainerImageCheckCurrent: "No update found for container {{ .name }}"
ContainerImageCheckChanged: "Container {{ .name }} was removed or its image changed; result was not saved"
ContainerImageCheckSummary: "Check finished: {{ .checked }} checked, {{ .updates }} upgradable, {{ .failed }} failed, {{ .skipped }} skipped"
ContainerImageCheckFailures: "{{ .count }} container checks failed; see the task log"
ErrImageCheckProxyRequired: "To check for image updates in the current runtime environment, please configure a proxy server first."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'No se pudo encontrar el recurso solicitado {{ .err }}'
# salto rápido # salto rápido
ErrMinQuickJump: 'Agrega al menos una entrada rápida' ErrMinQuickJump: 'Agrega al menos una entrada rápida'
ErrMaxQuickJump: 'Máximo 4 entradas rápidas' ErrMaxQuickJump: 'Máximo 5 entradas rápidas'
# común # común
ErrShutDown: 'Comando detenido manualmente' ErrShutDown: 'Comando detenido manualmente'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero' ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada' ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
ErrAgentAccountModelsRequired: 'Configure al menos un modelo' ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta' ErrAgentAccountSingleInitialModel: 'Este proveedor permite configurar como máximo un modelo'
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada' ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
ErrAgentVerifyModelInUse: 'Este modelo se usa para verificar la cuenta. Seleccione otro modelo de verificación antes de eliminarlo' ErrAgentVerifyModelInUse: 'Este modelo se usa para verificar la cuenta. Seleccione otro modelo de verificación antes de eliminarlo'
ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse' ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'El archivo de certificado no existe'
ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada' ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada'
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem' ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
ErrOpenrestyInstall: "Instale primero OpenResty desde la tienda de aplicaciones"
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty' ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]' ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
dnsAccount: 'DNS Automático' dnsAccount: 'DNS Automático'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
# máquina virtual # máquina virtual
ErrVMNotFound: "La máquina virtual no existe" ErrVMNotFound: "La máquina virtual no existe"
ErrVMAlreadyExists: "La máquina virtual ya existe" ErrVMAlreadyExists: "La máquina virtual ya existe"
ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo." ErrVMCountLimit: "La edición actual permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o cambia a una edición superior y vuelve a intentarlo."
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}" ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]" ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
ErrVMHelperNotFound: "No se encontró 1panel-kvm" ErrVMHelperNotFound: "No se encontró 1panel-kvm"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración co
AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original." AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "Buscar actualizaciones de imágenes"
ContainerImageCheckScope: "Comprobando los {{ .count }} contenedores de este nodo, sin aplicar paginación ni filtros"
ContainerImageCheckItem: "Comprobando el contenedor {{ .name }}, imagen {{ .image }}"
ContainerImageCheckFailed: "Error al comprobar el contenedor {{ .name }}: {{ .error }}; se conservan los resultados anteriores correspondientes a la imagen actual"
ContainerImageCheckSkipped: "Se omite {{ .name }}: no es una imagen de Docker Hub con etiqueta, es una imagen local o tiene un digest fijo"
ContainerImageCheckAvailable: "El contenedor {{ .name }} tiene una actualización: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "No se encontraron actualizaciones para el contenedor {{ .name }}"
ContainerImageCheckChanged: "El contenedor {{ .name }} se eliminó o su imagen cambió; no se guardó el resultado"
ContainerImageCheckSummary: "Comprobación finalizada: comprobados {{ .checked }}, actualizables {{ .updates }}, fallidos {{ .failed }}, omitidos {{ .skipped }}"
ContainerImageCheckFailures: "Falló la comprobación de {{ .count }} contenedores; consulte el registro de la tarea"
ErrImageCheckProxyRequired: "Para comprobar actualizaciones de imágenes en el entorno actual, configura primero un servidor proxy."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'منبع درخواستی یافت نشد: {{ .err }}'
# پرش سریع # پرش سریع
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید' ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
ErrMaxQuickJump: 'حداکثر ۴ ورودی پرش سریع' ErrMaxQuickJump: 'حداکثر ۵ ورودی پرش سریع'
# عمومی # عمومی
ErrShutDown: 'دستور به صورت دستی متوقف شد' ErrShutDown: 'دستور به صورت دستی متوقف شد'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'ارائه‌دهنده عامل پشتیبانی
ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید' ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید'
ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است' ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است'
ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید' ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید'
ErrAgentAccountSingleInitialModel: 'فقط یک مدل اولیه می‌تواند در هنگام ایجاد حساب پیکربندی شود' ErrAgentAccountSingleInitialModel: 'برای این ارائه‌دهنده حداکثر یک مدل می‌توان پیکربندی کرد'
ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است' ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است'
ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده می‌شود. پیش از حذف، مدل بررسی دیگری انتخاب کنید' ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده می‌شود. پیش از حذف، مدل بررسی دیگری انتخاب کنید'
ErrAgentFallbackModelDuplicate: 'مدل‌های جایگزین نمی‌توانند تکراری باشند' ErrAgentFallbackModelDuplicate: 'مدل‌های جایگزین نمی‌توانند تکراری باشند'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'فایل گواهی وجود ندارد'
ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود' ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود'
ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید' ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمی‌توانند خالی باشند' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمی‌توانند خالی باشند'
ErrOpenrestyInstall: "لطفاً ابتدا OpenResty را از فروشگاه برنامه‌ها نصب کنید"
ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد' ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد'
ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]' ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]'
dnsAccount: 'خودکار DNS' dnsAccount: 'خودکار DNS'
@@ -736,7 +737,7 @@ ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندار
# ماشین مجازی # ماشین مجازی
ErrVMNotFound: "ماشین مجازی وجود ندارد" ErrVMNotFound: "ماشین مجازی وجود ندارد"
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد" ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید." ErrVMCountLimit: "در نسخه فعلی می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه بالاتر ارتقا دهید و دوباره تلاش کنید."
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}" ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است" ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است"
ErrVMHelperNotFound: "1panel-kvm یافت نشد" ErrVMHelperNotFound: "1panel-kvm یافت نشد"
@@ -803,3 +804,15 @@ ErrFeishuUpgradeMounts: "این اتصال‌های ذخیره‌سازی کام
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد." AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "بررسی به‌روزرسانی ایمیج‌ها"
ContainerImageCheckScope: "در حال بررسی همه {{ .count }} کانتینر این گره، بدون توجه به صفحه‌بندی و فیلترها"
ContainerImageCheckItem: "در حال بررسی کانتینر {{ .name }}، ایمیج {{ .image }}"
ContainerImageCheckFailed: "بررسی کانتینر {{ .name }} ناموفق بود: {{ .error }}؛ نتایج قبلیِ منطبق با ایمیج فعلی حفظ می‌شوند"
ContainerImageCheckSkipped: "کانتینر {{ .name }} رد شد: ایمیج تگ‌دار Docker Hub نیست، ایمیج محلی است یا digest ثابت دارد"
ContainerImageCheckAvailable: "برای کانتینر {{ .name }} به‌روزرسانی موجود است: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "به‌روزرسانی برای کانتینر {{ .name }} یافت نشد"
ContainerImageCheckChanged: "کانتینر {{ .name }} حذف شده یا ایمیج آن تغییر کرده است؛ نتیجه ذخیره نشد"
ContainerImageCheckSummary: "بررسی پایان یافت: بررسی‌شده {{ .checked }}، قابل ارتقا {{ .updates }}، ناموفق {{ .failed }}، ردشده {{ .skipped }}"
ContainerImageCheckFailures: "بررسی {{ .count }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید"
ErrImageCheckProxyRequired: "برای بررسی به‌روزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '要求されたリソース {{ .err }} が見つかりま
# クイックジャンプ # クイックジャンプ
ErrMinQuickJump: 'クイックアクセスを1件以上追加' ErrMinQuickJump: 'クイックアクセスを1件以上追加'
ErrMaxQuickJump: '最大4件のクイックアクセス' ErrMaxQuickJump: '最大5件のクイックアクセス'
# 共通 # 共通
ErrShutDown: 'コマンドは手動で停止されました' ErrShutDown: 'コマンドは手動で停止されました'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択' ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です' ErrAgentAccountNotVerified: 'アカウント未検証です'
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください' ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです' ErrAgentAccountSingleInitialModel: 'このプロバイダーで設定できるモデルは最大 1 つです'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません' ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください' ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください'
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません' ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '証明書ファイルが存在しません'
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました' ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください' ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
ErrOpenrestyInstall: "先にアプリストアから OpenResty をインストールしてください"
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です' ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]' ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
dnsAccount: 'DNS 自動' dnsAccount: 'DNS 自動'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
# 仮想マシン # 仮想マシン
ErrVMNotFound: "仮想マシンが存在しません" ErrVMNotFound: "仮想マシンが存在しません"
ErrVMAlreadyExists: "仮想マシンは既に存在します" ErrVMAlreadyExists: "仮想マシンは既に存在します"
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。" ErrVMCountLimit: "現在のエディションでは最大 {{ .limit }} 台の仮想マシンを作成できます。 既存の仮想マシンを削除するか、上位エディションにアップグレードしてから再試行してください。"
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}" ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています" ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
ErrVMHelperNotFound: "1panel-kvm が見つかりません" ErrVMHelperNotFound: "1panel-kvm が見つかりません"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。" AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "イメージの更新を確認"
ContainerImageCheckScope: "ページ分割やフィルターに関係なく、このノードの全 {{ .count }} コンテナを確認します"
ContainerImageCheckItem: "コンテナ {{ .name }}、イメージ {{ .image }} を確認しています"
ContainerImageCheckFailed: "コンテナ {{ .name }} の確認に失敗しました:{{ .error }}。現在のイメージに対応する前回の結果を保持します"
ContainerImageCheckSkipped: "{{ .name }} をスキップ:タグ付きの Docker Hub イメージではないか、ローカルイメージまたは digest 固定のイメージです"
ContainerImageCheckAvailable: "コンテナ {{ .name }} は更新可能です:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "コンテナ {{ .name }} の更新は見つかりませんでした"
ContainerImageCheckChanged: "コンテナ {{ .name }} が削除されたか、イメージが変更されたため、結果は保存されませんでした"
ContainerImageCheckSummary: "確認終了:成功 {{ .checked }}、更新可能 {{ .updates }}、失敗 {{ .failed }}、スキップ {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} コンテナの確認に失敗しました。タスクログを確認してください"
ErrImageCheckProxyRequired: "現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。"
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '요청한 리소스 {{ .err }}을 찾을 수 없습니다.'
# 빠른 이동 # 빠른 이동
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요' ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
ErrMaxQuickJump: '퀵 액세스는 최대 4개' ErrMaxQuickJump: '퀵 액세스는 최대 5개'
# 공통 # 공통
ErrShutDown: '명령이 수동으로 중단되었습니다' ErrShutDown: '명령이 수동으로 중단되었습니다'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요' ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인' ErrAgentAccountNotVerified: '에이전트 계정 미확인'
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요' ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다' ErrAgentAccountSingleInitialModel: '이 제공업체에는 모델을 최대 하나만 설정할 수 있습니다'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다' ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요' ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요'
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다' ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '인증서 파일이 존재하지 않습니다'
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다' ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.' ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
ErrOpenrestyInstall: "먼저 앱 스토어에서 OpenResty를 설치하세요"
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다' ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]' ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
dnsAccount: 'DNS 자동' dnsAccount: 'DNS 자동'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
# 가상 머신 # 가상 머신
ErrVMNotFound: "가상 머신이 존재하지 않습니다" ErrVMNotFound: "가상 머신이 존재하지 않습니다"
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다" ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요." ErrVMCountLimit: "현재 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 상위 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}" ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다" ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다" ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다." AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "이미지 업데이트 확인"
ContainerImageCheckScope: "페이지 및 필터와 관계없이 현재 노드의 모든 컨테이너 {{ .count }}개를 확인합니다"
ContainerImageCheckItem: "컨테이너 {{ .name }}, 이미지 {{ .image }} 확인 중"
ContainerImageCheckFailed: "컨테이너 {{ .name }} 확인 실패: {{ .error }}. 현재 이미지와 일치하는 이전 결과를 유지합니다"
ContainerImageCheckSkipped: "{{ .name }} 건너뜀: 태그가 있는 Docker Hub 이미지가 아니거나 로컬 이미지 또는 digest가 고정된 이미지입니다"
ContainerImageCheckAvailable: "컨테이너 {{ .name }} 업데이트 가능: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "컨테이너 {{ .name }}의 업데이트를 찾지 못했습니다"
ContainerImageCheckChanged: "컨테이너 {{ .name }}이 삭제되었거나 이미지가 변경되어 결과를 저장하지 않았습니다"
ContainerImageCheckSummary: "확인 종료: 성공 {{ .checked }}, 업데이트 가능 {{ .updates }}, 실패 {{ .failed }}, 건너뜀 {{ .skipped }}"
ContainerImageCheckFailures: "컨테이너 {{ .count }}개 확인에 실패했습니다. 작업 로그를 확인하세요"
ErrImageCheckProxyRequired: "현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요."
+16 -3
View File
@@ -32,7 +32,7 @@ ErrApiConfigKeyTimeInvalid: 'ເວລາ (Timestamp) ຂອງ API ບໍ່ຖ
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ' ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ' ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 4 ລາຍການ' ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 5 ລາຍການ'
#common #common
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ' ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
@@ -62,7 +62,7 @@ ErrAgentProviderNotSupported: 'ບໍ່ຮອງຮັບຜູ້ໃຫ້ບ
ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ' ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ'
ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ' ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ'
ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ' ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ'
ErrAgentAccountSingleInitialModel: 'ສາມາດກຳນົດຄ່າໂມເດລເລີ່ມຕົ້ນໄດ້ພຽງຕົວດຽວໃນລະຫວ່າງການສ້າງບັນຊີ' ErrAgentAccountSingleInitialModel: 'ຜູ້ໃຫ້ບໍລິການນີ້ສາມາດກຳນົດຄ່າໄດ້ສູງສຸດໜຶ່ງໂມເດວ'
ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ' ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ'
ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.' ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.'
ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້' ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້'
@@ -242,6 +242,7 @@ ErrSSLCertificateNotFound: 'ບໍ່ມີໄຟລ໌ໃບຮັບຮອງ
ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ' ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ'
ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM' ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້'
ErrOpenrestyInstall: "ກະລຸນາຕິດຕັ້ງ OpenResty ຈາກຮ້ານແອັບກ່ອນ"
ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty' ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty'
ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]' ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]'
dnsAccount: 'DNS ອັດຕະໂນມັດ' dnsAccount: 'DNS ອັດຕະໂນມັດ'
@@ -726,7 +727,7 @@ ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນ
# virtual machine # virtual machine
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ" ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ" ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ." ErrVMCountLimit: "ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດລຸ້ນ ແລ້ວລອງອີກຄັ້ງ."
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}" ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ" ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm" ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
@@ -793,3 +794,15 @@ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້." AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "ກວດສອບການອັບເດດອິມເມຈ"
ContainerImageCheckScope: "ກຳລັງກວດສອບທັງໝົດ {{ .count }} ຄອນເທນເນີໃນໂນດນີ້ ໂດຍບໍ່ຂຶ້ນກັບການແບ່ງໜ້າ ຫຼື ຕົວກອງ"
ContainerImageCheckItem: "ກຳລັງກວດສອບຄອນເທນເນີ {{ .name }}, ອິມເມຈ {{ .image }}"
ContainerImageCheckFailed: "ກວດສອບຄອນເທນເນີ {{ .name }} ລົ້ມເຫຼວ: {{ .error }}; ເກັບຜົນກ່ອນໜ້າທີ່ກົງກັບອິມເມຈປັດຈຸບັນໄວ້"
ContainerImageCheckSkipped: "ຂ້າມ {{ .name }}: ບໍ່ແມ່ນອິມເມຈ Docker Hub ທີ່ມີແທັກ, ເປັນອິມເມຈພາຍໃນ ຫຼື ກຳນົດ digest ຄົງທີ່"
ContainerImageCheckAvailable: "ຄອນເທນເນີ {{ .name }} ມີອັບເດດ: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "ບໍ່ພົບອັບເດດສຳລັບຄອນເທນເນີ {{ .name }}"
ContainerImageCheckChanged: "ຄອນເທນເນີ {{ .name }} ຖືກລຶບ ຫຼື ອິມເມຈປ່ຽນແປງ; ບໍ່ໄດ້ບັນທຶກຜົນ"
ContainerImageCheckSummary: "ກວດສອບສິ້ນສຸດ: ກວດແລ້ວ {{ .checked }}, ອັບເກຣດໄດ້ {{ .updates }}, ລົ້ມເຫຼວ {{ .failed }}, ຂ້າມ {{ .skipped }}"
ContainerImageCheckFailures: "ກວດສອບ {{ .count }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
ErrImageCheckProxyRequired: "ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Sumber yang diminta {{ .err }} tidak dapat ditemui'
# lompat pantas # lompat pantas
ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas' ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas'
ErrMaxQuickJump: 'Maksimum 4 pintasan pantas' ErrMaxQuickJump: 'Maksimum 5 pintasan pantas'
# umum # umum
ErrShutDown: 'Arahan diberhentikan manual' ErrShutDown: 'Arahan diberhentikan manual'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu' ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan' ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model' ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun' ErrAgentAccountSingleInitialModel: 'Penyedia ini membenarkan paling banyak satu model dikonfigurasi'
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih' ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya' ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya'
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang' ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Fail sijil tidak wujud'
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal' ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem' ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
ErrOpenrestyInstall: "Sila pasang OpenResty daripada gedung aplikasi terlebih dahulu"
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu' ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]' ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
dnsAccount: 'DNS automatik' dnsAccount: 'DNS automatik'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
# mesin maya # mesin maya
ErrVMNotFound: "Mesin maya tidak wujud" ErrVMNotFound: "Mesin maya tidak wujud"
ErrVMAlreadyExists: "Mesin maya sudah wujud" ErrVMAlreadyExists: "Mesin maya sudah wujud"
ErrVMCountLimit: "Edisi Professional membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf kepada Enterprise dan cuba lagi." ErrVMCountLimit: "Edisi semasa membenarkan penciptaan sehingga {{ .limit }} mesin maya. Padamkan mesin maya sedia ada atau naik taraf edisi anda dan cuba lagi."
ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}" ErrVMImageAlreadyExists: "Fail imej mesin maya sudah wujud: {{ .detail }}"
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]" ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm tidak ditemui" ErrVMHelperNotFound: "1panel-kvm tidak ditemui"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuh
AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan." AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "Semak kemas kini imej"
ContainerImageCheckScope: "Menyemak kesemua {{ .count }} kontena pada nod ini tanpa mengira pembahagian halaman atau penapis"
ContainerImageCheckItem: "Menyemak kontena {{ .name }}, imej {{ .image }}"
ContainerImageCheckFailed: "Semakan kontena {{ .name }} gagal: {{ .error }}; keputusan terdahulu yang sepadan dengan imej semasa dikekalkan"
ContainerImageCheckSkipped: "Melangkau {{ .name }}: bukan imej Docker Hub bertag, imej setempat atau digest ditetapkan"
ContainerImageCheckAvailable: "Kontena {{ .name }} mempunyai kemas kini: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Tiada kemas kini ditemui untuk kontena {{ .name }}"
ContainerImageCheckChanged: "Kontena {{ .name }} telah dipadam atau imejnya berubah; keputusan tidak disimpan"
ContainerImageCheckSummary: "Semakan selesai: disemak {{ .checked }}, boleh dikemas kini {{ .updates }}, gagal {{ .failed }}, dilangkau {{ .skipped }}"
ContainerImageCheckFailures: "Semakan {{ .count }} kontena gagal; lihat log tugas"
ErrImageCheckProxyRequired: "Untuk menyemak kemas kini imej dalam persekitaran semasa, sila konfigurasikan pelayan proksi terlebih dahulu."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'O recurso solicitado {{ .err }} não pôde ser encontrado'
# atalho rápido # atalho rápido
ErrMinQuickJump: 'Adicione pelo menos um atalho rápido' ErrMinQuickJump: 'Adicione pelo menos um atalho rápido'
ErrMaxQuickJump: 'Máximo 4 atalhos rápidos' ErrMaxQuickJump: 'Máximo 5 atalhos rápidos'
# comum # comum
ErrShutDown: 'Comando parado manualmente' ErrShutDown: 'Comando parado manualmente'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro' ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada' ErrAgentAccountNotVerified: 'Conta de agente não verificada'
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo' ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta' ErrAgentAccountSingleInitialModel: 'Este provedor permite configurar no máximo um modelo'
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada' ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
ErrAgentVerifyModelInUse: 'Este modelo é usado para verificar a conta. Selecione outro modelo de verificação antes de excluí-lo' ErrAgentVerifyModelInUse: 'Este modelo é usado para verificar a conta. Selecione outro modelo de verificação antes de excluí-lo'
ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados' ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'O arquivo de certificado não existe'
ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada' ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada'
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem' ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ou EabHmacKey não podem estar em branco' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ou EabHmacKey não podem estar em branco'
ErrOpenrestyInstall: "Instale primeiro o OpenResty pela loja de aplicativos"
ErrOpenrestyNotFound: 'O modo HTTP requer o OpenResty instalado' ErrOpenrestyNotFound: 'O modo HTTP requer o OpenResty instalado'
ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]' ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]'
dnsAccount: 'DNS Automático' dnsAccount: 'DNS Automático'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
# máquina virtual # máquina virtual
ErrVMNotFound: "Máquina virtual não existe" ErrVMNotFound: "Máquina virtual não existe"
ErrVMAlreadyExists: "Máquina virtual já existe" ErrVMAlreadyExists: "Máquina virtual já existe"
ErrVMCountLimit: "A edição Professional permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para Enterprise e tente novamente." ErrVMCountLimit: "A edição atual permite criar até {{ .limit }} máquinas virtuais. Exclua uma máquina virtual existente ou atualize para uma edição superior e tente novamente."
ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}" ErrVMImageAlreadyExists: "A imagem da máquina virtual já existe: {{ .detail }}"
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]" ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm não encontrado" ErrVMHelperNotFound: "1panel-kvm não encontrado"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração co
AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido." AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "Verificar atualizações de imagens"
ContainerImageCheckScope: "Verificando todos os {{ .count }} contêineres deste nó, independentemente da paginação e dos filtros"
ContainerImageCheckItem: "Verificando o contêiner {{ .name }}, imagem {{ .image }}"
ContainerImageCheckFailed: "Falha ao verificar o contêiner {{ .name }}: {{ .error }}; os resultados anteriores correspondentes à imagem atual serão mantidos"
ContainerImageCheckSkipped: "Ignorando {{ .name }}: não é uma imagem do Docker Hub com tag, é uma imagem local ou tem digest fixo"
ContainerImageCheckAvailable: "O contêiner {{ .name }} tem uma atualização: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Nenhuma atualização encontrada para o contêiner {{ .name }}"
ContainerImageCheckChanged: "O contêiner {{ .name }} foi removido ou sua imagem mudou; o resultado não foi salvo"
ContainerImageCheckSummary: "Verificação concluída: verificados {{ .checked }}, atualizáveis {{ .updates }}, falhas {{ .failed }}, ignorados {{ .skipped }}"
ContainerImageCheckFailures: "Falha na verificação de {{ .count }} contêineres; consulte o log da tarefa"
ErrImageCheckProxyRequired: "Para verificar atualizações de imagens no ambiente atual, configure primeiro um servidor proxy."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Запрошенный ресурс {{ .err }} не най
# быстрый переход # быстрый переход
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку' ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
ErrMaxQuickJump: 'Не более 4 быстрых ссылок' ErrMaxQuickJump: 'Не более 5 быстрых ссылок'
# общее # общее
ErrShutDown: 'Команда остановлена вручную' ErrShutDown: 'Команда остановлена вручную'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер
ErrAgentAccountRequired: 'Выберите аккаунт агента' ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён' ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель' ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель' ErrAgentAccountSingleInitialModel: 'Для этого провайдера можно настроить не более одной модели'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте' ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки' ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки'
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться' ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Файл сертификата не существ
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась' ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem' ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
ErrOpenrestyInstall: "Сначала установите OpenResty из магазина приложений"
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty' ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]' ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
dnsAccount: 'DNS Авто' dnsAccount: 'DNS Авто'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не с
# виртуальная машина # виртуальная машина
ErrVMNotFound: "Виртуальная машина не существует" ErrVMNotFound: "Виртуальная машина не существует"
ErrVMAlreadyExists: "Виртуальная машина уже существует" ErrVMAlreadyExists: "Виртуальная машина уже существует"
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку." ErrVMCountLimit: "В текущей редакции можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на старшую редакцию и повторите попытку."
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}" ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]" ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm не найден" ErrVMHelperNotFound: "1panel-kvm не найден"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Эти монтирования не поддержив
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена." AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "Проверить обновления образов"
ContainerImageCheckScope: "Проверка всех контейнеров на текущем узле ({{ .count }}) независимо от страниц и фильтров"
ContainerImageCheckItem: "Проверка контейнера {{ .name }}, образ {{ .image }}"
ContainerImageCheckFailed: "Ошибка проверки контейнера {{ .name }}: {{ .error }}; сохранены предыдущие результаты, соответствующие текущему образу"
ContainerImageCheckSkipped: "Пропуск {{ .name }}: это не образ Docker Hub с тегом, локальный образ или образ с фиксированным digest"
ContainerImageCheckAvailable: "Доступно обновление контейнера {{ .name }}: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Обновлений для контейнера {{ .name }} не найдено"
ContainerImageCheckChanged: "Контейнер {{ .name }} удалён или его образ изменился; результат не сохранён"
ContainerImageCheckSummary: "Проверка завершена: проверено {{ .checked }}, доступны обновления {{ .updates }}, ошибок {{ .failed }}, пропущено {{ .skipped }}"
ContainerImageCheckFailures: "Не удалось проверить контейнеры: {{ .count }}. Подробности в журнале задачи"
ErrImageCheckProxyRequired: "Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'İstenen kaynak {{ .err }} bulunamadı'
# hızlı geçiş # hızlı geçiş
ErrMinQuickJump: 'En az bir hızlı erişim ekle' ErrMinQuickJump: 'En az bir hızlı erişim ekle'
ErrMaxQuickJump: '4 hızlı erişim hakkı var' ErrMaxQuickJump: '5 hızlı erişim hakkı var'
# genel # genel
ErrShutDown: 'Komut manuel durduruldu' ErrShutDown: 'Komut manuel durduruldu'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin' ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı' ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın' ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir' ErrAgentAccountSingleInitialModel: 'Bu sağlayıcı için en fazla bir model yapılandırılabilir'
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı' ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
ErrAgentVerifyModelInUse: 'Bu model hesap doğrulaması için kullanılıyor. Silmeden önce başka bir doğrulama modeli seçin' ErrAgentVerifyModelInUse: 'Bu model hesap doğrulaması için kullanılıyor. Silmeden önce başka bir doğrulama modeli seçin'
ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez' ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Sertifika dosyası mevcut değil'
ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız' ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız'
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın' ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
ErrOpenrestyInstall: "Lütfen önce uygulama mağazasından OpenResty yükleyin"
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir' ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]' ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
dnsAccount: 'DNS Otomatik' dnsAccount: 'DNS Otomatik'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
# sanal makine # sanal makine
ErrVMNotFound: "Sanal makine mevcut değil" ErrVMNotFound: "Sanal makine mevcut değil"
ErrVMAlreadyExists: "Sanal makine zaten mevcut" ErrVMAlreadyExists: "Sanal makine zaten mevcut"
ErrVMCountLimit: "Professional sürümü en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya Enterprise sürümüne yükseltip tekrar deneyin." ErrVMCountLimit: "Mevcut sürüm en fazla {{ .limit }} sanal makine oluşturulmasına izin verir. Mevcut bir sanal makineyi silin veya daha üst bir sürüme yükseltip tekrar deneyin."
ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}" ErrVMImageAlreadyExists: "Sanal makine imaj dosyası zaten mevcut: {{ .detail }}"
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor" ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
ErrVMHelperNotFound: "1panel-kvm bulunamadı" ErrVMHelperNotFound: "1panel-kvm bulunamadı"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygul
AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu." AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu."
ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot"
ContainerImageCheckUpdate: "İmaj güncellemelerini denetle"
ContainerImageCheckScope: "Sayfalama ve filtrelerden bağımsız olarak bu düğümdeki {{ .count }} konteynerin tümü denetleniyor"
ContainerImageCheckItem: "Konteyner {{ .name }}, imaj {{ .image }} denetleniyor"
ContainerImageCheckFailed: "{{ .name }} konteyneri denetlenemedi: {{ .error }}; mevcut imajla eşleşen önceki sonuçlar korunuyor"
ContainerImageCheckSkipped: "{{ .name }} atlanıyor: etiketli bir Docker Hub imajı değil, yerel bir imaj veya digest değeri sabitlenmiş"
ContainerImageCheckAvailable: "{{ .name }} konteyneri için güncelleme mevcut: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "{{ .name }} konteyneri için güncelleme bulunamadı"
ContainerImageCheckChanged: "{{ .name }} konteyneri silindi veya imajı değişti; sonuç kaydedilmedi"
ContainerImageCheckSummary: "Denetim tamamlandı: denetlenen {{ .checked }}, güncellenebilir {{ .updates }}, başarısız {{ .failed }}, atlanan {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} konteyner denetlenemedi; görev günlüğünü inceleyin"
ErrImageCheckProxyRequired: "Geçerli çalışma ortamında imaj güncellemelerini kontrol etmek için önce bir proxy sunucusu yapılandırın."
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '無法找到要求的資源{{ .err }}'
# 快速跳轉 # 快速跳轉
ErrMinQuickJump: '請至少設定一個快速跳轉入口!' ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
ErrMaxQuickJump: '最多可設定四個快速跳轉入口!' ErrMaxQuickJump: '最多可設定五個快速跳轉入口!'
# 通用 # 通用
ErrShutDown: '指令被手動結束!' ErrShutDown: '指令被手動結束!'
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試' ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試' ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
ErrAgentAccountModelsRequired: '請至少配置一個模型' ErrAgentAccountModelsRequired: '請至少配置一個模型'
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型' ErrAgentAccountSingleInitialModel: '目前提供商最多可設定一個模型'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試' ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除' ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除'
ErrAgentFallbackModelDuplicate: '備用模型不能重複' ErrAgentFallbackModelDuplicate: '備用模型不能重複'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '憑證檔案不存在'
ErrSSLKeyFormat: '私鑰檔案驗證失敗' ErrSSLKeyFormat: '私鑰檔案驗證失敗'
ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式' ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空'
ErrOpenrestyInstall: "請先在應用程式商店安裝 OpenResty"
ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty' ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty'
ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]' ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]'
dnsAccount: 'DNS 自動' dnsAccount: 'DNS 自動'
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
# 虛擬機 # 虛擬機
ErrVMNotFound: "虛擬機不存在" ErrVMNotFound: "虛擬機不存在"
ErrVMAlreadyExists: "虛擬機已存在" ErrVMAlreadyExists: "虛擬機已存在"
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。" ErrVMCountLimit: "目前版本最多可建立 {{ .limit }} 台虛擬機。 請刪除現有虛擬機或升級版本後重試。"
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}" ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用" ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在" ErrVMHelperNotFound: "1panel-kvm 不存在"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。" AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
ContainerUpgradeWaiting: "等待容器升級任務執行名額" ContainerUpgradeWaiting: "等待容器升級任務執行名額"
ContainerImageCheckUpdate: "檢查更新映像"
ContainerImageCheckScope: "檢查目前節點全部 {{ .count }} 個容器,不受分頁和篩選影響"
ContainerImageCheckItem: "檢查容器 {{ .name }},映像 {{ .image }}"
ContainerImageCheckFailed: "容器 {{ .name }} 檢查失敗:{{ .error }};保留符合目前映像的上次結果"
ContainerImageCheckSkipped: "略過 {{ .name }}:非 Docker Hub 標籤映像、本機映像或固定 digest"
ContainerImageCheckAvailable: "容器 {{ .name }} 可升級:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "容器 {{ .name }} 未發現更新"
ContainerImageCheckChanged: "容器 {{ .name }} 已刪除或映像已變更,未儲存本次結果"
ContainerImageCheckSummary: "檢查結束:成功 {{ .checked }},可升級 {{ .updates }},失敗 {{ .failed }},略過 {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} 個容器檢查失敗,請查看任務日誌"
ErrImageCheckProxyRequired: "目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器"
+16 -3
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: "无法找到请求的资源 {{ .err }}"
# 快速跳转 # 快速跳转
ErrMinQuickJump: "至少设置 1 个快速跳转入口" ErrMinQuickJump: "至少设置 1 个快速跳转入口"
ErrMaxQuickJump: "最多设置 4 个快速跳转入口" ErrMaxQuickJump: "最多设置 5 个快速跳转入口"
# 通用 # 通用
ErrShutDown: "命令已手动终止" ErrShutDown: "命令已手动终止"
@@ -77,7 +77,7 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试" ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证" ErrAgentAccountNotVerified: "账号未通过验证"
ErrAgentAccountModelsRequired: "请至少配置一个模型" ErrAgentAccountModelsRequired: "请至少配置一个模型"
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型" ErrAgentAccountSingleInitialModel: '当前提供商最多配置一个模型'
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中" ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除" ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除"
ErrAgentFallbackModelDuplicate: "备用模型不能重复" ErrAgentFallbackModelDuplicate: "备用模型不能重复"
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '证书文件不存在'
ErrSSLKeyFormat: '私钥文件校验失败' ErrSSLKeyFormat: '私钥文件校验失败'
ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式' ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空' ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空'
ErrOpenrestyInstall: "请先在应用商店安装 OpenResty"
ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty' ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty'
ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]' ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]'
dnsAccount: "DNS 自动" dnsAccount: "DNS 自动"
@@ -735,7 +736,7 @@ ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
# 虚拟机 # 虚拟机
ErrVMNotFound: "虚拟机不存在" ErrVMNotFound: "虚拟机不存在"
ErrVMAlreadyExists: "虚拟机已存在" ErrVMAlreadyExists: "虚拟机已存在"
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。" ErrVMCountLimit: "当前版本最多可创建 {{ .limit }} 台虚拟机。 请删除已有虚拟机或升级版本后重试。"
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}" ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用" ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在" ErrVMHelperNotFound: "1panel-kvm 不存在"
@@ -802,3 +803,15 @@ ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应
AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。" AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。"
ContainerUpgradeWaiting: "等待容器升级任务执行名额" ContainerUpgradeWaiting: "等待容器升级任务执行名额"
ContainerImageCheckUpdate: "检查更新镜像"
ContainerImageCheckScope: "检查当前节点全部 {{ .count }} 个容器,不受分页和筛选影响"
ContainerImageCheckItem: "检查容器 {{ .name }},镜像 {{ .image }}"
ContainerImageCheckFailed: "容器 {{ .name }} 检查失败:{{ .error }};保留匹配当前镜像的上次结果"
ContainerImageCheckSkipped: "跳过 {{ .name }}:非 Docker Hub 标签镜像、本地镜像或固定 digest"
ContainerImageCheckAvailable: "容器 {{ .name }} 可升级:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "容器 {{ .name }} 未发现更新"
ContainerImageCheckChanged: "容器 {{ .name }} 已删除或镜像发生变化,未保存本次结果"
ContainerImageCheckSummary: "检查结束:成功 {{ .checked }},可升级 {{ .updates }},失败 {{ .failed }},跳过 {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} 个容器检查失败,请查看任务日志"
ErrImageCheckProxyRequired: "当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器"
+6
View File
@@ -10,6 +10,12 @@ import (
) )
func Init() { func Init() {
global.LOG.Info("recover interrupted model download tasks start")
if err := service.NewIModelDownloaderService().RecoverInterruptedTasks(); err != nil {
global.LOG.Warnf("recover interrupted model download tasks failed, err: %v", err)
}
global.LOG.Info("recover interrupted model download tasks done")
go syncApp() go syncApp()
go syncInstalledApp() go syncInstalledApp()
go syncRuntime() go syncRuntime()
+18
View File
@@ -0,0 +1,18 @@
package kvm
import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
vmHelper "github.com/1Panel-dev/1Panel/agent/utils/vm/helper"
)
func Init() {
if helperPath, embedded, err := vmHelper.EnsureEmbeddedHelper(); err != nil {
global.LOG.Warnf("sync embedded kvm helper failed, err: %v", err)
} else if embedded {
global.LOG.Infof("embedded kvm helper synced: %s", helperPath)
}
if err := service.NewIVMService().SyncResourcesOnStartup(); err != nil {
global.LOG.Warnf("sync VM resources on startup failed, err: %v", err)
}
}
+4
View File
@@ -119,6 +119,10 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddQuickDirectory, migrations.AddQuickDirectory,
migrations.AddLocalModelAccountLink, migrations.AddLocalModelAccountLink,
migrations.AddOllamaModelAccountSync, migrations.AddOllamaModelAccountSync,
migrations.AddContainerImageCheckUpdates,
migrations.AddModelQuickJump,
migrations.MigrateModelDownloader,
migrations.MigrateVMDatabase,
} }
} }
@@ -0,0 +1,17 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddContainerImageCheckUpdates = &gormigrate.Migration{
ID: "20261028-container-image-updates",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ContainerImageCheckUpdate{}); err != nil {
return err
}
return tx.Where("key = ?", "ContainerImageUpdateEnabled").FirstOrCreate(&model.Setting{Key: "ContainerImageUpdateEnabled", Value: "Disable"}).Error
},
}
+13
View File
@@ -1939,3 +1939,16 @@ var AddOllamaModelAccountSync = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddModelQuickJump = &gormigrate.Migration{
ID: "20261030-add-model-quick-jump",
Migrate: func(tx *gorm.DB) error {
return tx.Where("name = ?", "ModelServices").FirstOrCreate(&model.QuickJump{
Name: "ModelServices",
Title: "aiTools.model.model",
Recommend: 0,
IsShow: true,
Router: "/ai/model",
}).Error
},
}
@@ -0,0 +1,125 @@
package migrations
import (
"errors"
"net/url"
"os"
"path/filepath"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/glebarez/sqlite"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var MigrateModelDownloader = &gormigrate.Migration{
ID: "20261102-migrate-model-downloader",
Migrate: func(db *gorm.DB) error {
return migrateModelDownloader(db, filepath.Join(global.Dir.DbDir, "xpack.db"))
},
}
func migrateModelDownloader(db *gorm.DB, legacyPath string) error {
var legacy *gorm.DB
if _, err := os.Stat(legacyPath); err == nil {
source := url.URL{Scheme: "file", Path: legacyPath, RawQuery: "mode=ro"}
legacy, err = gorm.Open(sqlite.Open(source.String()), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
if err != nil {
return err
}
sqlDB, err := legacy.DB()
if err != nil {
return err
}
defer sqlDB.Close()
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ModelDownloadTask{}, &model.FeaturedModel{}); err != nil {
return err
}
resetVersion := false
if legacy != nil {
if legacy.Migrator().HasTable(&model.ModelDownloadTask{}) {
var tasks []model.ModelDownloadTask
if err := legacy.FindInBatches(&tasks, 100, func(_ *gorm.DB, _ int) error {
for i := range tasks {
if tasks[i].DownloadType == "" {
tasks[i].DownloadType = "all"
}
}
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&tasks).Error
}).Error; err != nil {
return err
}
}
if legacy.Migrator().HasTable(&model.FeaturedModel{}) {
resetVersion = !legacy.Migrator().HasColumn(&model.FeaturedModel{}, "Sources") || !legacy.Migrator().HasColumn(&model.FeaturedModel{}, "SourceModelIDs")
var models []model.FeaturedModel
if err := legacy.FindInBatches(&models, 100, func(_ *gorm.DB, _ int) error {
for i := range models {
if len(models[i].Sources) == 0 {
models[i].Sources = []string{"modelscope"}
}
}
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error
}).Error; err != nil {
return err
}
} else {
resetVersion = true
}
if legacy.Migrator().HasTable("agent_settings") {
var settings []model.Setting
keys := []string{"ModelDownloaderDir", "ModelDownloaderHFEndpoint", "ModelDownloaderHFToken", "ModelDownloaderModelScopeEndpoint", "ModelDownloaderModelScopeToken", "FeaturedModelsSync", "FeaturedModelsVersion"}
if err := legacy.Table("agent_settings").Where("key IN ?", keys).Find(&settings).Error; err != nil {
return err
}
for _, setting := range settings {
if setting.Key == "FeaturedModelsVersion" && resetVersion {
setting.Value = ""
}
if err := tx.Where("key = ?", setting.Key).FirstOrCreate(&model.Setting{Key: setting.Key, Value: setting.Value, About: setting.About}).Error; err != nil {
return err
}
}
}
}
var count int64
if err := tx.Model(&model.FeaturedModel{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
if err := tx.Model(&model.Setting{}).Where("key = ?", "FeaturedModelsVersion").Update("value", "").Error; err != nil {
return err
}
models := []model.FeaturedModel{
{Family: "Qwen", RepoID: "Qwen/Qwen3.6-35B-A3B", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 67},
{Family: "Qwen", RepoID: "Qwen/Qwen3.6-35B-A3B-FP8", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 35},
{Family: "Qwen", RepoID: "nvidia/Qwen3.6-35B-A3B-NVFP4", Architecture: "MoE", BackboneParams: "35B", ActiveParams: "3B", Engram: "N/A", WeightGB: 22},
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-27B", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 55},
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-27B-FP8", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 28},
{Family: "Qwen", RepoID: "nvidia/Qwen3.8-27B-NVFP4", Architecture: "Dense", BackboneParams: "N/A", ActiveParams: "N/A", Engram: "N/A", WeightGB: 22},
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-Flash-Next", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 335},
{Family: "Qwen", RepoID: "Qwen/Qwen3.8-Flash-Next-FP8", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 172},
{Family: "Qwen", RepoID: "nvidia/Qwen3.8-Flash-Next-FP4-Local", Architecture: "MoE", BackboneParams: "125B", ActiveParams: "6B", Engram: "51B", WeightGB: 130},
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4-Flash-0731", Architecture: "MoE", BackboneParams: "284B", ActiveParams: "13B", Engram: "N/A", WeightGB: 167},
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp", Architecture: "MoE", BackboneParams: "305B", ActiveParams: "undisclosed", Engram: "N/A", WeightGB: 167},
{Family: "DeepSeek", RepoID: "deepseek-ai/DeepSeek-V4.1-Flash-1017", Architecture: "MoE", BackboneParams: "552B", ActiveParams: "8B/16B", Engram: "196B", WeightGB: 510},
{Family: "GLM", RepoID: "zai-org/GLM-5.3-Flash", Architecture: "MoE", BackboneParams: "320B", ActiveParams: "18B", Engram: "N/A", WeightGB: 306},
{Family: "GLM", RepoID: "nvidia/GLM-5.3-Flash-FP4-Local", Architecture: "MoE", BackboneParams: "320B", ActiveParams: "18B", Engram: "N/A", WeightGB: 185},
}
for i := range models {
models[i].Key = models[i].RepoID
models[i].Sort = i + 1
models[i].Description = "{}"
models[i].Sources = []string{"modelscope"}
}
return tx.Create(&models).Error
})
}
+107
View File
@@ -0,0 +1,107 @@
package migrations
import (
"fmt"
"net/url"
"os"
"path/filepath"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/glebarez/sqlite"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var MigrateVMDatabase = &gormigrate.Migration{
ID: "20261102-migrate-vm-to-community",
Migrate: func(db *gorm.DB) error {
return migrateVMDatabase(db, global.Dir.DbDir)
},
}
func migrateVMDatabase(db *gorm.DB, dbDir string) error {
var legacy *gorm.DB
legacyPath := filepath.Join(dbDir, "xpack.db")
if _, err := os.Stat(legacyPath); err == nil {
uri := (&url.URL{Scheme: "file", Path: legacyPath, RawQuery: "mode=ro"}).String()
legacy, err = gorm.Open(sqlite.Open(uri), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
if err != nil {
return fmt.Errorf("open legacy VM database: %w", err)
}
sqlDB, err := legacy.DB()
if err != nil {
return err
}
defer sqlDB.Close()
} else if !os.IsNotExist(err) {
return fmt.Errorf("inspect legacy VM database: %w", err)
}
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.VirtualMachine{}, &model.VMTemplate{}, &model.VMIso{}, &model.VMNetwork{}, &model.VMStorage{}); err != nil {
return err
}
if legacy != nil {
tables := []struct {
name string
records interface{}
}{
{name: "virtual_machines", records: &[]model.VirtualMachine{}},
{name: "vm_templates", records: &[]model.VMTemplate{}},
{name: "vm_isos", records: &[]model.VMIso{}},
{name: "vm_networks", records: &[]model.VMNetwork{}},
{name: "vm_storages", records: &[]model.VMStorage{}},
}
for _, table := range tables {
if err := migrateVMTable(legacy, tx, table.name, table.records); err != nil {
return fmt.Errorf("migrate %s: %w", table.name, err)
}
}
}
return nil
})
}
func migrateVMTable(source, target *gorm.DB, table string, records interface{}) error {
if !source.Migrator().HasTable(table) {
return nil
}
if err := target.Table(table).Where("1 = 1").Delete(records).Error; err != nil {
return err
}
query := source.Table(table).Find(records)
if query.Error != nil {
return query.Error
}
if query.RowsAffected != 0 {
if err := target.Table(table).CreateInBatches(records, 50).Error; err != nil {
return err
}
var migrated int64
if err := target.Table(table).Count(&migrated).Error; err != nil {
return err
}
if migrated != query.RowsAffected {
return fmt.Errorf("expected %d records, migrated %d", query.RowsAffected, migrated)
}
}
return migrateVMSequence(source, target, table)
}
func migrateVMSequence(source, target *gorm.DB, table string) error {
var sequenceTable int64
if err := source.Raw("SELECT COUNT(*) FROM sqlite_master WHERE name = 'sqlite_sequence'").Scan(&sequenceTable).Error; err != nil {
return err
}
if sequenceTable == 0 {
return nil
}
var sequence int64
if err := source.Raw("SELECT COALESCE(MAX(seq), 0) FROM sqlite_sequence WHERE name = ?", table).Scan(&sequence).Error; err != nil {
return err
}
if err := target.Exec("INSERT INTO sqlite_sequence (name, seq) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = ?)", table, sequence, table).Error; err != nil {
return err
}
return target.Exec("UPDATE sqlite_sequence SET seq = MAX(seq, ?) WHERE name = ?", sequence, table).Error
}
+1
View File
@@ -20,6 +20,7 @@ func Routers() *gin.Engine {
} }
Router = gin.Default() Router = gin.Default()
Router.Use(i18n.UseI18n()) Router.Use(i18n.UseI18n())
Router.Use(middleware.MCPErrorMetadata())
registerMCPRuntime(Router) registerMCPRuntime(Router)
PrivateGroup := Router.Group("/api/v2") PrivateGroup := Router.Group("/api/v2")
+13
View File
@@ -0,0 +1,13 @@
package middleware
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
"github.com/gin-gonic/gin"
)
func MCPErrorMetadata() gin.HandlerFunc {
return helper.WithErrorMetadata(func(c *gin.Context, _ int, _ string, err error) {
mcpapi.SetBusinessErrorHeader(c.Request, c.Writer.Header(), err)
})
}
+3
View File
@@ -60,6 +60,9 @@ func OperationResolveMeta() gin.HandlerFunc {
return return
} }
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2") reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
if strings.HasPrefix(reqPath, "/xpack/model/downloader/") {
reqPath = strings.Replace(reqPath, "/xpack/model/downloader/", "/ai/model/downloader/", 1)
}
meta, ok := metaMap[reqPath] meta, ok := metaMap[reqPath]
if !ok || len(meta.BeforeFunctions) == 0 { if !ok || len(meta.BeforeFunctions) == 0 {
c.Next() c.Next()
+2
View File
@@ -2,6 +2,7 @@ package router
func commonGroups() []CommonRouter { func commonGroups() []CommonRouter {
return []CommonRouter{ return []CommonRouter{
&VMRouter{},
&DashboardRouter{}, &DashboardRouter{},
&HostRouter{}, &HostRouter{},
&ContainerRouter{}, &ContainerRouter{},
@@ -23,6 +24,7 @@ func commonGroups() []CommonRouter {
&ProcessRouter{}, &ProcessRouter{},
&WebsiteCARouter{}, &WebsiteCARouter{},
&AIToolsRouter{}, &AIToolsRouter{},
&ModelDownloaderRouter{},
&GroupRouter{}, &GroupRouter{},
&AlertRouter{}, &AlertRouter{},
&RuntimeDiagnosticsRouter{}, &RuntimeDiagnosticsRouter{},
+4
View File
@@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("", baseApi.ContainerCreate) baRouter.POST("", baseApi.ContainerCreate)
baRouter.POST("/update", baseApi.ContainerUpdate) baRouter.POST("/update", baseApi.ContainerUpdate)
baRouter.POST("/upgrade", baseApi.ContainerUpgrade) baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
baRouter.GET("/image-updates", baseApi.GetContainerImageCheckUpdateStatus)
baRouter.POST("/image-updates/check", baseApi.ContainerImageCheckUpdate)
baRouter.POST("/image-updates/schedule", baseApi.SetContainerImageCheckUpdateSchedule)
baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer) baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/list", baseApi.ListContainer) baRouter.POST("/list", baseApi.ListContainer)
@@ -64,6 +67,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/template", baseApi.CreateComposeTemplate) baRouter.POST("/template", baseApi.CreateComposeTemplate)
baRouter.POST("/template/del", baseApi.DeleteComposeTemplate) baRouter.POST("/template/del", baseApi.DeleteComposeTemplate)
baRouter.POST("/image/remote/search", baseApi.SearchRemoteImages)
baRouter.GET("/image", baseApi.ListImage) baRouter.GET("/image", baseApi.ListImage)
baRouter.GET("/image/all", baseApi.ListAllImage) baRouter.GET("/image/all", baseApi.ListAllImage)
baRouter.POST("/image/search", baseApi.SearchImage) baRouter.POST("/image/search", baseApi.SearchImage)
+32
View File
@@ -0,0 +1,32 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
"github.com/gin-gonic/gin"
)
type ModelDownloaderRouter struct{}
func (m *ModelDownloaderRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
for _, prefix := range []string{"ai/model/downloader", "xpack/model/downloader"} {
router := Router.Group(prefix)
router.POST("/featured/search", baseApi.PageFeaturedModels)
router.POST("/featured/sync", baseApi.SyncFeaturedModels)
router.POST("/featured/sync/status", baseApi.UpdateFeaturedModelsSync)
router.POST("/hf/search", baseApi.SearchHFModels)
router.POST("/hf/info", baseApi.GetHFModelInfo)
router.POST("/hf/download", baseApi.StartHFModelDownload)
router.POST("/modelscope/search", baseApi.SearchModelScopeModels)
router.POST("/modelscope/info", baseApi.GetModelScopeModelInfo)
router.POST("/modelscope/download", baseApi.StartModelScopeModelDownload)
router.GET("/settings", baseApi.GetModelDownloaderSettings)
router.POST("/settings", baseApi.UpdateModelDownloaderSettings)
router.POST("/tasks/search", baseApi.PageModelDownloadTasks)
router.POST("/tasks/cancel", baseApi.CancelModelDownloadTask)
router.POST("/tasks/retry", baseApi.RetryModelDownloadTask)
router.POST("/tasks/remove", baseApi.RemoveModelDownloadTask)
router.POST("/local/search", baseApi.PageLocalDownloadedModels)
router.POST("/local/delete", baseApi.DeleteLocalDownloadedModel)
}
}
+59
View File
@@ -0,0 +1,59 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
"github.com/gin-gonic/gin"
)
type VMRouter struct{}
func (s *VMRouter) InitRouter(Router *gin.RouterGroup) {
vmRouter := Router.Group("vms")
baseApi := v2.ApiGroupApp.BaseApi
{
vmRouter.GET("/environment", baseApi.CheckVMEnvironment)
vmRouter.POST("/environment/enable", baseApi.EnableVMEnvironment)
vmRouter.POST("/search", baseApi.SearchVM)
vmRouter.POST("/sync", baseApi.SyncVMs)
vmRouter.POST("/info", baseApi.GetVM)
vmRouter.POST("/health/check", baseApi.HealthCheckVM)
vmRouter.POST("/stats", baseApi.GetVMStats)
vmRouter.POST("", baseApi.CreateVM)
vmRouter.POST("/templates/list", baseApi.ListVMTemplates)
vmRouter.POST("/templates", baseApi.CreateVMTemplate)
vmRouter.POST("/templates/del", baseApi.DeleteVMTemplate)
vmRouter.POST("/update", baseApi.UpdateVM)
vmRouter.POST("/disks/resize", baseApi.ResizeVMDisk)
vmRouter.POST("/rename", baseApi.RenameVM)
vmRouter.POST("/del", baseApi.DeleteVM)
vmRouter.POST("/snapshots/list", baseApi.ListVMSnapshots)
vmRouter.POST("/snapshots", baseApi.CreateVMSnapshot)
vmRouter.POST("/snapshots/recover", baseApi.RecoverVMSnapshot)
vmRouter.POST("/snapshots/del", baseApi.DeleteVMSnapshot)
vmRouter.POST("/orphans/scan", baseApi.ScanVMOrphans)
vmRouter.POST("/orphans/clean", baseApi.CleanVMOrphans)
vmRouter.POST("/operate", baseApi.OperateVM)
vmRouter.POST("/console", baseApi.GetVMConsole)
vmRouter.GET("/console/ws", baseApi.ProxyVMConsole)
vmRouter.POST("/iso/search", baseApi.SearchVMIso)
vmRouter.POST("/iso/list", baseApi.ListVMIso)
vmRouter.POST("/iso", baseApi.CreateVMIso)
vmRouter.POST("/iso/import/start", baseApi.StartVMIsoImport)
vmRouter.POST("/iso/import/update", baseApi.UpdateVMIsoImport)
vmRouter.POST("/iso/update", baseApi.UpdateVMIso)
vmRouter.POST("/iso/del", baseApi.DeleteVMIso)
vmRouter.POST("/networks/list", baseApi.ListVMNetworks)
vmRouter.GET("/networks/bridges", baseApi.ListVMNetworkBridges)
vmRouter.GET("/networks/interfaces", baseApi.ListVMNetworkInterfaces)
vmRouter.POST("/networks", baseApi.CreateVMNetwork)
vmRouter.POST("/networks/update", baseApi.UpdateVMNetwork)
vmRouter.POST("/networks/del", baseApi.DeleteVMNetwork)
vmRouter.POST("/storages/list", baseApi.ListVMStorages)
vmRouter.POST("/storages", baseApi.CreateVMStorage)
vmRouter.POST("/storages/update", baseApi.UpdateVMStorage)
vmRouter.POST("/storages/del", baseApi.DeleteVMStorage)
}
}
+2
View File
@@ -22,6 +22,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/init/dir" "github.com/1Panel-dev/1Panel/agent/init/dir"
"github.com/1Panel-dev/1Panel/agent/init/firewall" "github.com/1Panel-dev/1Panel/agent/init/firewall"
"github.com/1Panel-dev/1Panel/agent/init/hook" "github.com/1Panel-dev/1Panel/agent/init/hook"
"github.com/1Panel-dev/1Panel/agent/init/kvm"
"github.com/1Panel-dev/1Panel/agent/init/lang" "github.com/1Panel-dev/1Panel/agent/init/lang"
"github.com/1Panel-dev/1Panel/agent/init/log" "github.com/1Panel-dev/1Panel/agent/init/log"
"github.com/1Panel-dev/1Panel/agent/init/migration" "github.com/1Panel-dev/1Panel/agent/init/migration"
@@ -122,6 +123,7 @@ func Start() {
global.LOG.Info("agent startup: firewall init scheduled") global.LOG.Info("agent startup: firewall init scheduled")
InitOthers() InitOthers()
global.LOG.Info("agent startup: edition initialized") global.LOG.Info("agent startup: edition initialized")
kvm.Init()
rootRouter := router.Routers() rootRouter := router.Routers()
global.LOG.Info("agent startup: router initialized") global.LOG.Info("agent startup: router initialized")
+1
View File
@@ -72,6 +72,7 @@ type Device struct {
} }
type Basic struct { type Basic struct {
DrmDevice string `json:"-"`
UUID string `json:"uuid"` UUID string `json:"uuid"`
DeviceID int `json:"deviceID"` DeviceID int `json:"deviceID"`
+4
View File
@@ -93,6 +93,7 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
xpu := Device{ xpu := Device{
ProcessStatus: "unavailable", ProcessStatus: "unavailable",
Basic: Basic{ Basic: Basic{
DrmDevice: device.DrmDevice,
UUID: device.UUID, UUID: device.UUID,
DeviceID: device.DeviceID, DeviceID: device.DeviceID,
DeviceName: device.DeviceName, DeviceName: device.DeviceName,
@@ -137,6 +138,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
return return
} }
if info.DrmDevice != "" {
xpu.Basic.DrmDevice = info.DrmDevice
}
xpu.Basic.DriverVersion = info.DriverVersion xpu.Basic.DriverVersion = info.DriverVersion
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64) bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
+2
View File
@@ -202,6 +202,8 @@ func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string
} }
func (c Client) PullImageWithProcessAndOptionsContext(ctx context.Context, task *task.Task, imageName string, options image.PullOptions) error { func (c Client) PullImageWithProcessAndOptionsContext(ctx context.Context, task *task.Task, imageName string, options image.PullOptions) error {
ctx, cancel := context.WithTimeout(ctx, 24*time.Hour)
defer cancel()
out, err := c.cli.ImagePull(ctx, imageName, options) out, err := c.cli.ImagePull(ctx, imageName, options)
if err != nil { if err != nil {
return err return err
+269
View File
@@ -0,0 +1,269 @@
// Package dockerhub checks Docker Hub metadata without downloading image layers.
package dockerhub
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/distribution/reference"
)
const registryURL = "https://registry-1.docker.io"
// Reference only accepts tagged Docker Hub references. Digest pins are intentional.
func Reference(raw string) (string, string, bool) {
if strings.HasPrefix(raw, "sha256:") {
return "", "", false
}
ref, err := reference.ParseNormalizedNamed(raw)
if err != nil || reference.Domain(ref) != "docker.io" {
return "", "", false
}
if _, pinned := ref.(reference.Digested); pinned {
return "", "", false
}
tagged := reference.TagNameOnly(ref).(reference.NamedTagged)
return reference.Path(ref), tagged.Tag(), true
}
type Client struct {
HTTP *http.Client
repository string
token string
}
type Platform struct{ OS, Architecture, Variant string }
type Target struct{ Tag, Digest, ConfigDigest string }
func New(ctx context.Context, repository, username, password string, httpClient *http.Client) (*Client, error) {
if httpClient == nil {
httpClient = &http.Client{Timeout: 30 * time.Second}
}
c := &Client{HTTP: httpClient, repository: repository}
endpoint := "https://auth.docker.io/token?service=registry.docker.io&scope=" + url.QueryEscape("repository:"+repository+":pull")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
if username != "" {
req.SetBasicAuth(username, password)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Docker Hub authentication: HTTP %d", resp.StatusCode)
}
var body struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
return nil, err
}
c.token = body.Token
if c.token == "" {
c.token = body.AccessToken
}
if c.token == "" {
return nil, fmt.Errorf("Docker Hub returned an empty token")
}
return c, nil
}
func (c *Client) get(ctx context.Context, endpoint string, result interface{}) (http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json")
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Docker Hub metadata: HTTP %d", resp.StatusCode)
}
err = json.NewDecoder(io.LimitReader(resp.Body, 16<<20)).Decode(result)
return resp.Header, err
}
func (c *Client) Tags(ctx context.Context) ([]string, error) {
endpoint := registryURL + "/v2/" + c.repository + "/tags/list?n=1000"
var tags []string
for page := 0; page < 100; page++ {
var body struct {
Tags []string `json:"tags"`
}
headers, err := c.get(ctx, endpoint, &body)
if err != nil {
return nil, err
}
tags = append(tags, body.Tags...)
link := headers.Get("Link")
if link == "" {
return tags, nil
}
start, end := strings.Index(link, "<"), strings.Index(link, ">")
if start < 0 || end <= start {
return nil, fmt.Errorf("invalid Docker Hub pagination")
}
base, _ := url.Parse(registryURL)
next, err := base.Parse(link[start+1 : end])
if err != nil || next.Scheme != "https" || next.Host != base.Host || next.Path != "/v2/"+c.repository+"/tags/list" {
return nil, fmt.Errorf("invalid Docker Hub pagination URL")
}
endpoint = next.String()
}
return nil, fmt.Errorf("Docker Hub tag pagination limit exceeded")
}
type manifest struct {
Config struct {
Digest string `json:"digest"`
} `json:"config"`
Manifests []struct {
Digest string `json:"digest"`
Platform struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
Variant string `json:"variant"`
} `json:"platform"`
} `json:"manifests"`
}
func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Target, bool, error) {
var m manifest
headers, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(tag), &m)
if err != nil {
return Target{}, false, err
}
digest := headers.Get("Docker-Content-Digest")
if len(m.Manifests) > 0 {
digest = ""
for _, item := range m.Manifests {
if item.Platform.OS == platform.OS && item.Platform.Architecture == platform.Architecture && normalizedVariant(platform.Architecture, item.Platform.Variant) == normalizedVariant(platform.Architecture, platform.Variant) {
digest = item.Digest
break
}
}
if digest == "" {
return Target{}, false, nil
}
m = manifest{}
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(digest), &m); err != nil {
return Target{}, false, err
}
}
if m.Config.Digest == "" {
return Target{}, false, fmt.Errorf("unsupported Docker Hub manifest")
}
// Validate single-platform manifests too; the config digest is the Docker Image ID.
var config struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
Variant string `json:"variant"`
}
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/blobs/"+url.PathEscape(m.Config.Digest), &config); err != nil {
return Target{}, false, err
}
if config.OS != platform.OS || config.Architecture != platform.Architecture || (platform.Variant != "" && config.Variant != "" && config.Variant != platform.Variant) {
return Target{}, false, nil
}
return Target{Tag: tag, Digest: digest, ConfigDigest: m.Config.Digest}, true, nil
}
var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(-[a-zA-Z][a-zA-Z0-9_.-]*)?$`)
var prereleasePattern = regexp.MustCompile(`(?i)(^|[-_.])(alpha|beta|rc|dev|nightly|edge|test|unstable|preview)([0-9_.-]|$)`)
type version struct {
numbers [3]uint64
parts int
variant string
}
func parseVersion(tag string) (version, bool) {
m := versionPattern.FindStringSubmatch(tag)
if m == nil || prereleasePattern.MatchString(m[4]) {
return version{}, false
}
v := version{parts: 2, variant: m[4]}
for i := 0; i < 3; i++ {
if m[i+1] != "" {
n, err := strconv.ParseUint(m[i+1], 10, 64)
if err != nil {
return version{}, false
}
v.numbers[i] = n
}
}
if m[3] != "" {
v.parts = 3
}
return v, true
}
func greater(a, b version) bool {
for i := 0; i < 3; i++ {
if a.numbers[i] != b.numbers[i] {
return a.numbers[i] > b.numbers[i]
}
}
return false
}
// Candidates preserves the current tag's major version, variant and precision. Unknown/floating
// tags are checked only for content changes, never guessed from upload dates.
func Candidates(current string, tags []string) []string {
baseline, ok := parseVersion(current)
if !ok {
return []string{current}
}
type candidate struct {
tag string
version version
}
candidates := []candidate{}
seen := map[string]bool{}
for _, tag := range tags {
v, valid := parseVersion(tag)
if valid && v.numbers[0] == baseline.numbers[0] && v.parts == baseline.parts && v.variant == baseline.variant && greater(v, baseline) && !seen[tag] {
candidates = append(candidates, candidate{tag: tag, version: v})
seen[tag] = true
}
}
sort.Slice(candidates, func(i, j int) bool {
a, b := candidates[i], candidates[j]
if a.version.numbers == b.version.numbers {
return a.tag < b.tag
}
return greater(a.version, b.version)
})
result := make([]string, 0, len(candidates)+1)
for _, candidate := range candidates {
result = append(result, candidate.tag)
}
return append(result, current)
}
func NeedsTags(tag string) bool { _, ok := parseVersion(tag); return ok }
func normalizedVariant(architecture, variant string) string {
if architecture == "arm64" && variant == "" {
return "v8"
}
return variant
}
+71
View File
@@ -0,0 +1,71 @@
package dockerhub
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
type SearchItem struct {
Name string `json:"name"`
Description string `json:"description"`
}
type SearchResult struct {
Items []SearchItem `json:"items"`
Total int64 `json:"total"`
ProxyRequired bool `json:"proxyRequired"`
}
// Search uses the panel HTTP client, not the Docker daemon's proxy settings.
func Search(ctx context.Context, client *http.Client, query string, page, pageSize int) (SearchResult, error) {
result := SearchResult{Items: []SearchItem{}}
query = strings.TrimSpace(query)
if query == "" || len([]rune(query)) > 256 || page < 1 || pageSize < 1 || pageSize > 100 {
return result, fmt.Errorf("invalid Docker Hub search parameters")
}
params := url.Values{"query": {query}, "page": {strconv.Itoa(page)}, "page_size": {strconv.Itoa(pageSize)}}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://hub.docker.com/v2/search/repositories/?"+params.Encode(), nil)
if err != nil {
return result, err
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
// Transport errors can contain proxy credentials; do not expose their raw text.
if ctx.Err() != nil {
return result, ctx.Err()
}
return result, fmt.Errorf("Docker Hub connection failed; check network and proxy settings")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return result, fmt.Errorf("Docker Hub search: HTTP %d", resp.StatusCode)
}
var body struct {
Count *int64 `json:"count"`
Results []struct {
Name string `json:"repo_name"`
Description string `json:"short_description"`
} `json:"results"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(&body); err != nil {
return result, fmt.Errorf("invalid Docker Hub search response")
}
if body.Count == nil || *body.Count < 0 {
return result, fmt.Errorf("invalid Docker Hub search response")
}
result.Total = *body.Count
for _, item := range body.Results {
if item.Name == "" {
return result, fmt.Errorf("invalid Docker Hub repository name")
}
result.Items = append(result.Items, SearchItem{Name: item.Name, Description: item.Description})
}
return result, nil
}

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