mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-06 16:05:21 +00:00
Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd462e39e8 | ||
|
|
d7783e72cb | ||
|
|
e8ac89dcd2 | ||
|
|
8a68d30dcb | ||
|
|
66e9862cba | ||
|
|
23a54653f8 | ||
|
|
56ecd56c6d | ||
|
|
024a246a46 | ||
|
|
fe3953963e | ||
|
|
d658395188 | ||
|
|
51bae946e1 | ||
|
|
56c7981b3a | ||
|
|
730dbbab5b | ||
|
|
2e9c1794f2 | ||
|
|
d1059c7356 | ||
|
|
b07afd3785 | ||
|
|
89787a55ee | ||
|
|
5b9aaa7d7a | ||
|
|
3a30f69b91 | ||
|
|
30381a3c68 | ||
|
|
37170ae81a | ||
|
|
daae8c43f8 | ||
|
|
133a02741a | ||
|
|
fac15e3292 | ||
|
|
e29679768b | ||
|
|
8e1b761f4c | ||
|
|
963c1463a8 | ||
|
|
2401bb16a2 | ||
|
|
c06c4e338b | ||
|
|
f8c61993c5 | ||
|
|
b31e683f1b | ||
|
|
50f0a37bee | ||
|
|
51cf40a648 | ||
|
|
7ec31cd5e9 | ||
|
|
50a45b72f3 | ||
|
|
13362f84a6 | ||
|
|
046dc94d05 | ||
|
|
2059119d89 | ||
|
|
7f4c503982 | ||
|
|
93ebc6a8b9 | ||
|
|
9a49f38ffd | ||
|
|
7156addf79 | ||
|
|
c6329a62db | ||
|
|
9644d5429f | ||
|
|
8b6328fd04 | ||
|
|
e9c2162921 |
@@ -79,3 +79,5 @@ opencode.json
|
|||||||
superpowers
|
superpowers
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.codex/
|
.codex/
|
||||||
|
|
||||||
|
/agent/utils/vm/helper/bin/1panel-kvm*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+31
-11
@@ -380,7 +380,7 @@ func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Get model account providers
|
// @Summary Get model service providers
|
||||||
// @Success 200 {array} dto.ProviderInfo
|
// @Success 200 {array} dto.ProviderInfo
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
@@ -395,7 +395,7 @@ func (b *BaseApi) GetAgentProviders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Create model account
|
// @Summary Create model service
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountCreateReq true "request"
|
// @Param request body dto.AgentAccountCreateReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -415,7 +415,7 @@ func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Update model account
|
// @Summary Update model service
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountUpdateReq true "request"
|
// @Param request body dto.AgentAccountUpdateReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -435,7 +435,7 @@ func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page model accounts
|
// @Summary Page model services
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountSearch true "request"
|
// @Param request body dto.AgentAccountSearch true "request"
|
||||||
// @Success 200 {object} dto.PageResult
|
// @Success 200 {object} dto.PageResult
|
||||||
@@ -459,7 +459,7 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Count model accounts by provider
|
// @Summary Count model services by provider
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountProviderCountReq true "request"
|
// @Param request body dto.AgentAccountProviderCountReq true "request"
|
||||||
// @Success 200 {object} map[string]int64
|
// @Success 200 {object} map[string]int64
|
||||||
@@ -480,7 +480,7 @@ func (b *BaseApi) CountAgentAccountsByProviders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary List model account models
|
// @Summary List model service models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountModelReq true "request"
|
// @Param request body dto.AgentAccountModelReq true "request"
|
||||||
// @Success 200 {array} dto.AgentAccountModel
|
// @Success 200 {array} dto.AgentAccountModel
|
||||||
@@ -500,6 +500,26 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Update verification model
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentAccountVerifyModelUpdateReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/accounts/models/verify-model [post]
|
||||||
|
func (b *BaseApi) UpdateAgentAccountVerifyModel(c *gin.Context) {
|
||||||
|
var req dto.AgentAccountVerifyModelUpdateReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.UpdateAccountVerifyModel(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Discover custom provider models
|
// @Summary Discover custom provider models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -522,7 +542,7 @@ func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Create model account model
|
// @Summary Create model service model
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountModelCreateReq true "request"
|
// @Param request body dto.AgentAccountModelCreateReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -542,7 +562,7 @@ func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Update model account model
|
// @Summary Update model service model
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountModelUpdateReq true "request"
|
// @Param request body dto.AgentAccountModelUpdateReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -562,7 +582,7 @@ func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Delete model account model
|
// @Summary Delete model service model
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountModelDeleteReq true "request"
|
// @Param request body dto.AgentAccountModelDeleteReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -582,7 +602,7 @@ func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Verify model account
|
// @Summary Verify model service
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountVerifyReq true "request"
|
// @Param request body dto.AgentAccountVerifyReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -602,7 +622,7 @@ func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Delete model account
|
// @Summary Delete model service
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.AgentAccountDeleteReq true "request"
|
// @Param request body dto.AgentAccountDeleteReq true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -11,16 +11,16 @@ 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()
|
||||||
|
|
||||||
aiToolService = service.NewIAIToolService()
|
aiToolService = service.NewIAIToolService()
|
||||||
mcpServerService = service.NewIMcpServerService()
|
mcpServerService = service.NewIMcpServerService()
|
||||||
tensorrtLLMService = service.NewITensorRTLLMService()
|
agentService = service.NewIAgentService()
|
||||||
agentService = service.NewIAgentService()
|
|
||||||
|
|
||||||
containerService = service.NewIContainerService()
|
containerService = service.NewIContainerService()
|
||||||
composeTemplateService = service.NewIComposeTemplateService()
|
composeTemplateService = service.NewIComposeTemplateService()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,3 +103,46 @@ func (b *BaseApi) DeletePHPExtensions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Router /runtimes/php/extensions/detail [post]
|
||||||
|
func (b *BaseApi) GetPHPExtensions(c *gin.Context) {
|
||||||
|
var req request.PHPExtensionsDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := phpExtensionsService.Get(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Router /runtimes/php/extensions/import [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 PHP 扩展模版 [name]","formatEN":"Sync PHP extension template [name]"}
|
||||||
|
func (b *BaseApi) ImportPHPExtensions(c *gin.Context) {
|
||||||
|
var req request.PHPExtensionsCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := phpExtensionsService.Import(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Router /runtimes/php/extensions/push [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["sourceNode","targetNodes"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"推送 PHP 扩展模版 [sourceNode] 至 [targetNodes]","formatEN":"Push PHP extension template from [sourceNode] to [targetNodes]"}
|
||||||
|
func (b *BaseApi) PushPHPExtensions(c *gin.Context) {
|
||||||
|
var req dto.PHPExtensionsPush
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := phpExtensionsService.Push(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @Tags TensorRT LLM
|
|
||||||
// @Summary Page TensorRT LLMs
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body request.TensorRTLLMSearch true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/tensorrt/search [post]
|
|
||||||
func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
|
|
||||||
var req request.TensorRTLLMSearch
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
list := tensorrtLLMService.Page(req)
|
|
||||||
helper.SuccessWithData(c, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags TensorRT LLM
|
|
||||||
// @Summary Create TensorRT LLM
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body request.TensorRTLLMCreate true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/tensorrt/create [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 TensorRT LLM [name]","formatEN":"create TensorRT LLM [name]"}
|
|
||||||
func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
|
|
||||||
var req request.TensorRTLLMCreate
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := tensorrtLLMService.Create(req)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags TensorRT LLM
|
|
||||||
// @Summary Update TensorRT LLM
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body request.TensorRTLLMUpdate true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/tensorrt/update [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["id","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 TensorRT LLM [id][name]","formatEN":"update TensorRT LLM [id][name]"}
|
|
||||||
func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
|
|
||||||
var req request.TensorRTLLMUpdate
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := tensorrtLLMService.Update(req)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags TensorRT LLM
|
|
||||||
// @Summary Delete TensorRT LLM
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body request.TensorRTLLMDelete true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/tensorrt/delete [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 TensorRT LLM [id]","formatEN":"delete TensorRT LLM [id]"}
|
|
||||||
func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
|
|
||||||
var req request.TensorRTLLMDelete
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := tensorrtLLMService.Delete(req.ID)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags TensorRT LLM
|
|
||||||
// @Summary Operate TensorRT LLM
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body request.TensorRTLLMOperate true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/tensorrt/operate [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["id","operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作 TensorRT LLM [id][operate]","formatEN":"operate TensorRT LLM [id][operate]"}
|
|
||||||
func (b *BaseApi) OperateTensorRTLLM(c *gin.Context) {
|
|
||||||
var req request.TensorRTLLMOperate
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := tensorrtLLMService.Operate(req)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
+48
-35
@@ -301,15 +301,21 @@ type AgentOverviewSnapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModel struct {
|
type AgentAccountModel struct {
|
||||||
RecordID uint `json:"recordId"`
|
RecordID uint `json:"recordId"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
IsVerifyModel bool `json:"isVerifyModel,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelReq struct {
|
type AgentAccountModelReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentAccountVerifyModelUpdateReq struct {
|
||||||
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
|
RecordID uint `json:"recordId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentAccountModelDiscoverReq struct {
|
type AgentAccountModelDiscoverReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
BaseURL string `json:"baseURL" validate:"required"`
|
BaseURL string `json:"baseURL" validate:"required"`
|
||||||
@@ -318,6 +324,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,8 +335,9 @@ type AgentAccountModelUpdateReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelDeleteReq struct {
|
type AgentAccountModelDeleteReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
DeleteFiles bool `json:"deleteFiles"`
|
||||||
RecordID uint `json:"recordId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
|
RecordID uint `json:"recordId" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountCreateReq struct {
|
type AgentAccountCreateReq struct {
|
||||||
@@ -347,24 +355,26 @@ type AgentAccountCreateReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountUpdateReq struct {
|
type AgentAccountUpdateReq struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
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 {
|
||||||
Provider string `json:"provider" validate:"required"`
|
ID uint `json:"id"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
Provider string `json:"provider" validate:"required_without=ID"`
|
||||||
|
APIKey string `json:"apiKey" validate:"required_without=ID"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required_without=ID"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
}
|
}
|
||||||
@@ -386,22 +396,25 @@ type AgentAccountProviderCountReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountInfo struct {
|
type AgentAccountInfo struct {
|
||||||
AppInstallID uint `json:"appInstallId"`
|
AppInstallID uint `json:"appInstallId"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
MasterAccountID uint `json:"masterAccountId"`
|
MasterAccountID uint `json:"masterAccountId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
ProviderName string `json:"providerName"`
|
ProviderName string `json:"providerName"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
Verified bool `json:"verified"`
|
VerifiedAt *time.Time `json:"verifiedAt"`
|
||||||
Remark string `json:"remark"`
|
VerifyError string `json:"verifyError"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Verified bool `json:"verified"`
|
||||||
|
ValidateAvailability bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderModelInfo struct {
|
type ProviderModelInfo struct {
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ type InspectReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ContainerInfo struct {
|
type ContainerInfo struct {
|
||||||
ContainerID string `json:"containerID"`
|
LatestVersion string `json:"latestVersion"`
|
||||||
Name string `json:"name"`
|
TargetImage string `json:"targetImage"`
|
||||||
ImageId string `json:"imageID"`
|
HasUpdate bool `json:"hasUpdate"`
|
||||||
ImageName string `json:"imageName"`
|
ContainerID string `json:"containerID"`
|
||||||
CreateTime string `json:"createTime"`
|
Name string `json:"name"`
|
||||||
State string `json:"state"`
|
ImageId string `json:"imageID"`
|
||||||
RunTime string `json:"runTime"`
|
ImageName string `json:"imageName"`
|
||||||
|
CreateTime string `json:"createTime"`
|
||||||
|
State string `json:"state"`
|
||||||
|
RunTime string `json:"runTime"`
|
||||||
|
|
||||||
Network []string `json:"network"`
|
Network []string `json:"network"`
|
||||||
Ports []string `json:"ports"`
|
Ports []string `json:"ports"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// PHPExtensionsPush is submitted to the master agent, with an explicit source node.
|
||||||
|
type PHPExtensionsPush struct {
|
||||||
|
SourceNode string `json:"sourceNode" validate:"required"`
|
||||||
|
TargetNodes []string `json:"targetNodes" validate:"required,min=1,dive,required"`
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
TaskID string `json:"taskID" validate:"required,uuid"`
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
type MCPWebsiteBinding struct {
|
type MCPWebsiteBinding struct {
|
||||||
Operation string `json:"operation"`
|
Operation string `json:"operation"`
|
||||||
RequestID string `json:"requestId"`
|
RequestID string `json:"requestId"`
|
||||||
BindingID string `json:"bindingId"`
|
BindingID string `json:"bindingId"`
|
||||||
WebsiteID uint `json:"websiteId"`
|
WebsiteID uint `json:"websiteId"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
EnableSSL bool `json:"enableSSL"`
|
EnableSSL bool `json:"enableSSL"`
|
||||||
SSLID uint `json:"sslID"`
|
SSLID uint `json:"sslID"`
|
||||||
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,7 +4,8 @@ import "github.com/1Panel-dev/1Panel/agent/app/dto"
|
|||||||
|
|
||||||
type PHPExtensionsSearch struct {
|
type PHPExtensionsSearch struct {
|
||||||
dto.PageInfo
|
dto.PageInfo
|
||||||
All bool `json:"all"`
|
All bool `json:"all"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PHPExtensionsCreate struct {
|
type PHPExtensionsCreate struct {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package request
|
|
||||||
|
|
||||||
import "github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
||||||
|
|
||||||
type TensorRTLLMSearch struct {
|
|
||||||
dto.PageInfo
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TensorRTLLMCreate struct {
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
ContainerName string `json:"containerName" validate:"required"`
|
|
||||||
Version string `json:"version" validate:"required"`
|
|
||||||
ModelDir string `json:"modelDir" validate:"required"`
|
|
||||||
Image string `json:"image" validate:"required"`
|
|
||||||
Command string `json:"command" validate:"required"`
|
|
||||||
ModelType string `json:"modelType"`
|
|
||||||
ModelSpeedup bool `json:"modelSpeedup"`
|
|
||||||
GPU *dto.ModelGPUConfig `json:"gpu"`
|
|
||||||
DockerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type TensorRTLLMUpdate struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
TensorRTLLMCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
type TensorRTLLMDelete struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TensorRTLLMOperate struct {
|
|
||||||
ID uint `json:"id" validate:"required"`
|
|
||||||
Operate string `json:"operate" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerConfig struct {
|
|
||||||
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
|
||||||
Environments []Environment `json:"environments"`
|
|
||||||
Volumes []Volume `json:"volumes"`
|
|
||||||
}
|
|
||||||
@@ -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,17 +1,18 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
type MCPWebsiteBinding struct {
|
type MCPWebsiteBinding struct {
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
WebsiteID uint `json:"websiteId"`
|
WebsiteID uint `json:"websiteId"`
|
||||||
BindingID string `json:"bindingId"`
|
BindingID string `json:"bindingId"`
|
||||||
RequestID string `json:"requestId"`
|
RequestID string `json:"requestId"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
EnableSSL bool `json:"enableSSL"`
|
EnableSSL bool `json:"enableSSL"`
|
||||||
SSLID uint `json:"sslID"`
|
SSLID uint `json:"sslID"`
|
||||||
WebsiteStatus string `json:"websiteStatus"`
|
WebsiteStatus string `json:"websiteStatus"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ type NginxParam struct {
|
|||||||
// NginxBrotliRes carries the brotli settings together with where they live.
|
// NginxBrotliRes carries the brotli settings together with where they live.
|
||||||
// ManagedExternally is true when the user defined brotli by hand, in which
|
// ManagedExternally is true when the user defined brotli by hand, in which
|
||||||
// case the panel only reports the values and must not write its own copy.
|
// case the panel only reports the values and must not write its own copy.
|
||||||
// ManagedUnavailable is true when the panel could not wire the managed
|
// ManagedPending is true when nginx does not load the managed configuration
|
||||||
|
// yet but saving will wire it in, so the reported values are not in effect
|
||||||
|
// until then.
|
||||||
|
// ManagedUnavailable is true when the panel cannot wire the managed
|
||||||
// configuration into nginx.conf at all, so the reported values are inert.
|
// configuration into nginx.conf at all, so the reported values are inert.
|
||||||
type NginxBrotliRes struct {
|
type NginxBrotliRes struct {
|
||||||
Params []NginxParam `json:"params"`
|
Params []NginxParam `json:"params"`
|
||||||
ManagedExternally bool `json:"managedExternally"`
|
ManagedExternally bool `json:"managedExternally"`
|
||||||
|
ManagedPending bool `json:"managedPending"`
|
||||||
ManagedUnavailable bool `json:"managedUnavailable"`
|
ManagedUnavailable bool `json:"managedUnavailable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package response
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TensorRTLLMsRes struct {
|
|
||||||
Items []TensorRTLLMDTO `json:"items"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TensorRTLLMDTO struct {
|
|
||||||
model.TensorRTLLM
|
|
||||||
Version string `json:"version"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
ModelDir string `json:"modelDir"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
GPU *dto.ModelGPUConfig `json:"gpu"`
|
|
||||||
|
|
||||||
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
|
|
||||||
Environments []request.Environment `json:"environments"`
|
|
||||||
Volumes []request.Volume `json:"volumes"`
|
|
||||||
}
|
|
||||||
@@ -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,19 +1,24 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type AgentAccount struct {
|
type AgentAccount struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
AppInstallID uint `json:"appInstallId"`
|
AppInstallID uint `json:"appInstallId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
Verified bool `json:"verified"`
|
VerifiedAt *time.Time `json:"verifiedAt"`
|
||||||
Remark string `json:"remark"`
|
VerifyError string `json:"verifyError"`
|
||||||
MasterAccountID uint `json:"masterAccountId" gorm:"index"`
|
Verified bool `json:"verified"`
|
||||||
|
ValidateAvailability bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
MasterAccountID uint `json:"masterAccountId" gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentAccount) TableName() string {
|
func (AgentAccount) TableName() string {
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type TensorRTLLM struct {
|
|
||||||
BaseModel
|
|
||||||
Name string `json:"name"`
|
|
||||||
DockerCompose string `json:"dockerCompose"`
|
|
||||||
ContainerName string `json:"containerName"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Env string `json:"env"`
|
|
||||||
TaskID string `json:"taskID"`
|
|
||||||
ModelType string `json:"modelType"`
|
|
||||||
ModelSpeedup bool `json:"modelSpeedup"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ var catalog = map[string]Meta{
|
|||||||
},
|
},
|
||||||
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||||
"llmman": {
|
"llmman": {
|
||||||
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
Key: "llmman", DisplayName: "llmman", Sort: 75, DefaultAPIType: "openai-responses",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||||
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||||
@@ -59,6 +59,10 @@ var catalog = map[string]Meta{
|
|||||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||||
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||||
},
|
},
|
||||||
|
"1panel-ai-gateway": {
|
||||||
|
Key: "1panel-ai-gateway", DisplayName: "1Panel AI Gateway", DisplayNameKey: "AIProvider1PanelGateway", Sort: 21, DefaultAPIType: "openai-completions", EnvKey: "ONEPANEL_AI_GATEWAY_API_KEY",
|
||||||
|
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||||
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
|
|||||||
@@ -32,15 +32,6 @@ const (
|
|||||||
defaultVerifyMaxTokens = 16
|
defaultVerifyMaxTokens = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
func SkipVerification(provider string) bool {
|
|
||||||
switch provider {
|
|
||||||
case "vllm", "ollama", "llmman", "kimi-coding":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
|
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
|
||||||
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
|
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
|
||||||
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
|
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ type IAgentAccountRepo interface {
|
|||||||
WithByMasterAccountID(masterID uint) DBOption
|
WithByMasterAccountID(masterID uint) DBOption
|
||||||
Create(account *model.AgentAccount) error
|
Create(account *model.AgentAccount) error
|
||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
|
UpdateVerification(account *model.AgentAccount) error
|
||||||
|
ResetVerificationByAppInstallID(appInstallID uint) 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 {
|
||||||
@@ -57,6 +60,30 @@ func (a AgentAccountRepo) Save(account *model.AgentAccount) error {
|
|||||||
return getDb().Save(account).Error
|
return getDb().Save(account).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountRepo) UpdateVerification(account *model.AgentAccount) error {
|
||||||
|
result := getDb().Model(&model.AgentAccount{}).Where("id = ?", account.ID).Updates(map[string]interface{}{
|
||||||
|
"verified": account.Verified,
|
||||||
|
"verified_at": account.VerifiedAt,
|
||||||
|
"verify_error": account.VerifyError,
|
||||||
|
"verify_model": account.VerifyModel,
|
||||||
|
})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountRepo) ResetVerificationByAppInstallID(appInstallID uint) error {
|
||||||
|
return getDb().Model(&model.AgentAccount{}).Where("app_install_id = ? AND validate_availability = ?", appInstallID, true).Updates(map[string]interface{}{
|
||||||
|
"verified": false,
|
||||||
|
"verified_at": nil,
|
||||||
|
"verify_error": "",
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (a AgentAccountRepo) DeleteByID(id uint) error {
|
func (a AgentAccountRepo) DeleteByID(id uint) error {
|
||||||
return getDb().Delete(&model.AgentAccount{}, id).Error
|
return getDb().Delete(&model.AgentAccount{}, id).Error
|
||||||
}
|
}
|
||||||
@@ -69,6 +96,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 +132,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
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import "github.com/1Panel-dev/1Panel/agent/app/model"
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type AgentAccountModelRepo struct{}
|
type AgentAccountModelRepo struct{}
|
||||||
|
|
||||||
@@ -9,7 +12,9 @@ type IAgentAccountModelRepo interface {
|
|||||||
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
|
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
|
||||||
Create(item *model.AgentAccountModel) error
|
Create(item *model.AgentAccountModel) error
|
||||||
Save(item *model.AgentAccountModel) error
|
Save(item *model.AgentAccountModel) error
|
||||||
|
SaveWithAccount(item *model.AgentAccountModel, account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
|
DeleteWithAccount(id uint, account *model.AgentAccount) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,10 +46,28 @@ func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) error {
|
|||||||
return getDb().Save(item).Error
|
return getDb().Save(item).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountModelRepo) SaveWithAccount(item *model.AgentAccountModel, account *model.AgentAccount) error {
|
||||||
|
return getDb().Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Save(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Save(account).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
|
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
|
||||||
return getDb().Delete(&model.AgentAccountModel{}, id).Error
|
return getDb().Delete(&model.AgentAccountModel{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountModelRepo) DeleteWithAccount(id uint, account *model.AgentAccount) error {
|
||||||
|
return getDb().Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Delete(&model.AgentAccountModel{}, id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Save(account).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
|
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
|
||||||
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
|
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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-services"},
|
||||||
{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"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import "github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
|
|
||||||
type TensorRTLLMRepo struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type ITensorRTLLMRepo interface {
|
|
||||||
Page(page, size int, opts ...DBOption) (int64, []model.TensorRTLLM, error)
|
|
||||||
GetFirst(opts ...DBOption) (*model.TensorRTLLM, error)
|
|
||||||
Create(tensorrtLLM *model.TensorRTLLM) error
|
|
||||||
Save(tensorrtLLM *model.TensorRTLLM) error
|
|
||||||
DeleteBy(opts ...DBOption) error
|
|
||||||
List(opts ...DBOption) ([]model.TensorRTLLM, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewITensorRTLLMRepo() ITensorRTLLMRepo {
|
|
||||||
return &TensorRTLLMRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) Page(page, size int, opts ...DBOption) (int64, []model.TensorRTLLM, error) {
|
|
||||||
var servers []model.TensorRTLLM
|
|
||||||
db := getDb(opts...).Model(&model.TensorRTLLM{})
|
|
||||||
count := int64(0)
|
|
||||||
db = db.Count(&count)
|
|
||||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
|
|
||||||
return count, servers, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) GetFirst(opts ...DBOption) (*model.TensorRTLLM, error) {
|
|
||||||
var tensorrtLLM model.TensorRTLLM
|
|
||||||
if err := getDb(opts...).First(&tensorrtLLM).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tensorrtLLM, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) List(opts ...DBOption) ([]model.TensorRTLLM, error) {
|
|
||||||
var tensorrtLLMs []model.TensorRTLLM
|
|
||||||
if err := getDb(opts...).Find(&tensorrtLLMs).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tensorrtLLMs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) Create(tensorrtLLM *model.TensorRTLLM) error {
|
|
||||||
return getDb().Create(tensorrtLLM).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) Save(tensorrtLLM *model.TensorRTLLM) error {
|
|
||||||
return getDb().Save(tensorrtLLM).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMRepo) DeleteBy(opts ...DBOption) error {
|
|
||||||
return getDb(opts...).Delete(&model.TensorRTLLM{}).Error
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+317
-54
@@ -79,6 +79,7 @@ type IAgentService interface {
|
|||||||
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
||||||
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
||||||
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
||||||
|
UpdateAccountVerifyModel(req dto.AgentAccountVerifyModelUpdateReq) error
|
||||||
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
||||||
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
||||||
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
||||||
@@ -184,7 +185,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
storedModel := ""
|
storedModel := ""
|
||||||
var allowedOrigins []string
|
var allowedOrigins []string
|
||||||
var account *model.AgentAccount
|
var account *model.AgentAccount
|
||||||
var installHooks *appInstallHooks
|
var installHooks *AppInstallHooks
|
||||||
var dashboardAuth agentDashboardAuth
|
var dashboardAuth agentDashboardAuth
|
||||||
|
|
||||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||||
@@ -195,7 +196,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !account.Verified {
|
if account.ValidateAvailability && !account.Verified {
|
||||||
return nil, buserr.New("ErrAgentAccountNotVerified")
|
return nil, buserr.New("ErrAgentAccountNotVerified")
|
||||||
}
|
}
|
||||||
provider = account.Provider
|
provider = account.Provider
|
||||||
@@ -224,14 +225,14 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
token = generateToken()
|
token = generateToken()
|
||||||
}
|
}
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &AppInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
|
return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if agentType == constant.AppHermesAgent {
|
} else if agentType == constant.AppHermesAgent {
|
||||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &AppInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -298,7 +299,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
DockerCompose: req.DockerCompose,
|
DockerCompose: req.DockerCompose,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
appInstall, err := AppService{}.installWithHooks(installReq, false, installHooks)
|
appInstall, err := AppService{}.InstallWithHooks(installReq, false, installHooks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1041,8 +1042,6 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||||
agentAccountMutationMu.Lock()
|
|
||||||
defer agentAccountMutationMu.Unlock()
|
|
||||||
provider := req.Provider
|
provider := req.Provider
|
||||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1060,17 +1059,27 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
account := &model.AgentAccount{
|
account := &model.AgentAccount{
|
||||||
Provider: resolvedInput.Provider,
|
Provider: resolvedInput.Provider,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
APIKey: resolvedInput.APIKey,
|
APIKey: resolvedInput.APIKey,
|
||||||
RememberAPIKey: req.RememberAPIKey,
|
RememberAPIKey: req.RememberAPIKey,
|
||||||
BaseURL: resolvedInput.BaseURL,
|
BaseURL: resolvedInput.BaseURL,
|
||||||
APIType: resolvedInput.APIType,
|
APIType: resolvedInput.APIType,
|
||||||
AuthMode: resolvedInput.AuthMode,
|
AuthMode: resolvedInput.AuthMode,
|
||||||
VerifyModel: verifyModel,
|
VerifyModel: verifyModel,
|
||||||
Verified: true,
|
Verified: validateAvailability,
|
||||||
Remark: req.Remark,
|
ValidateAvailability: validateAvailability,
|
||||||
|
Remark: req.Remark,
|
||||||
|
}
|
||||||
|
if validateAvailability {
|
||||||
|
now := time.Now()
|
||||||
|
account.VerifiedAt = &now
|
||||||
}
|
}
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Create(account).Error; err != nil {
|
if err := tx.Create(account).Error; err != nil {
|
||||||
@@ -1088,8 +1097,6 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||||
agentAccountMutationMu.Lock()
|
|
||||||
defer agentAccountMutationMu.Unlock()
|
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1105,9 +1112,24 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
previousModels := append([]dto.AgentAccountModel(nil), models...)
|
||||||
|
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) == "" {
|
||||||
requestedVerifyModel = account.VerifyModel
|
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
|
||||||
|
requestedVerifyModel = account.VerifyModel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
verifyModel := ""
|
verifyModel := ""
|
||||||
if account.AppInstallID == 0 || len(models) > 0 {
|
if account.AppInstallID == 0 || len(models) > 0 {
|
||||||
@@ -1116,11 +1138,51 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := account.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
if req.ValidateAvailability != nil {
|
||||||
|
validateAvailability = *req.ValidateAvailability
|
||||||
|
}
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, false, account.AppInstallID != 0 && len(models) == 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
previousVerifyModel := account.VerifyModel
|
||||||
|
if previousVerifyModel == "" && len(previousModels) > 0 {
|
||||||
|
previousVerifyModel = previousModels[0].ID
|
||||||
|
}
|
||||||
|
connectionChanged := account.APIKey != resolvedInput.APIKey || account.BaseURL != resolvedInput.BaseURL ||
|
||||||
|
account.APIType != resolvedInput.APIType || account.AuthMode != resolvedInput.AuthMode || !sameProviderModelID(provider, previousVerifyModel, verifyModel)
|
||||||
|
shouldVerify := validateAvailability && (connectionChanged || !account.ValidateAvailability)
|
||||||
|
if shouldVerify {
|
||||||
|
if _, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, true, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := ensureAgentAccountUnchanged(account, previousModels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.Models != nil {
|
||||||
|
if err := ensureAccountModelsNotBound(account, models); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if account.ValidateAvailability && (!validateAvailability || connectionChanged) {
|
||||||
|
account.Verified = false
|
||||||
|
account.VerifiedAt = nil
|
||||||
|
account.VerifyError = ""
|
||||||
|
}
|
||||||
|
if shouldVerify {
|
||||||
|
now := time.Now()
|
||||||
|
account.Verified = true
|
||||||
|
account.VerifiedAt = &now
|
||||||
|
account.VerifyError = ""
|
||||||
|
}
|
||||||
|
account.ValidateAvailability = validateAvailability
|
||||||
account.Name = req.Name
|
account.Name = req.Name
|
||||||
account.APIKey = resolvedInput.APIKey
|
account.APIKey = resolvedInput.APIKey
|
||||||
account.RememberAPIKey = req.RememberAPIKey
|
account.RememberAPIKey = req.RememberAPIKey
|
||||||
@@ -1129,9 +1191,17 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.AuthMode = resolvedInput.AuthMode
|
account.AuthMode = resolvedInput.AuthMode
|
||||||
account.VerifyModel = verifyModel
|
account.VerifyModel = verifyModel
|
||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
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()
|
||||||
@@ -1189,22 +1259,25 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
appInstallID = item.AppInstallID
|
appInstallID = item.AppInstallID
|
||||||
}
|
}
|
||||||
items = append(items, dto.AgentAccountInfo{
|
items = append(items, dto.AgentAccountInfo{
|
||||||
AppInstallID: appInstallID,
|
AppInstallID: appInstallID,
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
ProviderName: localizedAgentProviderName(item.Provider),
|
ProviderName: localizedAgentProviderName(item.Provider),
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
RememberAPIKey: item.RememberAPIKey,
|
RememberAPIKey: item.RememberAPIKey,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
Models: nil,
|
Models: nil,
|
||||||
APIType: item.APIType,
|
APIType: item.APIType,
|
||||||
AuthMode: item.AuthMode,
|
AuthMode: item.AuthMode,
|
||||||
VerifyModel: item.VerifyModel,
|
VerifyModel: item.VerifyModel,
|
||||||
Verified: item.Verified,
|
Verified: item.Verified,
|
||||||
Remark: item.Remark,
|
ValidateAvailability: item.ValidateAvailability,
|
||||||
CreatedAt: item.CreatedAt,
|
VerifiedAt: item.VerifiedAt,
|
||||||
|
VerifyError: item.VerifyError,
|
||||||
|
Remark: item.Remark,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(list) > 0 {
|
if len(list) > 0 {
|
||||||
@@ -1240,7 +1313,51 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return loadAgentAccountModels(account)
|
models, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
verifyModel := account.VerifyModel
|
||||||
|
if verifyModel == "" && len(models) > 0 {
|
||||||
|
verifyModel = models[0].ID
|
||||||
|
}
|
||||||
|
for i := range models {
|
||||||
|
models[i].IsVerifyModel = sameProviderModelID(account.Provider, verifyModel, models[i].ID)
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) UpdateAccountVerifyModel(req dto.AgentAccountVerifyModelUpdateReq) error {
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
models, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
previousVerifyModel := account.VerifyModel
|
||||||
|
if previousVerifyModel == "" && len(models) > 0 {
|
||||||
|
previousVerifyModel = models[0].ID
|
||||||
|
}
|
||||||
|
if account.ValidateAvailability && !sameProviderModelID(account.Provider, previousVerifyModel, record.Model) {
|
||||||
|
account.Verified = false
|
||||||
|
account.VerifiedAt = nil
|
||||||
|
account.VerifyError = ""
|
||||||
|
}
|
||||||
|
account.VerifyModel = record.Model
|
||||||
|
if err := agentAccountRepo.UpdateVerification(account); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||||
@@ -1275,6 +1392,36 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := ensureAgentAccountUnchanged(account, models); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
sortOrder := len(models) + 1
|
sortOrder := len(models) + 1
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
@@ -1282,7 +1429,8 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
|
|||||||
Name: nextModel.Name,
|
Name: nextModel.Name,
|
||||||
SortOrder: sortOrder,
|
SortOrder: sortOrder,
|
||||||
}
|
}
|
||||||
if err := agentAccountModelRepo.Create(record); err != nil {
|
err = agentAccountModelRepo.Create(record)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return a.syncAgentsByAccount(account)
|
return a.syncAgentsByAccount(account)
|
||||||
@@ -1305,6 +1453,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 {
|
||||||
@@ -1325,17 +1482,34 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := ensureAgentAccountUnchanged(account, models); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
previousModelID := record.Model
|
previousModelID := record.Model
|
||||||
record.Model = nextModel.ID
|
record.Model = nextModel.ID
|
||||||
record.Name = nextModel.Name
|
record.Name = nextModel.Name
|
||||||
if err := agentAccountModelRepo.Save(record); err != nil {
|
verifyModel := account.VerifyModel
|
||||||
return err
|
if verifyModel == "" && len(models) > 0 {
|
||||||
|
verifyModel = models[0].ID
|
||||||
}
|
}
|
||||||
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
|
if sameProviderModelID(account.Provider, verifyModel, previousModelID) && !sameProviderModelID(account.Provider, previousModelID, nextModel.ID) {
|
||||||
account.VerifyModel = nextModel.ID
|
if account.ValidateAvailability {
|
||||||
if err := agentAccountRepo.Save(account); err != nil {
|
account.Verified = false
|
||||||
return err
|
account.VerifiedAt = nil
|
||||||
|
account.VerifyError = ""
|
||||||
}
|
}
|
||||||
|
account.VerifyModel = nextModel.ID
|
||||||
|
err = agentAccountModelRepo.SaveWithAccount(record, account)
|
||||||
|
} else {
|
||||||
|
err = agentAccountModelRepo.Save(record)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
terminalai.InvalidateFileAIRuntimeCache()
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
@@ -1343,6 +1517,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
|
||||||
@@ -1351,13 +1529,17 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
|
|
||||||
return buserr.New("ErrAgentVerifyModelInUse")
|
|
||||||
}
|
|
||||||
models, err := loadAgentAccountModels(account)
|
models, err := loadAgentAccountModels(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
verifyModel := account.VerifyModel
|
||||||
|
if verifyModel == "" && len(models) > 0 {
|
||||||
|
verifyModel = models[0].ID
|
||||||
|
}
|
||||||
|
if account.ValidateAvailability && sameProviderModelID(account.Provider, verifyModel, record.Model) {
|
||||||
|
return buserr.New("ErrAgentVerifyModelInUse")
|
||||||
|
}
|
||||||
nextModels := make([]dto.AgentAccountModel, 0, len(models))
|
nextModels := make([]dto.AgentAccountModel, 0, len(models))
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if item.RecordID == req.RecordID {
|
if item.RecordID == req.RecordID {
|
||||||
@@ -1368,7 +1550,51 @@ 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 err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil {
|
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 sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
|
||||||
|
account.VerifyModel = ""
|
||||||
|
err = agentAccountModelRepo.DeleteWithAccount(req.RecordID, account)
|
||||||
|
} else {
|
||||||
|
err = agentAccountModelRepo.DeleteByID(req.RecordID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := compactPersistedAgentAccountModelSortOrder(req.AccountID); err != nil {
|
if err := compactPersistedAgentAccountModelSortOrder(req.AccountID); err != nil {
|
||||||
@@ -1387,8 +1613,45 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
if req.ID == 0 {
|
||||||
return err
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !account.ValidateAvailability {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
models, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verifyModel, verifyErr := resolveAgentAccountVerifyModel(account.Provider, account.VerifyModel, models)
|
||||||
|
if verifyErr == nil {
|
||||||
|
_, verifyErr = resolveAgentAccountInput(account.Provider, account.APIType, account.AuthMode, account.APIKey, account.BaseURL, verifyModel, true, false)
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := ensureAgentAccountUnchanged(account, models); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
account.VerifyModel = verifyModel
|
||||||
|
account.VerifiedAt = &now
|
||||||
|
account.Verified = verifyErr == nil
|
||||||
|
account.VerifyError = ""
|
||||||
|
if verifyErr != nil {
|
||||||
|
account.VerifyError = verifyErr.Error()
|
||||||
|
}
|
||||||
|
err = agentAccountRepo.UpdateVerification(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
|
return verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
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"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool, apiKey *string) (*model.AgentAccount, error) {
|
||||||
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||||
return nil, fmt.Errorf("invalid local model installation")
|
return nil, fmt.Errorf("invalid local model installation")
|
||||||
}
|
}
|
||||||
@@ -30,7 +36,7 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||||
return nil, fmt.Errorf("model account belongs to another installation")
|
return nil, fmt.Errorf("model service belongs to another installation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -52,9 +58,13 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
|
|||||||
if install.App.Key == "ollama" {
|
if install.App.Key == "ollama" {
|
||||||
apiType, apiKey = "openai-responses", "ollama-local"
|
apiType, apiKey = "openai-responses", "ollama-local"
|
||||||
}
|
}
|
||||||
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, ValidateAvailability: true}
|
||||||
}
|
}
|
||||||
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||||
|
if apiKey != nil && account.APIKey != strings.TrimSpace(*apiKey) {
|
||||||
|
account.APIKey = strings.TrimSpace(*apiKey)
|
||||||
|
accountChanged = true
|
||||||
|
}
|
||||||
account.AppInstallID = install.ID
|
account.AppInstallID = install.ID
|
||||||
if updateBaseURL {
|
if updateBaseURL {
|
||||||
account.BaseURL = baseURL
|
account.BaseURL = baseURL
|
||||||
@@ -71,6 +81,11 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
|
|||||||
account.VerifyModel = ""
|
account.VerifyModel = ""
|
||||||
accountChanged = true
|
accountChanged = true
|
||||||
}
|
}
|
||||||
|
if accountChanged && account.ValidateAvailability {
|
||||||
|
account.Verified = false
|
||||||
|
account.VerifiedAt = nil
|
||||||
|
account.VerifyError = ""
|
||||||
|
}
|
||||||
var previous []model.AgentAccountModel
|
var previous []model.AgentAccountModel
|
||||||
if account.ID != 0 {
|
if account.ID != 0 {
|
||||||
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||||
@@ -88,7 +103,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 {
|
||||||
@@ -132,10 +147,85 @@ func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBase
|
|||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||||
}
|
}
|
||||||
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing, nil)
|
||||||
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 account.ValidateAvailability {
|
||||||
|
account.Verified = false
|
||||||
|
account.VerifiedAt = nil
|
||||||
|
account.VerifyError = ""
|
||||||
|
}
|
||||||
|
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 resetLocalModelAccountVerification(install model.AppInstall) error {
|
||||||
|
if install.App.Key != "ollama" && install.App.Key != "vllm" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
if err := agentAccountRepo.ResetVerificationByAppInstallID(install.ID); 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 == "" {
|
||||||
@@ -166,12 +256,12 @@ func CleanupLocalModelAccount(installID uint) {
|
|||||||
}
|
}
|
||||||
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("load local model accounts: %v", err)
|
global.LOG.Errorf("load local model services: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||||
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
global.LOG.Warnf("keep local model service %d after uninstall: %v", account.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +271,7 @@ func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, cont
|
|||||||
case "custom":
|
case "custom":
|
||||||
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||||
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||||
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
return "", fmt.Errorf("model service base URL must start with http:// or https://")
|
||||||
}
|
}
|
||||||
return baseURL, nil
|
return baseURL, nil
|
||||||
case "localhost":
|
case "localhost":
|
||||||
@@ -200,3 +290,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,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -63,6 +64,21 @@ func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureAgentAccountUnchanged(account *model.AgentAccount, models []dto.AgentAccountModel) error {
|
||||||
|
current, err := agentAccountRepo.GetFirst(repo.WithByID(account.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentModels, err := loadAgentAccountModels(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(account, current) || !slices.Equal(models, currentModels) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "model service changed during validation; reload and retry", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
|
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
|
||||||
var settings []model.Setting
|
var settings []model.Setting
|
||||||
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
|
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
|
||||||
@@ -113,11 +129,10 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
modelID = strings.TrimSpace(modelID)
|
modelID = strings.TrimSpace(modelID)
|
||||||
if modelID == "" && !allowEmptyModel {
|
if modelID == "" && (validateAvailability || !allowEmptyModel) {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
if validateAvailability {
|
||||||
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
|
|
||||||
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
@@ -1328,7 +1343,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 +1532,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
@@ -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 {
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ type IAppService interface {
|
|||||||
GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
|
GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type appInstallHooks struct {
|
type AppInstallHooks struct {
|
||||||
|
AfterCreate func(appInstall *model.AppInstall) error
|
||||||
AfterCopyData func(appInstall *model.AppInstall) error
|
AfterCopyData func(appInstall *model.AppInstall) error
|
||||||
PrepareSiteDir func(model.App, model.AppDetail, request.AppInstallCreate, *task.Task) error
|
PrepareSiteDir func(model.App, model.AppDetail, request.AppInstallCreate, *task.Task) error
|
||||||
}
|
}
|
||||||
@@ -367,10 +368,10 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
|
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
|
||||||
return a.installWithHooks(req, executeScript, nil)
|
return a.InstallWithHooks(req, executeScript, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript bool, hooks *appInstallHooks) (appInstall *model.AppInstall, err error) {
|
func (a AppService) InstallWithHooks(req request.AppInstallCreate, executeScript bool, hooks *AppInstallHooks) (appInstall *model.AppInstall, err error) {
|
||||||
if err = docker.CreateDefaultDockerNetwork(); err != nil {
|
if err = docker.CreateDefaultDockerNetwork(); err != nil {
|
||||||
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
|
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
|
||||||
return
|
return
|
||||||
@@ -589,6 +590,20 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hooks != nil && hooks.AfterCreate != nil {
|
||||||
|
if err = hooks.AfterCreate(appInstall); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
if _, err = SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true, nil); err != nil {
|
||||||
|
appInstall.Status = constant.StatusInstallErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
appInstall.Status = constant.StatusInstallErr
|
appInstall.Status = constant.StatusInstallErr
|
||||||
@@ -629,10 +644,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateToolApp(appInstall)
|
updateToolApp(appInstall)
|
||||||
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
|
||||||
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +653,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(¤t, 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) {
|
||||||
|
|||||||
@@ -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,12 +287,12 @@ 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:
|
||||||
return rebuildApp(install)
|
return rebuildApp(install, req.TaskID, nil)
|
||||||
case constant.Start:
|
case constant.Start:
|
||||||
out, err := compose.Up(dockerComposePath)
|
out, err := compose.Up(dockerComposePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -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,19 +378,36 @@ 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.Operate == constant.Start || req.Operate == constant.Restart {
|
||||||
return err
|
if err := resetLocalModelAccountVerification(install); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
close(started)
|
if req.UseLifecycleScripts {
|
||||||
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
|
||||||
@@ -391,26 +428,22 @@ 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:
|
return
|
||||||
default:
|
|
||||||
ready <- taskErr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if onFailure != nil {
|
|
||||||
onFailure(taskErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
install.Status = constant.StatusUpErr
|
|
||||||
install.Message = taskErr.Error()
|
|
||||||
_ = appInstallRepo.Save(context.Background(), &install)
|
|
||||||
}
|
}
|
||||||
|
if onFailure != nil {
|
||||||
|
onFailure(taskErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
install.Status = constant.StatusUpErr
|
||||||
|
install.Message = taskErr.Error()
|
||||||
|
_ = 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 {
|
||||||
@@ -435,10 +468,27 @@ func (a *AppInstallService) UpdateSort(req request.AppInstallSort) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||||
|
return a.UpdateWithBeforeRestart(req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppInstallService) UpdateWithBeforeRestart(req request.AppInstalledUpdate, beforeRestart func(*model.AppInstall) error) error {
|
||||||
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallId))
|
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallId))
|
||||||
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 {
|
||||||
@@ -577,24 +627,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
failed.Message = operationErr.Error()
|
failed.Message = operationErr.Error()
|
||||||
_ = appInstallRepo.Save(context.Background(), &failed)
|
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||||
}
|
}
|
||||||
|
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
||||||
|
if err := syncOllamaModelAccount(installed, modelAccountBaseURL, true, true); err != nil {
|
||||||
|
restoreConfig(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if beforeRestart != nil {
|
||||||
|
if err := beforeRestart(&installed); err != nil {
|
||||||
|
restoreConfig(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
UseLifecycleScripts: true,
|
UseLifecycleScripts: true,
|
||||||
}, restoreConfig)
|
}, restoreConfig)
|
||||||
} else {
|
} else {
|
||||||
err = rebuildApp(installed)
|
err = rebuildApp(installed, req.TaskID, restoreConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
||||||
@@ -634,9 +692,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
||||||
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
||||||
}
|
}
|
||||||
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
|
||||||
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,7 +1117,17 @@ 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 {
|
||||||
return nil, err
|
var businessErr buserr.BusinessError
|
||||||
|
if !errors.As(err, &businessErr) || businessErr.Msg != "ErrAppVersionUnavailable" {
|
||||||
|
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 {
|
||||||
@@ -1079,41 +1144,56 @@ 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 {
|
||||||
operation := ""
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
switch appInstall.Status {
|
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, appInstall.ID)
|
||||||
case constant.StatusInstalling:
|
|
||||||
operation = task.TaskInstall
|
|
||||||
case constant.StatusUpgrading:
|
|
||||||
operation = task.TaskUpgrade
|
|
||||||
case constant.StatusRebuilding:
|
|
||||||
operation = task.TaskBuild
|
|
||||||
case constant.StatusUninstalling:
|
|
||||||
operation = task.TaskUninstall
|
|
||||||
case constant.StatusStarting, constant.StatusWaiting:
|
|
||||||
operation = task.TaskUpdate
|
|
||||||
case constant.StatusRestarting:
|
|
||||||
operation = task.TaskRestart
|
|
||||||
}
|
|
||||||
if operation != "" {
|
|
||||||
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch appInstall.Status {
|
|
||||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
|
||||||
constant.StatusStarting, constant.StatusWaiting:
|
|
||||||
return nil
|
|
||||||
case constant.StatusRestarting:
|
|
||||||
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if count > 0 {
|
appInstall.TaskID = operationTask.ID
|
||||||
|
if operationTask.ID != "" {
|
||||||
|
appInstall.Status = localEngineTaskStatus(operationTask.Operate, appInstall.Status)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
force = true
|
||||||
if !force {
|
} else {
|
||||||
|
operation := ""
|
||||||
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
operation = task.TaskInstall
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
operation = task.TaskUpgrade
|
||||||
|
case constant.StatusRebuilding:
|
||||||
|
operation = task.TaskBuild
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
operation = task.TaskUninstall
|
||||||
|
case constant.StatusStarting:
|
||||||
|
operation = "Run"
|
||||||
|
case constant.StatusWaiting:
|
||||||
|
operation = "Stop"
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
operation = task.TaskRestart
|
||||||
|
}
|
||||||
|
if operation != "" {
|
||||||
|
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
|
constant.StatusStarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||||
|
if !force {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a AppService) InstallForMCP(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error) {
|
func (a AppService) InstallForMCP(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error) {
|
||||||
return a.installWithHooks(req, executeScript, &appInstallHooks{PrepareSiteDir: prepareMCPInstallSiteDirectory})
|
return a.InstallWithHooks(req, executeScript, &AppInstallHooks{PrepareSiteDir: prepareMCPInstallSiteDirectory})
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareMCPInstallSiteDirectory(app model.App, detail model.AppDetail, req request.AppInstallCreate, _ *task.Task) error {
|
func prepareMCPInstallSiteDirectory(app model.App, detail model.AppDetail, req request.AppInstallCreate, _ *task.Task) error {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -136,8 +136,8 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeCandidate(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
||||||
return errors.New("vLLM can only upgrade within the same image type")
|
return errors.New("vLLM upgrade version is not supported")
|
||||||
}
|
}
|
||||||
if install.Version == detail.Version {
|
if install.Version == detail.Version {
|
||||||
return errors.New("two version is same")
|
return errors.New("two version is same")
|
||||||
@@ -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
|
||||||
@@ -271,6 +271,9 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
|
|||||||
}
|
}
|
||||||
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
|
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
|
||||||
envs[vllmImageEnvKey] = image
|
envs[vllmImageEnvKey] = image
|
||||||
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(u.detail.Version)), vllmGB10VersionPrefix) {
|
||||||
|
envs["DSPARK_VLLM_IMAGE"] = image
|
||||||
|
}
|
||||||
paramBytes, marshalErr := json.Marshal(envs)
|
paramBytes, marshalErr := json.Marshal(envs)
|
||||||
if marshalErr != nil {
|
if marshalErr != nil {
|
||||||
return marshalErr
|
return marshalErr
|
||||||
@@ -404,6 +407,9 @@ func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *appUpgradeContext) cutover(t *task.Task) error {
|
func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||||
|
if err := resetLocalModelAccountVerification(u.original); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
u.stopAttempted = true
|
u.stopAttempted = true
|
||||||
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
|
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
|
||||||
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
|
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1224,30 +1228,51 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecyc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rebuildApp(appInstall model.AppInstall) error {
|
func rebuildApp(appInstall model.AppInstall, taskID string, onFailure func(error)) 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, taskID, 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 {
|
||||||
|
if err := resetLocalModelAccountVerification(appInstall); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Log(task.GetTaskName(appInstall.Name, "Stop", task.TaskScopeApp))
|
||||||
|
out, err := compose.Down(appInstall.GetComposePath())
|
||||||
|
if strings.TrimSpace(out) != "" {
|
||||||
|
t.Log(out)
|
||||||
}
|
}
|
||||||
out, err = compose.Up(appInstall.GetComposePath())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handleErr(appInstall, err, out)
|
return handleErr(appInstall, err, out)
|
||||||
return
|
}
|
||||||
|
t.Log(task.GetTaskName(appInstall.Name, "Run", task.TaskScopeApp))
|
||||||
|
out, err = compose.Up(appInstall.GetComposePath())
|
||||||
|
if strings.TrimSpace(out) != "" {
|
||||||
|
t.Log(out)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return handleErr(appInstall, err, out)
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
if onFailure != nil {
|
||||||
|
onFailure(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = handleErr(appInstall, err, "")
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1477,7 +1502,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()
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
|
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||||
|
} else {
|
||||||
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notFoundNames := make([]string, 0)
|
notFoundNames := make([]string, 0)
|
||||||
@@ -1536,7 +1565,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
|||||||
appInstall.Message = msg
|
appInstall.Message = msg
|
||||||
appInstall.Status = constant.StatusUnHealthy
|
appInstall.Status = constant.StatusUnHealthy
|
||||||
}
|
}
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
|
||||||
|
_ = appInstallRepo.UpdateStatus(appInstall)
|
||||||
|
} else {
|
||||||
|
_ = 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) {
|
||||||
@@ -1544,22 +1577,43 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
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 {
|
||||||
defer cli.Close()
|
return nil, err
|
||||||
containers, err := cli.ListAllContainers()
|
}
|
||||||
if err == nil {
|
defer cli.Close()
|
||||||
containersMap = make(map[string]container.Summary, len(containers))
|
containers, err := cli.ListAllContainers()
|
||||||
for _, contain := range containers {
|
if err != nil {
|
||||||
containersMap[contain.Names[0]] = contain
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
containersMap = make(map[string]container.Summary, len(containers))
|
||||||
|
for _, contain := range containers {
|
||||||
|
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 +1622,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 +1636,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,
|
||||||
@@ -2041,7 +2096,7 @@ func getAppTags(appID uint, lang string) ([]response.TagDTO, error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareAppInstallSiteDirectory(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task, hooks *appInstallHooks) error {
|
func prepareAppInstallSiteDirectory(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task, hooks *AppInstallHooks) error {
|
||||||
if hooks != nil && hooks.PrepareSiteDir != nil {
|
if hooks != nil && hooks.PrepareSiteDir != nil {
|
||||||
return hooks.PrepareSiteDir(app, appDetail, req, t)
|
return hooks.PrepareSiteDir(app, appDetail, req, t)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -851,7 +874,7 @@ func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan
|
|||||||
return false
|
return false
|
||||||
case message, ok := <-messages:
|
case message, ok := <-messages:
|
||||||
if ok {
|
if ok {
|
||||||
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
|
_, err := fmt.Fprintf(w, "data: %s\n\n", base64.StdEncoding.EncodeToString([]byte(message)))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
// The collector closes both channels when it finishes. Drain all buffered
|
// The collector closes both channels when it finishes. Drain all buffered
|
||||||
@@ -929,8 +952,6 @@ func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<-
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reader := bufio.NewReader(stdout)
|
|
||||||
|
|
||||||
processKilled := false
|
processKilled := false
|
||||||
go func() {
|
go func() {
|
||||||
<-done
|
<-done
|
||||||
@@ -943,29 +964,23 @@ func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<-
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
buffer := make([]byte, 32*1024)
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
n, err := stdout.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
select {
|
||||||
|
case messageChan <- string(buffer[:n]):
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
if len(line) > 0 {
|
|
||||||
line = strings.TrimSuffix(line, "\n")
|
|
||||||
select {
|
|
||||||
case messageChan <- line:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
errorChan <- fmt.Errorf("reader error: %v", err)
|
errorChan <- fmt.Errorf("reader error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
line = strings.TrimSuffix(line, "\n")
|
|
||||||
select {
|
|
||||||
case messageChan <- line:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ = dockerCmd.Wait()
|
_ = dockerCmd.Wait()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
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
|
||||||
|
sameImage := inspected.ID == remote.target.ConfigDigest
|
||||||
|
if inspected.Descriptor != nil {
|
||||||
|
// The containerd store uses the pulled index/manifest as its image ID,
|
||||||
|
// not the config digest. Persist the ID that pulling this tag will produce
|
||||||
|
// so post-upgrade reconciliation uses the same identity as Docker.
|
||||||
|
if remote.target.ReferenceDigest == "" {
|
||||||
|
return record, fmt.Errorf("Docker Hub manifest response is missing its digest")
|
||||||
|
}
|
||||||
|
record.TargetImageID = remote.target.ReferenceDigest
|
||||||
|
sameImage = inspected.ID == remote.target.ReferenceDigest || inspected.ID == remote.target.Digest
|
||||||
|
}
|
||||||
|
// Both versioned and floating tags can be republished with new image content.
|
||||||
|
record.HasUpdate = remote.target.Tag != tag || !sameImage
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,6 +3,7 @@ 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()
|
||||||
appRepo = repo.NewIAppRepo()
|
appRepo = repo.NewIAppRepo()
|
||||||
appTagRepo = repo.NewIAppTagRepo()
|
appTagRepo = repo.NewIAppTagRepo()
|
||||||
appDetailRepo = repo.NewIAppDetailRepo()
|
appDetailRepo = repo.NewIAppDetailRepo()
|
||||||
@@ -14,7 +15,6 @@ var (
|
|||||||
|
|
||||||
aiRepo = repo.NewIAiRepo()
|
aiRepo = repo.NewIAiRepo()
|
||||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||||
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
|
|
||||||
agentRepo = repo.NewIAgentRepo()
|
agentRepo = repo.NewIAgentRepo()
|
||||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||||
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -140,14 +145,22 @@ func mcpWebsiteHasWWWMount(mounts []container.MountPoint, root string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mcpWebsiteOwner struct {
|
type mcpWebsiteOwner struct {
|
||||||
BindingID string `json:"bindingId"`
|
BindingID string `json:"bindingId"`
|
||||||
WebsiteID uint `json:"websiteId"`
|
WebsiteID uint `json:"websiteId"`
|
||||||
KeyHash string `json:"keyHash"`
|
KeyHash string `json:"keyHash"`
|
||||||
BindHash string `json:"bindHash"`
|
BindHash string `json:"bindHash"`
|
||||||
ProxyHash string `json:"proxyHash"`
|
ProxyHash string `json:"proxyHash"`
|
||||||
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,30 +477,248 @@ 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 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if port == 0 {
|
explicitPort := port != 0
|
||||||
|
if !explicitPort || req.EnableSSL {
|
||||||
installed, installErr := s.openresty()
|
installed, installErr := s.openresty()
|
||||||
if installErr != nil {
|
if installErr != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
port = installed.HttpPort
|
if !explicitPort {
|
||||||
if req.EnableSSL {
|
port = installed.HttpPort
|
||||||
port = installed.HttpsPort
|
}
|
||||||
|
if req.EnableSSL && port == installed.HttpPort {
|
||||||
|
for _, domain := range site.Domains {
|
||||||
|
if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL {
|
||||||
|
return true // Complete an interrupted bind against its existing HTTP base.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 == (req.EnableSSL && explicitPort) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func mcpWebsiteListenConflict(candidate model.Website, installed model.AppInstall) error {
|
||||||
|
if err := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sites, err := websiteRepo.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ports := websiteListenPorts(candidate, installed.HttpPort, installed.HttpsPort)
|
||||||
|
isTLS := func(site model.Website, port int) bool {
|
||||||
|
return site.Protocol == constant.ProtocolHTTPS && slices.Contains(buildWebsiteTLSPlan(site.Domains, installed.HttpPort, installed.HttpsPort).httpsPorts, port)
|
||||||
|
}
|
||||||
|
for _, site := range sites {
|
||||||
|
if site.ID == candidate.ID || site.Type == constant.Stream {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for port := range websiteListenPorts(site, installed.HttpPort, installed.HttpsPort) {
|
||||||
|
if _, exists := ports[port]; exists && isTLS(candidate, port) != isTLS(site, port) {
|
||||||
|
return buserr.WithName("ErrDomainIsUsed", site.PrimaryDomain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mcpWebsiteTLSUsesHTTPPort(site model.Website, installed model.AppInstall) bool {
|
||||||
|
for _, domain := range site.Domains {
|
||||||
|
if domain.SSL && domain.Port == installed.HttpPort {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,6 +851,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 +876,17 @@ 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.Operation == "status" && req.WebsiteID == 0 {
|
||||||
|
partial := model.Website{Alias: mcpWebsiteAlias(req.BindingID)}
|
||||||
|
for _, location := range []string{GetSitePath(partial, SiteDir), GetSitePath(partial, SiteConf), mcpWebsitePrivatePath(partial, mcpWebsiteOwnerName), mcpWebsitePrivatePath(partial, mcpWebsiteProxyName)} {
|
||||||
|
if _, err := os.Lstat(location); !os.IsNotExist(err) {
|
||||||
|
return response.MCPWebsiteBinding{State: "unavailable", BindingID: req.BindingID, RequestID: req.RequestID, Reason: "website_missing"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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}
|
||||||
}
|
}
|
||||||
@@ -647,6 +895,12 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
|||||||
if req.Operation != "bind" {
|
if req.Operation != "bind" {
|
||||||
return mcpWebsiteFailure("website_missing", true, true)
|
return mcpWebsiteFailure("website_missing", true, true)
|
||||||
}
|
}
|
||||||
|
if req.WebsiteID != 0 {
|
||||||
|
return mcpWebsiteFailure("website_missing", true, true)
|
||||||
|
}
|
||||||
|
if _, ownerErr := os.Lstat(mcpWebsitePrivatePath(model.Website{Alias: mcpWebsiteAlias(req.BindingID)}, mcpWebsiteOwnerName)); !os.IsNotExist(ownerErr) {
|
||||||
|
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||||
|
}
|
||||||
installed, installErr := s.openresty()
|
installed, installErr := s.openresty()
|
||||||
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||||
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||||
@@ -656,21 +910,35 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
|||||||
return mcpWebsiteFailure("website_group_unavailable", true, false)
|
return mcpWebsiteFailure("website_group_unavailable", true, false)
|
||||||
}
|
}
|
||||||
domain, port, _ := mcpWebsiteDomain(req.Domain)
|
domain, port, _ := mcpWebsiteDomain(req.Domain)
|
||||||
|
explicitPort := port != 0
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = installed.HttpPort
|
port = installed.HttpPort
|
||||||
if req.EnableSSL {
|
}
|
||||||
port = installed.HttpsPort
|
domainSSL := req.EnableSSL && explicitPort
|
||||||
|
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port, SSL: domainSSL}}}
|
||||||
|
if req.EnableSSL {
|
||||||
|
candidate.Protocol = constant.ProtocolHTTPS
|
||||||
|
if mcpWebsiteTLSUsesHTTPPort(candidate, installed) {
|
||||||
|
return mcpWebsiteFailure("invalid_https_port", true, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if existing, conflictErr := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain), websiteDomainRepo.WithPort(port)); conflictErr == nil && existing.ID > 0 {
|
if conflictErr := mcpWebsiteListenConflict(candidate, installed); conflictErr != nil {
|
||||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
var business buserr.BusinessError
|
||||||
} else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) {
|
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||||
|
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||||
|
}
|
||||||
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, SSL: domainSSL}}, 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 +969,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,21 +982,71 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Operation == "bind" && owner.BindHash != mcpWebsiteBindHash(req) {
|
if req.Operation == "bind" && owner.BindHash != mcpWebsiteBindHash(req) {
|
||||||
return mcpWebsiteFailure("binding_request_changed", true, true)
|
return mcpWebsiteFailure("binding_request_changed", true, true)
|
||||||
}
|
}
|
||||||
|
if req.Operation == "update" && (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL {
|
||||||
|
installed, installErr := s.openresty()
|
||||||
|
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||||
|
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||||
|
}
|
||||||
|
candidate := site
|
||||||
|
candidate.Protocol = constant.ProtocolHTTP
|
||||||
|
if req.EnableSSL {
|
||||||
|
candidate.Protocol = constant.ProtocolHTTPS
|
||||||
|
if mcpWebsiteTLSUsesHTTPPort(candidate, installed) {
|
||||||
|
return mcpWebsiteFailure("invalid_https_port", true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conflictErr := mcpWebsiteListenConflict(candidate, installed); conflictErr != nil {
|
||||||
|
var business buserr.BusinessError
|
||||||
|
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||||
|
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||||
|
}
|
||||||
|
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
if owner.ProxyHash != "" && owner.Operation != "unbind" && req.Operation != "unbind" && !mcpWebsiteProxyIncluded(site) {
|
if owner.ProxyHash != "" && owner.Operation != "unbind" && req.Operation != "unbind" && !mcpWebsiteProxyIncluded(site) {
|
||||||
return mcpWebsiteFailure("proxy_not_loaded", true, true)
|
return mcpWebsiteFailure("proxy_not_loaded", true, true)
|
||||||
}
|
}
|
||||||
@@ -774,11 +1095,25 @@ 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" {
|
||||||
|
|||||||
@@ -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,76 +18,72 @@ 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"} {
|
}
|
||||||
entries, err := os.ReadDir(directory)
|
snapshot, err := client.Collect(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
result.Warnings = append(result.Warnings, err.Error())
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
device := dto.ModelGPUDevice{Available: true}
|
||||||
var device dto.ModelGPUDevice
|
devicePath := ""
|
||||||
var devicePath string
|
switch {
|
||||||
switch directory {
|
case monitored.GPU != nil:
|
||||||
case "/proc/driver/nvidia/gpus":
|
if monitored.Vendor == "nvidia" && !re.GetRegex(re.NvidiaGPUUUIDPattern).MatchString(monitored.GPU.UUID) {
|
||||||
informationPath := filepath.Join(directory, entry.Name(), "information")
|
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID from nvidia-smi: %s", monitored.GPU.UUID))
|
||||||
content, err := os.ReadFile(informationPath)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Warnings = append(result.Warnings, err.Error())
|
result.Warnings = append(result.Warnings, fmt.Sprintf("resolve Intel GPU %d render device: %v", monitored.Index, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fields := map[string]string{}
|
devicePath = ""
|
||||||
for _, line := range strings.Split(string(content), "\n") {
|
for _, entry := range entries {
|
||||||
key, value, found := strings.Cut(line, ":")
|
if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
|
||||||
if found {
|
devicePath = filepath.Join("/dev/dri", entry.Name())
|
||||||
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uuid := fields["GPU UUID"]
|
if devicePath == "" {
|
||||||
minor, err := strconv.Atoi(fields["Device Minor"])
|
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
|
||||||
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
|
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())
|
|
||||||
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"}
|
|
||||||
case "/dev":
|
|
||||||
if !ascendPattern.MatchString(entry.Name()) {
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -286,6 +286,11 @@ func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous
|
|||||||
return point, err
|
return point, err
|
||||||
}
|
}
|
||||||
point.CreatedAt = time.Now()
|
point.CreatedAt = time.Now()
|
||||||
|
if len(metrics) == 0 {
|
||||||
|
point.Status = "no_data"
|
||||||
|
point.RawMetrics = "{}"
|
||||||
|
return point, nil
|
||||||
|
}
|
||||||
raw, err := json.Marshal(metrics)
|
raw, err := json.Marshal(metrics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return point, err
|
return point, err
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ var (
|
|||||||
// nginxHTTPBlockStartRe locates the http block opening, the fallback
|
// nginxHTTPBlockStartRe locates the http block opening, the fallback
|
||||||
// insertion point, and captures its indentation.
|
// insertion point, and captures its indentation.
|
||||||
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
|
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
|
||||||
|
|
||||||
|
// errNginxHTTPIncludeNoInsertionPoint tells a config the panel cannot edit
|
||||||
|
// safely apart from an I/O failure while editing it.
|
||||||
|
errNginxHTTPIncludeNoInsertionPoint = errors.New("no insertion point for the http.d include in nginx.conf")
|
||||||
)
|
)
|
||||||
|
|
||||||
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
|
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
|
||||||
@@ -96,7 +100,19 @@ func insertNginxHTTPInclude(content string) (string, error) {
|
|||||||
indent := content[m[2]:m[3]] + " "
|
indent := content[m[2]:m[3]] + " "
|
||||||
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
|
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
|
||||||
}
|
}
|
||||||
return "", errors.New("no insertion point for the http.d include in nginx.conf")
|
return "", errNginxHTTPIncludeNoInsertionPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxHTTPIncludeInsertable reports whether nginx.conf already loads http.d
|
||||||
|
// or the include can be added automatically. Nothing is written: the
|
||||||
|
// insertion is computed in memory and discarded.
|
||||||
|
func nginxHTTPIncludeInsertable(install model.AppInstall) bool {
|
||||||
|
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err = insertNginxHTTPInclude(string(content))
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
|
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -83,9 +84,15 @@ func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
|
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
|
||||||
if _, err := insertNginxHTTPInclude("events {}\n"); err == nil {
|
_, err := insertNginxHTTPInclude("events {}\n")
|
||||||
|
if err == nil {
|
||||||
t.Fatal("a config without an http block must be rejected so callers can degrade")
|
t.Fatal("a config without an http block must be rejected so callers can degrade")
|
||||||
}
|
}
|
||||||
|
// The save path maps this error to an actionable message and must be able
|
||||||
|
// to tell it apart from an I/O failure.
|
||||||
|
if !errors.Is(err, errNginxHTTPIncludeNoInsertionPoint) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
|
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
|
||||||
|
|||||||
@@ -224,12 +224,9 @@ func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
|||||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||||
}
|
}
|
||||||
res := &response.NginxBrotliRes{
|
res := &response.NginxBrotliRes{ManagedExternally: managedExternally}
|
||||||
ManagedExternally: managedExternally,
|
if !managedExternally {
|
||||||
// Without the include, values the panel would write would never reach
|
res.ManagedPending, res.ManagedUnavailable = nginxBrotliManagedState(install)
|
||||||
// nginx, so they are reported as unavailable rather than shown as if
|
|
||||||
// they were in effect.
|
|
||||||
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
|
|
||||||
}
|
}
|
||||||
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
|
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
|
||||||
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
|
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
|
||||||
@@ -237,6 +234,28 @@ func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nginxBrotliManagedState reports whether the managed brotli file is missing
|
||||||
|
// from what nginx loads, and if so whether saving from the settings page can
|
||||||
|
// fix that. Either way the reported values are not in effect, so they must
|
||||||
|
// not be presented as if they were.
|
||||||
|
//
|
||||||
|
// A missing include is the common case for installs that enabled brotli
|
||||||
|
// before the panel managed its directives, and the save path inserts it. Only
|
||||||
|
// a config without a safe insertion point is reported as unavailable.
|
||||||
|
func nginxBrotliManagedState(install model.AppInstall) (pending, unavailable bool) {
|
||||||
|
if !nginxHTTPIncludePresent(install) {
|
||||||
|
if nginxHTTPIncludeInsertable(install) {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
|
if _, err := os.Stat(path.Join(nginxHTTPConfigDir(install), fileName)); os.IsNotExist(err) {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
|
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
|
||||||
// in any of the files nginx loads them from.
|
// in any of the files nginx loads them from.
|
||||||
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
|
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
|
||||||
@@ -313,26 +332,19 @@ func updateNginxBrotliParams(params []dto.NginxParam) error {
|
|||||||
return updateUserNginxBrotliParams(install, values)
|
return updateUserNginxBrotliParams(install, values)
|
||||||
}
|
}
|
||||||
// A managed write needs the include. Installations missing it are
|
// A managed write needs the include. Installations missing it are
|
||||||
// upgraded in place here; when nginx.conf cannot be edited safely the
|
// upgraded in place here, through the same path that enabling the
|
||||||
// write is refused with an actionable error instead of writing values
|
// module takes; when nginx.conf cannot be edited safely the write is
|
||||||
// nginx would never load.
|
// refused with an actionable error instead of writing values nginx
|
||||||
if !nginxHTTPIncludePresent(install) {
|
// would never load.
|
||||||
configPath := nginxMainConfigPath(install)
|
_, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
|
||||||
content, readErr := os.ReadFile(configPath)
|
if includeErr != nil {
|
||||||
if readErr != nil {
|
if errors.Is(includeErr, errNginxHTTPIncludeNoInsertionPoint) {
|
||||||
return readErr
|
|
||||||
}
|
|
||||||
updated, insErr := insertNginxHTTPInclude(string(content))
|
|
||||||
if insErr != nil {
|
|
||||||
return buserr.New("ErrBrotliUnsupported")
|
return buserr.New("ErrBrotliUnsupported")
|
||||||
}
|
}
|
||||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
return includeErr
|
||||||
return err
|
}
|
||||||
}
|
if confSnapshot != nil {
|
||||||
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
if err = nginxCheckAndReload(string(confSnapshot), nginxMainConfigPath(install), install.ContainerName); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,3 +229,85 @@ func TestValidateNginxBrotliValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newBrotliStateInstall lays out an OpenResty install whose nginx.conf holds
|
||||||
|
// conf, or that has no nginx.conf at all when conf is empty.
|
||||||
|
func newBrotliStateInstall(t *testing.T, conf string) model.AppInstall {
|
||||||
|
t.Helper()
|
||||||
|
installRoot := t.TempDir()
|
||||||
|
confDir := path.Join(installRoot, "openresty", "openresty", "conf")
|
||||||
|
if err := os.MkdirAll(confDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conf != "" {
|
||||||
|
if err := os.WriteFile(path.Join(confDir, "nginx.conf"), []byte(conf), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous := global.Dir.AppInstallDir
|
||||||
|
global.Dir.AppInstallDir = installRoot
|
||||||
|
t.Cleanup(func() { global.Dir.AppInstallDir = previous })
|
||||||
|
install := model.AppInstall{Name: "openresty"}
|
||||||
|
install.App.Key = constant.AppOpenresty
|
||||||
|
return install
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installs that enabled brotli before the panel managed its directives have
|
||||||
|
// no http.d include. Saving inserts it, so the settings page must report the
|
||||||
|
// values as pending rather than as something the panel can never apply.
|
||||||
|
func TestNginxBrotliManagedState(t *testing.T) {
|
||||||
|
withInclude, err := insertNginxHTTPInclude(plainNginxConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
managedFile := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
conf string
|
||||||
|
managedFile bool
|
||||||
|
wantPending bool
|
||||||
|
wantUnavailable bool
|
||||||
|
}{
|
||||||
|
{"include missing but insertable", plainNginxConf, false, true, false},
|
||||||
|
{"no insertion point", "events {}\n", false, false, true},
|
||||||
|
{"nginx.conf unreadable", "", false, false, true},
|
||||||
|
{"include present, managed file missing", withInclude, false, true, false},
|
||||||
|
{"include present and managed file written", withInclude, true, false, false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
install := newBrotliStateInstall(t, tc.conf)
|
||||||
|
if tc.managedFile {
|
||||||
|
configDir := nginxHTTPConfigDir(install)
|
||||||
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path.Join(configDir, managedFile), []byte("brotli on;\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending, unavailable := nginxBrotliManagedState(install)
|
||||||
|
if pending != tc.wantPending || unavailable != tc.wantUnavailable {
|
||||||
|
t.Fatalf("expected pending=%v unavailable=%v, got pending=%v unavailable=%v",
|
||||||
|
tc.wantPending, tc.wantUnavailable, pending, unavailable)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The state is computed on every visit to the settings page; only an explicit
|
||||||
|
// save may change the configuration.
|
||||||
|
func TestNginxBrotliManagedStateDoesNotWrite(t *testing.T) {
|
||||||
|
install := newBrotliStateInstall(t, plainNginxConf)
|
||||||
|
nginxBrotliManagedState(install)
|
||||||
|
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(content) != plainNginxConf {
|
||||||
|
t.Fatal("checking the state must not edit nginx.conf")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(nginxHTTPConfigDir(install)); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("checking the state must not create the http.d directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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"
|
||||||
"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/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var phpExtensionsMu sync.Mutex
|
||||||
|
|
||||||
type PHPExtensionsService struct {
|
type PHPExtensionsService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +26,9 @@ type IPHPExtensionsService interface {
|
|||||||
Create(req request.PHPExtensionsCreate) error
|
Create(req request.PHPExtensionsCreate) error
|
||||||
Update(req request.PHPExtensionsUpdate) error
|
Update(req request.PHPExtensionsUpdate) error
|
||||||
Delete(req request.PHPExtensionsDelete) error
|
Delete(req request.PHPExtensionsDelete) error
|
||||||
|
Get(id uint) (model.PHPExtensions, error)
|
||||||
|
Import(req request.PHPExtensionsCreate) (string, error)
|
||||||
|
Push(req dto.PHPExtensionsPush) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIPHPExtensionsService() IPHPExtensionsService {
|
func NewIPHPExtensionsService() IPHPExtensionsService {
|
||||||
@@ -30,7 +42,7 @@ func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []re
|
|||||||
err error
|
err error
|
||||||
result []response.PHPExtensionsDTO
|
result []response.PHPExtensionsDTO
|
||||||
)
|
)
|
||||||
total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize)
|
total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,8 @@ func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
|
func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
|
||||||
|
phpExtensionsMu.Lock()
|
||||||
|
defer phpExtensionsMu.Unlock()
|
||||||
exist, _ := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
|
exist, _ := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
|
||||||
if exist.ID > 0 {
|
if exist.ID > 0 {
|
||||||
return buserr.New("ErrNameIsExist")
|
return buserr.New("ErrNameIsExist")
|
||||||
@@ -73,6 +87,8 @@ func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
|
func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
|
||||||
|
phpExtensionsMu.Lock()
|
||||||
|
defer phpExtensionsMu.Unlock()
|
||||||
exist, err := phpExtensionsRepo.GetFirst(repo.WithByID(req.ID))
|
exist, err := phpExtensionsRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -82,5 +98,41 @@ func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error {
|
func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error {
|
||||||
|
phpExtensionsMu.Lock()
|
||||||
|
defer phpExtensionsMu.Unlock()
|
||||||
return phpExtensionsRepo.DeleteBy(repo.WithByID(req.ID))
|
return phpExtensionsRepo.DeleteBy(repo.WithByID(req.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PHPExtensionsService) Get(id uint) (model.PHPExtensions, error) {
|
||||||
|
return phpExtensionsRepo.GetFirst(repo.WithByID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PHPExtensionsService) Import(req request.PHPExtensionsCreate) (string, error) {
|
||||||
|
phpExtensionsMu.Lock()
|
||||||
|
defer phpExtensionsMu.Unlock()
|
||||||
|
existing, err := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
|
||||||
|
if err == nil {
|
||||||
|
if existing.Extensions == req.Extensions {
|
||||||
|
return "skipped", nil
|
||||||
|
}
|
||||||
|
return "", buserr.New("ErrPHPExtensionsConflict")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
item := model.PHPExtensions{Name: req.Name, Extensions: req.Extensions}
|
||||||
|
if err := phpExtensionsRepo.Create(&item); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "created", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PHPExtensionsService) Push(req dto.PHPExtensionsPush) error {
|
||||||
|
if !global.IsMaster {
|
||||||
|
return errors.New("PHP extension template sync must be submitted to the master node")
|
||||||
|
}
|
||||||
|
if !xpack.MultiNodeProvider.IsXpack() {
|
||||||
|
return buserr.New("ErrXpackNotFound")
|
||||||
|
}
|
||||||
|
return xpack.MultiNodeProvider.PushPHPExtensionsToNode(req)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,445 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/subosito/gotenv"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"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/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/env"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TensorRTLLMService struct{}
|
|
||||||
|
|
||||||
type ITensorRTLLMService interface {
|
|
||||||
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes
|
|
||||||
Create(create request.TensorRTLLMCreate) error
|
|
||||||
Update(req request.TensorRTLLMUpdate) error
|
|
||||||
Delete(id uint) error
|
|
||||||
Operate(req request.TensorRTLLMOperate) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewITensorRTLLMService() ITensorRTLLMService {
|
|
||||||
return &TensorRTLLMService{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes {
|
|
||||||
var (
|
|
||||||
res response.TensorRTLLMsRes
|
|
||||||
items []response.TensorRTLLMDTO
|
|
||||||
)
|
|
||||||
|
|
||||||
total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
|
|
||||||
for _, item := range data {
|
|
||||||
_ = syncTensorRTLLMContainerStatus(&item)
|
|
||||||
serverDTO := response.TensorRTLLMDTO{
|
|
||||||
TensorRTLLM: item,
|
|
||||||
}
|
|
||||||
envs, _ := gotenv.Unmarshal(item.Env)
|
|
||||||
serverDTO.Version = envs["VERSION"]
|
|
||||||
serverDTO.ModelDir = envs["MODEL_PATH"]
|
|
||||||
serverDTO.Dir = path.Join(global.Dir.TensorRTLLMDir, item.Name)
|
|
||||||
serverDTO.Image = envs["IMAGE"]
|
|
||||||
serverDTO.Command = getCommand(item.Env)
|
|
||||||
var project map[string]interface{}
|
|
||||||
if yaml.Unmarshal([]byte(item.DockerCompose), &project) == nil {
|
|
||||||
services, _ := project["services"].(map[string]interface{})
|
|
||||||
if service, ok := services[item.Name].(map[string]interface{}); ok {
|
|
||||||
params := make(map[string]interface{}, len(envs))
|
|
||||||
for key, value := range envs {
|
|
||||||
params[key] = value
|
|
||||||
}
|
|
||||||
serverDTO.GPU = ReadModelGPU(service, params, "nvidia")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "0.0.0.0", false)
|
|
||||||
|
|
||||||
composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml"))
|
|
||||||
if err == nil {
|
|
||||||
serverDTO.Environments, _ = getDockerComposeEnvironments(composeByte)
|
|
||||||
environments := make([]request.Environment, 0, len(serverDTO.Environments))
|
|
||||||
for _, item := range serverDTO.Environments {
|
|
||||||
if item.Key == "MODEL_PATH" || (serverDTO.GPU != nil && (item.Key == "NVIDIA_VISIBLE_DEVICES" || item.Key == "NVIDIA_DRIVER_CAPABILITIES")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
environments = append(environments, item)
|
|
||||||
}
|
|
||||||
serverDTO.Environments = environments
|
|
||||||
}
|
|
||||||
volumes, err := getDockerComposeVolumes(composeByte)
|
|
||||||
if err == nil {
|
|
||||||
var defaultVolumes = map[string]string{
|
|
||||||
"${MODEL_PATH}": "${MODEL_PATH}",
|
|
||||||
}
|
|
||||||
for _, volume := range volumes {
|
|
||||||
exist := false
|
|
||||||
for key, value := range defaultVolumes {
|
|
||||||
if key == volume.Source && value == volume.Target {
|
|
||||||
exist = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exist {
|
|
||||||
serverDTO.Volumes = append(serverDTO.Volumes, volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = append(items, serverDTO)
|
|
||||||
}
|
|
||||||
res.Total = total
|
|
||||||
res.Items = items
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) error {
|
|
||||||
var composeContent []byte
|
|
||||||
if llm.ID == 0 {
|
|
||||||
composeContent = ai.DefaultTensorrtLLMCompose
|
|
||||||
} else {
|
|
||||||
composeContent = []byte(llm.DockerCompose)
|
|
||||||
}
|
|
||||||
composeMap := make(map[string]interface{})
|
|
||||||
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
services, serviceValid := composeMap["services"].(map[string]interface{})
|
|
||||||
if !serviceValid {
|
|
||||||
return buserr.New("ErrFileParse")
|
|
||||||
}
|
|
||||||
serviceName := ""
|
|
||||||
serviceValue := make(map[string]interface{})
|
|
||||||
|
|
||||||
if llm.ID > 0 {
|
|
||||||
serviceName = llm.Name
|
|
||||||
serviceValue = services[serviceName].(map[string]interface{})
|
|
||||||
} else {
|
|
||||||
for name, service := range services {
|
|
||||||
serviceName = name
|
|
||||||
serviceValue = service.(map[string]interface{})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
delete(services, serviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(serviceValue, "ports")
|
|
||||||
if len(create.ExposedPorts) > 0 {
|
|
||||||
var ports []interface{}
|
|
||||||
for i, port := range create.ExposedPorts {
|
|
||||||
containerPortStr, hostPortStr, hostIPStr, _ := composePortEnvKeys(i)
|
|
||||||
ports = append(ports, formatComposePortMapping(hostIPStr, hostPortStr, containerPortStr, port.Protocol))
|
|
||||||
}
|
|
||||||
serviceValue["ports"] = ports
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(serviceValue, "environment")
|
|
||||||
var environments []interface{}
|
|
||||||
environments = append(environments, fmt.Sprintf("MODEL_PATH=%s", create.ModelDir))
|
|
||||||
for _, e := range create.Environments {
|
|
||||||
if e.Key == "MODEL_PATH" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
environments = append(environments, fmt.Sprintf("%s=%s", e.Key, e.Value))
|
|
||||||
}
|
|
||||||
serviceValue["environment"] = environments
|
|
||||||
|
|
||||||
var volumes []interface{}
|
|
||||||
var defaultVolumes = map[string]string{
|
|
||||||
"${MODEL_PATH}": "${MODEL_PATH}",
|
|
||||||
}
|
|
||||||
for k, v := range defaultVolumes {
|
|
||||||
volumes = append(volumes, fmt.Sprintf("%s:%s", k, v))
|
|
||||||
}
|
|
||||||
for _, volume := range create.Volumes {
|
|
||||||
volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
|
|
||||||
}
|
|
||||||
serviceValue["volumes"] = volumes
|
|
||||||
gpuParams := map[string]interface{}{}
|
|
||||||
existingEnv, err := gotenv.Unmarshal(llm.Env)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for key, value := range existingEnv {
|
|
||||||
gpuParams[key] = value
|
|
||||||
}
|
|
||||||
gpuParams["MODEL_PATH"] = create.ModelDir
|
|
||||||
if err := ApplyModelGPU(serviceValue, create.GPU, gpuParams, "nvidia"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
services[llm.Name] = serviceValue
|
|
||||||
composeByte, err := yaml.Marshal(composeMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
llm.DockerCompose = string(composeByte)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gotenv.Env {
|
|
||||||
envMap := make(gotenv.Env)
|
|
||||||
envMap["CONTAINER_NAME"] = create.ContainerName
|
|
||||||
envMap["MODEL_PATH"] = create.ModelDir
|
|
||||||
envMap["VERSION"] = create.Version
|
|
||||||
envMap["IMAGE"] = create.Image
|
|
||||||
envMap["COMMAND"] = create.Command
|
|
||||||
for i, port := range create.ExposedPorts {
|
|
||||||
containerPortStr, hostPortStr, hostIPStr, protocolStr := composePortEnvKeys(i)
|
|
||||||
envMap[containerPortStr] = strconv.Itoa(port.ContainerPort)
|
|
||||||
envMap[hostPortStr] = strconv.Itoa(port.HostPort)
|
|
||||||
envMap[hostIPStr] = port.HostIP
|
|
||||||
envMap[protocolStr] = normalizeComposeProtocol(port.Protocol)
|
|
||||||
}
|
|
||||||
orders := []string{"MODEL_PATH", "COMMAND"}
|
|
||||||
envStr, _ := env.MarshalWithOrder(envMap, orders)
|
|
||||||
llm.Env = envStr
|
|
||||||
return envMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
|
|
||||||
servers, _ := tensorrtLLMRepo.List()
|
|
||||||
for _, server := range servers {
|
|
||||||
if server.ContainerName == create.ContainerName {
|
|
||||||
return buserr.New("ErrContainerName")
|
|
||||||
}
|
|
||||||
if server.Name == create.Name {
|
|
||||||
return buserr.New("ErrNameIsExist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, export := range create.ExposedPorts {
|
|
||||||
if err := checkPortExistWithProtocol(export.HostPort, export.Protocol); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := checkContainerName(create.ContainerName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tensorrtLLMDir := path.Join(global.Dir.TensorRTLLMDir, create.Name)
|
|
||||||
filesOp := files.NewFileOp()
|
|
||||||
if !filesOp.Stat(tensorrtLLMDir) {
|
|
||||||
_ = filesOp.CreateDir(tensorrtLLMDir, 0644)
|
|
||||||
}
|
|
||||||
if create.ModelSpeedup {
|
|
||||||
if err := handleModelArchive(create.ModelType, create.ModelDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tensorrtLLM := &model.TensorRTLLM{
|
|
||||||
Name: create.Name,
|
|
||||||
ContainerName: create.ContainerName,
|
|
||||||
Status: constant.StatusStarting,
|
|
||||||
ModelType: create.ModelType,
|
|
||||||
ModelSpeedup: create.ModelSpeedup,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := handleLLMParams(tensorrtLLM, create); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
envMap := handleLLMEnv(tensorrtLLM, create)
|
|
||||||
llmDir := path.Join(global.Dir.TensorRTLLMDir, create.Name)
|
|
||||||
envPath := path.Join(llmDir, ".env")
|
|
||||||
if err := env.WriteWithOrder(envMap, envPath, []string{"MODEL_PATH", "COMMAND"}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dockerComposePath := path.Join(llmDir, "docker-compose.yml")
|
|
||||||
if err := filesOp.SaveFile(dockerComposePath, tensorrtLLM.DockerCompose, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tensorrtLLM.Status = constant.StatusStarting
|
|
||||||
|
|
||||||
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go startTensorRTLLM(tensorrtLLM)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
|
|
||||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tensorrtLLM.ContainerName != req.ContainerName {
|
|
||||||
if err := checkContainerName(req.ContainerName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tensorrtLLM.ModelType = req.ModelType
|
|
||||||
tensorrtLLM.ModelSpeedup = req.ModelSpeedup
|
|
||||||
tensorrtLLM.ContainerName = req.ContainerName
|
|
||||||
|
|
||||||
if err := handleLLMParams(tensorrtLLM, req.TensorRTLLMCreate); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
envMap := handleLLMEnv(tensorrtLLM, req.TensorRTLLMCreate)
|
|
||||||
envStr, _ := gotenv.Marshal(envMap)
|
|
||||||
tensorrtLLM.Env = envStr
|
|
||||||
llmDir := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name)
|
|
||||||
envPath := path.Join(llmDir, ".env")
|
|
||||||
if err := env.WriteWithOrder(envMap, envPath, []string{"MODEL_PATH", "COMMAND"}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dockerComposePath := path.Join(llmDir, "docker-compose.yml")
|
|
||||||
if err := files.NewFileOp().SaveFile(dockerComposePath, tensorrtLLM.DockerCompose, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tensorrtLLM.Status = constant.StatusStarting
|
|
||||||
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go startTensorRTLLM(tensorrtLLM)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TensorRTLLMService) Delete(id uint) error {
|
|
||||||
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(id))
|
|
||||||
if err != nil {
|
|
||||||
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))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
|
||||||
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) {
|
|
||||||
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
|
|
||||||
if tensorrtLLM.Status != constant.StatusNormal {
|
|
||||||
_, _ = compose.Down(composePath)
|
|
||||||
}
|
|
||||||
if out, err := compose.Up(composePath); err != nil {
|
|
||||||
tensorrtLLM.Status = constant.StatusError
|
|
||||||
tensorrtLLM.Message = out
|
|
||||||
} else {
|
|
||||||
tensorrtLLM.Status = constant.StatusRunning
|
|
||||||
tensorrtLLM.Message = ""
|
|
||||||
}
|
|
||||||
_ = syncTensorRTLLMContainerStatus(tensorrtLLM)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
|
|
||||||
containerNames := []string{tensorrtLLM.ContainerName}
|
|
||||||
cli, err := docker.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
containers, err := cli.ListContainersByName(containerNames)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(containers) == 0 {
|
|
||||||
if tensorrtLLM.Status == constant.StatusStarting {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
|
||||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
|
||||||
}
|
|
||||||
container := containers[0]
|
|
||||||
switch container.State {
|
|
||||||
case "exited":
|
|
||||||
tensorrtLLM.Status = constant.StatusError
|
|
||||||
case "running":
|
|
||||||
tensorrtLLM.Status = constant.StatusRunning
|
|
||||||
case "paused":
|
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
|
||||||
case "restarting":
|
|
||||||
tensorrtLLM.Status = constant.StatusRestarting
|
|
||||||
default:
|
|
||||||
if tensorrtLLM.Status != constant.StatusStarting {
|
|
||||||
tensorrtLLM.Status = constant.StatusStopped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tensorrtLLMRepo.Save(tensorrtLLM)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findModelArchive(modelType string) (string, error) {
|
|
||||||
const baseDir = "/home/models"
|
|
||||||
prefix := fmt.Sprintf("FusionXplay_%s_Accelerator", modelType)
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(baseDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read %s: %w", baseDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := entry.Name()
|
|
||||||
if strings.HasPrefix(name, prefix) && strings.HasSuffix(name, ".tar.gz") {
|
|
||||||
return filepath.Join(baseDir, name), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no FusionXplay_%s_Accelerator*.tar.gz found in /home/models", modelType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleModelArchive(modelType string, modelDir string) error {
|
|
||||||
filePath, err := findModelArchive(modelType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileOp := files.NewFileOp()
|
|
||||||
if err = fileOp.TarGzExtractPro(filePath, modelDir, ""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = fileOp.ChmodR(path.Join(modelDir, "fusionxpark_accelerator"), 0755, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCommand(envStr string) string {
|
|
||||||
lines := strings.Split(envStr, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(line, "COMMAND=") {
|
|
||||||
return strings.TrimPrefix(line, "COMMAND=")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,9 @@ func resolveVllmVersionFamily(version, image string) string {
|
|||||||
func trimVllmVersionFamily(version string) string {
|
func trimVllmVersionFamily(version string) string {
|
||||||
trimmed := strings.TrimSpace(version)
|
trimmed := strings.TrimSpace(version)
|
||||||
normalized := strings.ToLower(trimmed)
|
normalized := strings.ToLower(trimmed)
|
||||||
|
if strings.HasPrefix(normalized, vllmGB10VersionPrefix) {
|
||||||
|
return strings.TrimSpace(trimmed[len(vllmGB10VersionPrefix):])
|
||||||
|
}
|
||||||
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
|
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
|
||||||
prefix := family + "-"
|
prefix := family + "-"
|
||||||
if strings.HasPrefix(normalized, prefix) {
|
if strings.HasPrefix(normalized, prefix) {
|
||||||
@@ -69,6 +72,9 @@ func trimVllmVersionFamily(version string) string {
|
|||||||
|
|
||||||
func buildDefaultVllmImageByVersion(version string) string {
|
func buildDefaultVllmImageByVersion(version string) string {
|
||||||
tag := trimVllmVersionFamily(version)
|
tag := trimVllmVersionFamily(version)
|
||||||
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix) {
|
||||||
|
return "1panel/vllm-gb10-dspark:" + tag
|
||||||
|
}
|
||||||
family := resolveVllmVersionFamily(version, "")
|
family := resolveVllmVersionFamily(version, "")
|
||||||
if family == vllmImageTypeIntel {
|
if family == vllmImageTypeIntel {
|
||||||
return "intel/llm-scaler-vllm:" + tag
|
return "intel/llm-scaler-vllm:" + tag
|
||||||
@@ -83,6 +89,11 @@ func buildDefaultVllmImageByVersion(version string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
|
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
|
||||||
|
currentGB10 := strings.HasPrefix(strings.ToLower(strings.TrimSpace(currentVersion)), vllmGB10VersionPrefix)
|
||||||
|
targetGB10 := strings.HasPrefix(strings.ToLower(strings.TrimSpace(targetVersion)), vllmGB10VersionPrefix)
|
||||||
|
if currentGB10 != targetGB10 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
|
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
|
||||||
targetFamily := resolveVllmVersionFamily(targetVersion, "")
|
targetFamily := resolveVllmVersionFamily(targetVersion, "")
|
||||||
return currentFamily == targetFamily
|
return currentFamily == targetFamily
|
||||||
@@ -102,7 +113,7 @@ func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string)
|
|||||||
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
|
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if common.CompareVersion(targetVersion, currentVersion) {
|
if common.CompareVersion(trimVllmVersionFamily(targetVersion), trimVllmVersionFamily(currentVersion)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return !hasVllmVersionFamilyPrefix(currentVersion) &&
|
return !hasVllmVersionFamilyPrefix(currentVersion) &&
|
||||||
|
|||||||
@@ -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, `/\\`)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -6,6 +6,3 @@ import (
|
|||||||
|
|
||||||
//go:embed compose.yml
|
//go:embed compose.yml
|
||||||
var DefaultMcpCompose []byte
|
var DefaultMcpCompose []byte
|
||||||
|
|
||||||
//go:embed llm-compose.yml
|
|
||||||
var DefaultTensorrtLLMCompose []byte
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
services:
|
|
||||||
tensorrt-llm:
|
|
||||||
image: ${IMAGE}:${VERSION}
|
|
||||||
container_name: ${CONTAINER_NAME}
|
|
||||||
restart: always
|
|
||||||
runtime: nvidia
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: all
|
|
||||||
capabilities: [gpu]
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
volumes:
|
|
||||||
- ${MODEL_PATH}:${MODEL_PATH}
|
|
||||||
ipc: host
|
|
||||||
ulimits:
|
|
||||||
memlock:
|
|
||||||
soft: -1
|
|
||||||
hard: -1
|
|
||||||
nofile:
|
|
||||||
soft: 65535
|
|
||||||
hard: 65535
|
|
||||||
stack: 67108864
|
|
||||||
command: ${COMMAND}
|
|
||||||
networks:
|
|
||||||
1panel-network:
|
|
||||||
external: true
|
|
||||||
@@ -3,7 +3,6 @@ base:
|
|||||||
mode: dev
|
mode: dev
|
||||||
is_demo: false
|
is_demo: false
|
||||||
is_offline: false
|
is_offline: false
|
||||||
is_fxplay: false
|
|
||||||
is_enterprise: false
|
is_enterprise: false
|
||||||
|
|
||||||
log:
|
log:
|
||||||
|
|||||||
+130
-159
@@ -8,6 +8,107 @@
|
|||||||
"formatZH": "更新 Ollama 绑定域名 [domain]",
|
"formatZH": "更新 Ollama 绑定域名 [domain]",
|
||||||
"formatEN": "update Ollama bind domain [domain]"
|
"formatEN": "update Ollama bind domain [domain]"
|
||||||
},
|
},
|
||||||
|
"/xpack/model/downloader/featured/sync/status": {
|
||||||
|
"bodyKeys": [
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"paramKeys": [],
|
||||||
|
"beforeFunctions": [],
|
||||||
|
"formatZH": "更新推荐模型自动同步 [value]",
|
||||||
|
"formatEN": "update featured model automatic synchronization [value]"
|
||||||
|
},
|
||||||
|
"/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]"
|
||||||
|
},
|
||||||
"/ai/ollama/close": {
|
"/ai/ollama/close": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
@@ -53,44 +154,6 @@
|
|||||||
"formatZH": "添加 Ollama 模型重试 [name]",
|
"formatZH": "添加 Ollama 模型重试 [name]",
|
||||||
"formatEN": "re-add Ollama model [name]"
|
"formatEN": "re-add Ollama model [name]"
|
||||||
},
|
},
|
||||||
"/ai/tensorrt/create": {
|
|
||||||
"bodyKeys": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
|
||||||
"beforeFunctions": [],
|
|
||||||
"formatZH": "创建 TensorRT LLM [name]",
|
|
||||||
"formatEN": "create TensorRT LLM [name]"
|
|
||||||
},
|
|
||||||
"/ai/tensorrt/delete": {
|
|
||||||
"bodyKeys": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
|
||||||
"beforeFunctions": [],
|
|
||||||
"formatZH": "删除 TensorRT LLM [id]",
|
|
||||||
"formatEN": "delete TensorRT LLM [id]"
|
|
||||||
},
|
|
||||||
"/ai/tensorrt/operate": {
|
|
||||||
"bodyKeys": [
|
|
||||||
"id",
|
|
||||||
"operate"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
|
||||||
"beforeFunctions": [],
|
|
||||||
"formatZH": "操作 TensorRT LLM [id][operate]",
|
|
||||||
"formatEN": "operate TensorRT LLM [id][operate]"
|
|
||||||
},
|
|
||||||
"/ai/tensorrt/update": {
|
|
||||||
"bodyKeys": [
|
|
||||||
"id",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
|
||||||
"beforeFunctions": [],
|
|
||||||
"formatZH": "更新 TensorRT LLM [id][name]",
|
|
||||||
"formatEN": "update TensorRT LLM [id][name]"
|
|
||||||
},
|
|
||||||
"/alert": {
|
"/alert": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"title"
|
"title"
|
||||||
@@ -103,27 +166,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 +5034,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 +5182,7 @@
|
|||||||
"formatZH": "更新 vLLM [name]",
|
"formatZH": "更新 vLLM [name]",
|
||||||
"formatEN": "update vLLM [name]"
|
"formatEN": "update vLLM [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms": {
|
"/vms": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
@@ -5220,7 +5191,7 @@
|
|||||||
"formatZH": "创建虚拟机 [name]",
|
"formatZH": "创建虚拟机 [name]",
|
||||||
"formatEN": "create VM [name]"
|
"formatEN": "create VM [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/del": {
|
"/vms/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@@ -5238,7 +5209,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 +5220,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 +5237,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 +5255,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 +5265,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 +5275,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 +5293,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 +5311,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 +5321,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 +5338,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 +5348,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 +5358,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 +5368,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 +5378,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 +5396,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 +5414,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 +5431,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 +5449,7 @@
|
|||||||
"formatZH": "删除虚拟机模板 [name]",
|
"formatZH": "删除虚拟机模板 [name]",
|
||||||
"formatEN": "delete VM template [name]"
|
"formatEN": "delete VM template [name]"
|
||||||
},
|
},
|
||||||
"/xpack/vms/update": {
|
"/vms/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
|
|||||||
+5
-1
@@ -77,7 +77,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
Port string `mapstructure:"port"`
|
Port string `mapstructure:"port"`
|
||||||
IsFxplay bool `mapstructure:"is_fxplay"`
|
|
||||||
IsEnterprise bool `mapstructure:"is_enterprise"`
|
IsEnterprise bool `mapstructure:"is_enterprise"`
|
||||||
Edition string `mapstructure:"edition"` // [ cn / intl]
|
Edition string `mapstructure:"edition"` // [ cn / intl]
|
||||||
Version string `mapstructure:"version"`
|
Version string `mapstructure:"version"`
|
||||||
@@ -43,7 +42,6 @@ type SystemDir struct {
|
|||||||
SSLLogDir string
|
SSLLogDir string
|
||||||
McpDir string
|
McpDir string
|
||||||
ConvertLogDir string
|
ConvertLogDir string
|
||||||
TensorRTLLMDir string
|
|
||||||
FirewallDir string
|
FirewallDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ func RepoURL() string {
|
|||||||
if CONF.Base.IsEnterprise {
|
if CONF.Base.IsEnterprise {
|
||||||
return "https://resource.fit2cloud.com/1panel/package/enterprise"
|
return "https://resource.fit2cloud.com/1panel/package/enterprise"
|
||||||
}
|
}
|
||||||
if CONF.Base.IsFxplay {
|
|
||||||
return "https://resource.fit2cloud.com/1panel/package/fusionxplay"
|
|
||||||
}
|
|
||||||
if CONF.Base.Edition != "intl" {
|
if CONF.Base.Edition != "intl" {
|
||||||
return "https://resource.fit2cloud.com/1panel/package/v2"
|
return "https://resource.fit2cloud.com/1panel/package/v2"
|
||||||
}
|
}
|
||||||
@@ -81,9 +78,6 @@ func ResourceURL() string {
|
|||||||
if CONF.Base.IsEnterprise {
|
if CONF.Base.IsEnterprise {
|
||||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
||||||
}
|
}
|
||||||
if CONF.Base.IsFxplay {
|
|
||||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
|
||||||
}
|
|
||||||
if CONF.Base.Edition != "intl" {
|
if CONF.Base.Edition != "intl" {
|
||||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
||||||
}
|
}
|
||||||
@@ -93,9 +87,6 @@ func AppRepoURL() string {
|
|||||||
if CONF.Base.IsEnterprise {
|
if CONF.Base.IsEnterprise {
|
||||||
return "https://apps-assets.fit2cloud.com"
|
return "https://apps-assets.fit2cloud.com"
|
||||||
}
|
}
|
||||||
if CONF.Base.IsFxplay {
|
|
||||||
return "https://apps-assets.fit2cloud.com"
|
|
||||||
}
|
|
||||||
if CONF.Base.Edition != "intl" {
|
if CONF.Base.Edition != "intl" {
|
||||||
return "https://apps-assets.fit2cloud.com"
|
return "https://apps-assets.fit2cloud.com"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
+24
-6
@@ -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'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: "Decrypt"
|
|||||||
|
|
||||||
# agent
|
# agent
|
||||||
ErrAgentAccountBound: 'Account is bound to an agent and cannot be deleted'
|
ErrAgentAccountBound: 'Account is bound to an agent and cannot be deleted'
|
||||||
ErrTerminalAIAccountInUse: 'This model account is currently used by Terminal AI. Switch the account in Terminal AI settings or disable Terminal AI and try again.'
|
ErrTerminalAIAccountInUse: 'This model service is currently used by Terminal AI. Switch the account in Terminal AI settings or disable Terminal AI and try again.'
|
||||||
ErrFileAIAccountInUse: 'This model account is currently used by File Management AI. Switch the account in File Management AI settings or disable File Management AI and try again.'
|
ErrFileAIAccountInUse: 'This model service is currently used by File Management AI. Switch the account in File Management AI settings or disable File Management AI and try again.'
|
||||||
ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Unsupported agent provider'
|
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'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
|
|||||||
|
|
||||||
# AI gateway
|
# AI gateway
|
||||||
ErrAIProxyServiceNotInstalled: '{{ .name }} service is not installed'
|
ErrAIProxyServiceNotInstalled: '{{ .name }} service is not installed'
|
||||||
ErrAIProxyModelAccountNotFound: 'Model account {{ .id }} not found'
|
ErrAIProxyModelAccountNotFound: 'Model service {{ .id }} not found'
|
||||||
ErrAIProxyModelMapRequired: 'This field is required.'
|
ErrAIProxyModelMapRequired: 'This field is required.'
|
||||||
ErrAIProxyModelMapInvalid: 'Model mapping must be a valid JSON object: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'Model mapping must be a valid JSON object: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'Model mapping cannot be empty'
|
ErrAIProxyModelMapEmpty: 'Model mapping cannot be empty'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: '1Panel AI Gateway'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
|
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "The model and tokenizer directories must be different, and neither can contain the other"
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: 'Descifrar'
|
|||||||
|
|
||||||
# agente
|
# agente
|
||||||
ErrAgentAccountBound: 'Cuenta vinculada a agente'
|
ErrAgentAccountBound: 'Cuenta vinculada a agente'
|
||||||
ErrTerminalAIAccountInUse: 'Esta cuenta de modelo esta siendo usada por Terminal AI. Cambia la cuenta en la configuracion de Terminal AI o desactiva Terminal AI y vuelve a intentarlo.'
|
ErrTerminalAIAccountInUse: 'Este servicio de modelo esta siendo usado por Terminal AI. Cambia la cuenta en la configuracion de Terminal AI o desactiva Terminal AI y vuelve a intentarlo.'
|
||||||
ErrFileAIAccountInUse: 'Esta cuenta de modelo está siendo utilizada por la IA del gestor de archivos. Cambia la cuenta en su configuración o desactívala y vuelve a intentarlo.'
|
ErrFileAIAccountInUse: 'Este servicio de modelo está siendo utilizado por la IA del gestor de archivos. Cambia la cuenta en su configuración o desactívala y vuelve a intentarlo.'
|
||||||
ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
|
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'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
|
|||||||
|
|
||||||
# puerta de enlace de IA
|
# puerta de enlace de IA
|
||||||
ErrAIProxyServiceNotInstalled: 'El servicio {{ .name }} no está instalado'
|
ErrAIProxyServiceNotInstalled: 'El servicio {{ .name }} no está instalado'
|
||||||
ErrAIProxyModelAccountNotFound: 'Cuenta de modelo {{ .id }} no encontrada'
|
ErrAIProxyModelAccountNotFound: 'Servicio de modelo {{ .id }} no encontrado'
|
||||||
ErrAIProxyModelMapRequired: 'Este campo es obligatorio.'
|
ErrAIProxyModelMapRequired: 'Este campo es obligatorio.'
|
||||||
ErrAIProxyModelMapInvalid: 'El mapeo de modelos debe ser un objeto JSON válido: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'El mapeo de modelos debe ser un objeto JSON válido: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'El mapeo de modelos no puede estar vacío'
|
ErrAIProxyModelMapEmpty: 'El mapeo de modelos no puede estar vacío'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'Puerta de enlace de IA de 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
|
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "Ya existe una plantilla de extensiones PHP con este nombre y contenido diferente; no se ha sobrescrito"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "Los directorios de modelos y tokenizadores deben ser distintos y ninguno puede contener al otro"
|
||||||
|
|||||||
+24
-6
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'منبع درخواستی یافت نشد: {{ .err }}'
|
|||||||
|
|
||||||
# پرش سریع
|
# پرش سریع
|
||||||
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
|
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
|
||||||
ErrMaxQuickJump: 'حداکثر ۴ ورودی پرش سریع'
|
ErrMaxQuickJump: 'حداکثر ۵ ورودی پرش سریع'
|
||||||
|
|
||||||
# عمومی
|
# عمومی
|
||||||
ErrShutDown: 'دستور به صورت دستی متوقف شد'
|
ErrShutDown: 'دستور به صورت دستی متوقف شد'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: "رمزگشایی"
|
|||||||
|
|
||||||
# عامل
|
# عامل
|
||||||
ErrAgentAccountBound: 'حساب به یک عامل متصل است و قابل حذف نیست'
|
ErrAgentAccountBound: 'حساب به یک عامل متصل است و قابل حذف نیست'
|
||||||
ErrTerminalAIAccountInUse: 'این حساب مدل در حال حاضر توسط ترمینال هوش مصنوعی استفاده میشود. حساب را در تنظیمات ترمینال هوش مصنوعی تغییر دهید یا ترمینال هوش مصنوعی را غیرفعال کرده و دوباره تلاش کنید.'
|
ErrTerminalAIAccountInUse: 'این سرویس مدل در حال حاضر توسط ترمینال هوش مصنوعی استفاده میشود. حساب را در تنظیمات ترمینال هوش مصنوعی تغییر دهید یا ترمینال هوش مصنوعی را غیرفعال کرده و دوباره تلاش کنید.'
|
||||||
ErrFileAIAccountInUse: 'این حساب مدل توسط هوش مصنوعی مدیریت فایل استفاده میشود. حساب را در تنظیمات آن تغییر دهید یا هوش مصنوعی مدیریت فایل را غیرفعال کرده و دوباره تلاش کنید.'
|
ErrFileAIAccountInUse: 'این سرویس مدل توسط هوش مصنوعی مدیریت فایل استفاده میشود. حساب را در تنظیمات آن تغییر دهید یا هوش مصنوعی مدیریت فایل را غیرفعال کرده و دوباره تلاش کنید.'
|
||||||
ErrAgentAccountUnavailable: 'اتصال حساب در دسترس نیست: {{ .err }}'
|
ErrAgentAccountUnavailable: 'اتصال حساب در دسترس نیست: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'ارائهدهنده عامل پشتیبانی نمیشود'
|
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'
|
||||||
@@ -676,7 +677,7 @@ TerminalAIRequestFailed: 'درخواست هوش مصنوعی ناموفق بود
|
|||||||
|
|
||||||
# دروازه هوش مصنوعی
|
# دروازه هوش مصنوعی
|
||||||
ErrAIProxyServiceNotInstalled: 'سرویس {{ .name }} نصب نشده است'
|
ErrAIProxyServiceNotInstalled: 'سرویس {{ .name }} نصب نشده است'
|
||||||
ErrAIProxyModelAccountNotFound: 'حساب مدل {{ .id }} یافت نشد'
|
ErrAIProxyModelAccountNotFound: 'سرویس مدل {{ .id }} یافت نشد'
|
||||||
ErrAIProxyModelMapRequired: 'این فیلد الزامی است.'
|
ErrAIProxyModelMapRequired: 'این فیلد الزامی است.'
|
||||||
ErrAIProxyModelMapInvalid: 'نگاشت مدل باید یک شی JSON معتبر باشد: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'نگاشت مدل باید یک شی JSON معتبر باشد: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'نگاشت مدل نمیتواند خالی باشد'
|
ErrAIProxyModelMapEmpty: 'نگاشت مدل نمیتواند خالی باشد'
|
||||||
@@ -687,6 +688,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'درگاه هوش مصنوعی 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راهاندازی مجدد کنید و دوباره تلاش کنید'
|
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راهاندازی مجدد کنید و دوباره تلاش کنید'
|
||||||
@@ -736,7 +738,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 +805,19 @@ 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: "برای بررسی بهروزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "قالب افزونه PHP با این نام و محتوای متفاوت از قبل وجود دارد؛ بازنویسی نشد"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "پوشههای مدل و توکنساز باید متفاوت باشند و هیچکدام نباید درون دیگری قرار گیرد"
|
||||||
|
|||||||
+24
-6
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '要求されたリソース {{ .err }} が見つかりま
|
|||||||
|
|
||||||
# クイックジャンプ
|
# クイックジャンプ
|
||||||
ErrMinQuickJump: 'クイックアクセスを1件以上追加'
|
ErrMinQuickJump: 'クイックアクセスを1件以上追加'
|
||||||
ErrMaxQuickJump: '最大4件のクイックアクセス'
|
ErrMaxQuickJump: '最大5件のクイックアクセス'
|
||||||
|
|
||||||
# 共通
|
# 共通
|
||||||
ErrShutDown: 'コマンドは手動で停止されました'
|
ErrShutDown: 'コマンドは手動で停止されました'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: '復号化'
|
|||||||
|
|
||||||
# エージェント
|
# エージェント
|
||||||
ErrAgentAccountBound: 'アカウントはエージェントに紐づいています'
|
ErrAgentAccountBound: 'アカウントはエージェントに紐づいています'
|
||||||
ErrTerminalAIAccountInUse: 'このモデルアカウントは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。'
|
ErrTerminalAIAccountInUse: 'このモデルサービスは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。'
|
||||||
ErrFileAIAccountInUse: 'このモデルアカウントはファイル管理 AI で使用中です。ファイル管理 AI の設定で別のアカウントへ切り替えるか、ファイル管理 AI を無効にしてから再試行してください。'
|
ErrFileAIAccountInUse: 'このモデルサービスはファイル管理 AI で使用中です。ファイル管理 AI の設定で別のアカウントへ切り替えるか、ファイル管理 AI を無効にしてから再試行してください。'
|
||||||
ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
|
ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
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 自動'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI リクエストに失敗しました: {{ .err }}'
|
|||||||
|
|
||||||
# AIゲートウェイ
|
# AIゲートウェイ
|
||||||
ErrAIProxyServiceNotInstalled: '{{ .name }} サービスはインストールされていません'
|
ErrAIProxyServiceNotInstalled: '{{ .name }} サービスはインストールされていません'
|
||||||
ErrAIProxyModelAccountNotFound: 'モデルアカウント {{ .id }} が見つかりません'
|
ErrAIProxyModelAccountNotFound: 'モデルサービス {{ .id }} が見つかりません'
|
||||||
ErrAIProxyModelMapRequired: 'この項目は必須です。'
|
ErrAIProxyModelMapRequired: 'この項目は必須です。'
|
||||||
ErrAIProxyModelMapInvalid: 'モデルマッピングは有効な JSON オブジェクトである必要があります: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'モデルマッピングは有効な JSON オブジェクトである必要があります: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'モデルマッピングは空にできません'
|
ErrAIProxyModelMapEmpty: 'モデルマッピングは空にできません'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: '1Panel AI ゲートウェイ'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
|
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
|
||||||
@@ -735,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 が見つかりません"
|
||||||
@@ -802,3 +804,19 @@ 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: "現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。"
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "同じ名前で内容が異なる PHP 拡張機能テンプレートが既に存在するため、上書きしませんでした"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "モデルとトークナイザーのディレクトリは別々にし、一方が他方を含まないようにしてください"
|
||||||
|
|||||||
+24
-6
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '요청한 리소스 {{ .err }}을 찾을 수 없습니다.'
|
|||||||
|
|
||||||
# 빠른 이동
|
# 빠른 이동
|
||||||
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
|
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
|
||||||
ErrMaxQuickJump: '퀵 액세스는 최대 4개'
|
ErrMaxQuickJump: '퀵 액세스는 최대 5개'
|
||||||
|
|
||||||
# 공통
|
# 공통
|
||||||
ErrShutDown: '명령이 수동으로 중단되었습니다'
|
ErrShutDown: '명령이 수동으로 중단되었습니다'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: '복호화'
|
|||||||
|
|
||||||
# 에이전트
|
# 에이전트
|
||||||
ErrAgentAccountBound: '계정이 에이전트에 묶여 있습니다'
|
ErrAgentAccountBound: '계정이 에이전트에 묶여 있습니다'
|
||||||
ErrTerminalAIAccountInUse: '이 모델 계정은 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.'
|
ErrTerminalAIAccountInUse: '이 모델 서비스는 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.'
|
||||||
ErrFileAIAccountInUse: '이 모델 계정은 현재 파일 관리 AI에서 사용 중입니다. 파일 관리 AI 설정에서 다른 계정으로 변경하거나 파일 관리 AI를 비활성화한 뒤 다시 시도하세요.'
|
ErrFileAIAccountInUse: '이 모델 서비스는 현재 파일 관리 AI에서 사용 중입니다. 파일 관리 AI 설정에서 다른 계정으로 변경하거나 파일 관리 AI를 비활성화한 뒤 다시 시도하세요.'
|
||||||
ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
|
ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
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 자동'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI 요청 실패: {{ .err }}'
|
|||||||
|
|
||||||
# AI 게이트웨이
|
# AI 게이트웨이
|
||||||
ErrAIProxyServiceNotInstalled: '{{ .name }} 서비스가 설치되어 있지 않습니다'
|
ErrAIProxyServiceNotInstalled: '{{ .name }} 서비스가 설치되어 있지 않습니다'
|
||||||
ErrAIProxyModelAccountNotFound: '모델 계정 {{ .id }}을(를) 찾을 수 없습니다'
|
ErrAIProxyModelAccountNotFound: '모델 서비스 {{ .id }}을(를) 찾을 수 없습니다'
|
||||||
ErrAIProxyModelMapRequired: '이 필드는 필수입니다.'
|
ErrAIProxyModelMapRequired: '이 필드는 필수입니다.'
|
||||||
ErrAIProxyModelMapInvalid: '모델 매핑은 유효한 JSON 객체여야 합니다: {{ .err }}'
|
ErrAIProxyModelMapInvalid: '모델 매핑은 유효한 JSON 객체여야 합니다: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: '모델 매핑은 비워 둘 수 없습니다'
|
ErrAIProxyModelMapEmpty: '모델 매핑은 비워 둘 수 없습니다'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: '1Panel AI 게이트웨이'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
|
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
|
||||||
@@ -735,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을 찾을 수 없습니다"
|
||||||
@@ -802,3 +804,19 @@ 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: "현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "같은 이름에 내용이 다른 PHP 확장 템플릿이 이미 있어 덮어쓰지 않았습니다"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "모델 디렉터리와 토크나이저 디렉터리는 서로 달라야 하며 서로 포함할 수 없습니다"
|
||||||
|
|||||||
+24
-6
@@ -32,7 +32,7 @@ ErrApiConfigKeyTimeInvalid: 'ເວລາ (Timestamp) ຂອງ API ບໍ່ຖ
|
|||||||
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
|
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
|
||||||
|
|
||||||
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
|
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
|
||||||
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 4 ລາຍການ'
|
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 5 ລາຍການ'
|
||||||
|
|
||||||
#common
|
#common
|
||||||
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
|
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
|
||||||
@@ -55,14 +55,14 @@ Decrypt: "ຖອດລະຫັດ"
|
|||||||
|
|
||||||
#agent
|
#agent
|
||||||
ErrAgentAccountBound: 'ບັນຊີຖືກຜູກມັດກັບຕົວແທນ (Agent) ແລະບໍ່ສາມາດລຶບໄດ້'
|
ErrAgentAccountBound: 'ບັນຊີຖືກຜູກມັດກັບຕົວແທນ (Agent) ແລະບໍ່ສາມາດລຶບໄດ້'
|
||||||
ErrTerminalAIAccountInUse: 'ບັນຊີໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ Terminal AI. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ Terminal AI ຫຼື ປິດໃຊ້ງານ Terminal AI ແລ້ວລອງໃໝ່.'
|
ErrTerminalAIAccountInUse: 'ບໍລິການໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ Terminal AI. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ Terminal AI ຫຼື ປິດໃຊ້ງານ Terminal AI ແລ້ວລອງໃໝ່.'
|
||||||
ErrFileAIAccountInUse: 'ບັນຊີໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ AI ຈັດການໄຟລ໌. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ AI ຈັດການໄຟລ໌ ຫຼື ປິດໃຊ້ງານແລ້ວລອງໃໝ່.'
|
ErrFileAIAccountInUse: 'ບໍລິການໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ AI ຈັດການໄຟລ໌. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ AI ຈັດການໄຟລ໌ ຫຼື ປິດໃຊ້ງານແລ້ວລອງໃໝ່.'
|
||||||
ErrAgentAccountUnavailable: 'ການເຊື່ອມຕໍ່ບັນຊີບໍ່ສາມາດໃຊ້ງານໄດ້: {{ .err }}'
|
ErrAgentAccountUnavailable: 'ການເຊື່ອມຕໍ່ບັນຊີບໍ່ສາມາດໃຊ້ງານໄດ້: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'ບໍ່ຮອງຮັບຜູ້ໃຫ້ບໍລິການຕົວແທນນີ້'
|
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 ອັດຕະໂນມັດ'
|
||||||
@@ -666,7 +667,7 @@ FileAISearchEmptyDir: 'ບໍ່ພົບໄຟລ໌ ຫຼື ໂຟນເດ
|
|||||||
|
|
||||||
# ai proxy
|
# ai proxy
|
||||||
ErrAIProxyServiceNotInstalled: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງບໍລິການ {{ .name }}'
|
ErrAIProxyServiceNotInstalled: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງບໍລິການ {{ .name }}'
|
||||||
ErrAIProxyModelAccountNotFound: 'ບໍ່ພົບບັນຊີໂມເດລ {{ .id }}'
|
ErrAIProxyModelAccountNotFound: 'ບໍ່ພົບບໍລິການໂມເດລ {{ .id }}'
|
||||||
ErrAIProxyModelMapRequired: 'ຟິວ (Field) ນີ້ຈຳເປັນຕ້ອງມີ.'
|
ErrAIProxyModelMapRequired: 'ຟິວ (Field) ນີ້ຈຳເປັນຕ້ອງມີ.'
|
||||||
ErrAIProxyModelMapInvalid: 'ການຈັບຄູ່ໂມເດລຕ້ອງເປັນວັດຖຸ JSON ທີ່ຖືກຕ້ອງ: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'ການຈັບຄູ່ໂມເດລຕ້ອງເປັນວັດຖຸ JSON ທີ່ຖືກຕ້ອງ: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'ການຈັບຄູ່ໂມເດລບໍ່ສາມາດຫວ່າງເປົ່າໄດ້'
|
ErrAIProxyModelMapEmpty: 'ການຈັບຄູ່ໂມເດລບໍ່ສາມາດຫວ່າງເປົ່າໄດ້'
|
||||||
@@ -677,6 +678,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'ເກດເວ AI ຂອງ 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
|
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
|
||||||
@@ -726,7 +728,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 +795,19 @@ 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: "ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "ມີແມ່ແບບສ່ວນຂະຫຍາຍ PHP ຊື່ນີ້ຢູ່ແລ້ວແຕ່ເນື້ອຫາຕ່າງກັນ; ບໍ່ໄດ້ຂຽນທັບ"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "ໂຟນເດີໂມເດວ ແລະ tokenizer ຕ້ອງແຕກຕ່າງກັນ ແລະ ບໍ່ສາມາດຢູ່ພາຍໃນກັນໄດ້"
|
||||||
|
|||||||
+24
-6
@@ -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'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: 'Dekripsi'
|
|||||||
|
|
||||||
# ejen
|
# ejen
|
||||||
ErrAgentAccountBound: 'Akaun terikat kepada ejen'
|
ErrAgentAccountBound: 'Akaun terikat kepada ejen'
|
||||||
ErrTerminalAIAccountInUse: 'Akaun model ini sedang digunakan oleh Terminal AI. Tukar akaun dalam tetapan Terminal AI atau nyahaktifkan Terminal AI, kemudian cuba lagi.'
|
ErrTerminalAIAccountInUse: 'Perkhidmatan model ini sedang digunakan oleh Terminal AI. Tukar akaun dalam tetapan Terminal AI atau nyahaktifkan Terminal AI, kemudian cuba lagi.'
|
||||||
ErrFileAIAccountInUse: 'Akaun model ini sedang digunakan oleh AI pengurusan fail. Tukar akaun dalam tetapannya atau nyahaktifkan AI tersebut, kemudian cuba lagi.'
|
ErrFileAIAccountInUse: 'Perkhidmatan model ini sedang digunakan oleh AI pengurusan fail. Tukar akaun dalam tetapannya atau nyahaktifkan AI tersebut, kemudian cuba lagi.'
|
||||||
ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
|
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'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Permintaan AI gagal: {{ .err }}'
|
|||||||
|
|
||||||
# gerbang AI
|
# gerbang AI
|
||||||
ErrAIProxyServiceNotInstalled: 'Perkhidmatan {{ .name }} belum dipasang'
|
ErrAIProxyServiceNotInstalled: 'Perkhidmatan {{ .name }} belum dipasang'
|
||||||
ErrAIProxyModelAccountNotFound: 'Akaun model {{ .id }} tidak ditemui'
|
ErrAIProxyModelAccountNotFound: 'Perkhidmatan model {{ .id }} tidak ditemui'
|
||||||
ErrAIProxyModelMapRequired: 'Medan ini wajib diisi.'
|
ErrAIProxyModelMapRequired: 'Medan ini wajib diisi.'
|
||||||
ErrAIProxyModelMapInvalid: 'Pemetaan model mestilah objek JSON yang sah: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'Pemetaan model mestilah objek JSON yang sah: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'Pemetaan model tidak boleh kosong'
|
ErrAIProxyModelMapEmpty: 'Pemetaan model tidak boleh kosong'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'Gerbang AI 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
|
ErrDockerIptablesChainUnavailable: 'Rantaian DOCKER-USER Docker tidak ditemui. Mulakan semula Docker dan cuba lagi'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "Templat sambungan PHP dengan nama ini sudah wujud tetapi kandungannya berbeza; tidak ditulis ganti"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "Direktori model dan tokenizer mestilah berbeza dan tidak boleh mengandungi satu sama lain"
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: 'Descriptografar'
|
|||||||
|
|
||||||
# agente
|
# agente
|
||||||
ErrAgentAccountBound: 'Conta vinculada a agente'
|
ErrAgentAccountBound: 'Conta vinculada a agente'
|
||||||
ErrTerminalAIAccountInUse: 'Esta conta de modelo esta sendo usada pelo Terminal AI. Troque a conta nas configuracoes do Terminal AI ou desative o Terminal AI e tente novamente.'
|
ErrTerminalAIAccountInUse: 'Este serviço de modelo esta sendo usado pelo Terminal AI. Troque a conta nas configuracoes do Terminal AI ou desative o Terminal AI e tente novamente.'
|
||||||
ErrFileAIAccountInUse: 'Esta conta de modelo está sendo usada pela IA do gerenciador de arquivos. Troque a conta nas configurações ou desative a IA e tente novamente.'
|
ErrFileAIAccountInUse: 'Este serviço de modelo está sendo usado pela IA do gerenciador de arquivos. Troque a conta nas configurações ou desative a IA e tente novamente.'
|
||||||
ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
|
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'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Falha na solicitação de AI: {{ .err }}'
|
|||||||
|
|
||||||
# gateway de IA
|
# gateway de IA
|
||||||
ErrAIProxyServiceNotInstalled: 'O serviço {{ .name }} não está instalado'
|
ErrAIProxyServiceNotInstalled: 'O serviço {{ .name }} não está instalado'
|
||||||
ErrAIProxyModelAccountNotFound: 'Conta de modelo {{ .id }} não encontrada'
|
ErrAIProxyModelAccountNotFound: 'Serviço de modelo {{ .id }} não encontrado'
|
||||||
ErrAIProxyModelMapRequired: 'Este campo é obrigatório.'
|
ErrAIProxyModelMapRequired: 'Este campo é obrigatório.'
|
||||||
ErrAIProxyModelMapInvalid: 'O mapeamento de modelos deve ser um objeto JSON válido: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'O mapeamento de modelos deve ser um objeto JSON válido: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'O mapeamento de modelos não pode estar vazio'
|
ErrAIProxyModelMapEmpty: 'O mapeamento de modelos não pode estar vazio'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'Gateway de IA do 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
|
ErrDockerIptablesChainUnavailable: 'A cadeia DOCKER-USER do Docker não foi encontrada. Reinicie o Docker e tente novamente'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "Já existe um modelo de extensões PHP com este nome e conteúdo diferente; ele não foi sobrescrito"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "Os diretórios de modelos e tokenizadores devem ser diferentes e nenhum pode conter o outro"
|
||||||
|
|||||||
+24
-6
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Запрошенный ресурс {{ .err }} не най
|
|||||||
|
|
||||||
# быстрый переход
|
# быстрый переход
|
||||||
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
|
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
|
||||||
ErrMaxQuickJump: 'Не более 4 быстрых ссылок'
|
ErrMaxQuickJump: 'Не более 5 быстрых ссылок'
|
||||||
|
|
||||||
# общее
|
# общее
|
||||||
ErrShutDown: 'Команда остановлена вручную'
|
ErrShutDown: 'Команда остановлена вручную'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: 'Расшифровать'
|
|||||||
|
|
||||||
# агент
|
# агент
|
||||||
ErrAgentAccountBound: 'Акаунт привязан к агенту'
|
ErrAgentAccountBound: 'Акаунт привязан к агенту'
|
||||||
ErrTerminalAIAccountInUse: 'Эта учетная запись модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.'
|
ErrTerminalAIAccountInUse: 'Этот сервис модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.'
|
||||||
ErrFileAIAccountInUse: 'Эта учетная запись модели используется ИИ файлового менеджера. Смените учетную запись в его настройках или отключите ИИ и повторите попытку.'
|
ErrFileAIAccountInUse: 'Этот сервис модели используется ИИ файлового менеджера. Смените учетную запись в его настройках или отключите ИИ и повторите попытку.'
|
||||||
ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступна: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступна: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
|
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 Авто'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Ошибка запроса AI: {{ .err }}'
|
|||||||
|
|
||||||
# AI-шлюз
|
# AI-шлюз
|
||||||
ErrAIProxyServiceNotInstalled: 'Сервис {{ .name }} не установлен'
|
ErrAIProxyServiceNotInstalled: 'Сервис {{ .name }} не установлен'
|
||||||
ErrAIProxyModelAccountNotFound: 'Аккаунт модели {{ .id }} не найден'
|
ErrAIProxyModelAccountNotFound: 'Сервис модели {{ .id }} не найден'
|
||||||
ErrAIProxyModelMapRequired: 'Это поле обязательно.'
|
ErrAIProxyModelMapRequired: 'Это поле обязательно.'
|
||||||
ErrAIProxyModelMapInvalid: 'Сопоставление моделей должно быть допустимым JSON-объектом: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'Сопоставление моделей должно быть допустимым JSON-объектом: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'Сопоставление моделей не может быть пустым'
|
ErrAIProxyModelMapEmpty: 'Сопоставление моделей не может быть пустым'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: 'ИИ-шлюз 1Panel'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
|
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
|
||||||
@@ -735,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 не найден"
|
||||||
@@ -802,3 +804,19 @@ 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: "Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "Шаблон расширений PHP с таким именем уже существует и имеет другое содержимое; перезапись не выполнена"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "Каталоги моделей и токенизаторов должны различаться и не могут быть вложены друг в друга"
|
||||||
|
|||||||
+24
-6
@@ -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'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: 'Şifre Çöz'
|
|||||||
|
|
||||||
# ajan
|
# ajan
|
||||||
ErrAgentAccountBound: 'Hesap bir ajana bağlı'
|
ErrAgentAccountBound: 'Hesap bir ajana bağlı'
|
||||||
ErrTerminalAIAccountInUse: 'Bu model hesabi su anda Terminal AI tarafindan kullaniliyor. Terminal AI ayarlarinda baska bir hesaba gecin veya Terminal AI yi devre disi birakip tekrar deneyin.'
|
ErrTerminalAIAccountInUse: 'Bu model hizmeti su anda Terminal AI tarafindan kullaniliyor. Terminal AI ayarlarinda baska bir hesaba gecin veya Terminal AI yi devre disi birakip tekrar deneyin.'
|
||||||
ErrFileAIAccountInUse: 'Bu model hesabı dosya yönetimi yapay zekası tarafından kullanılıyor. Ayarlardan hesabı değiştirin veya yapay zekayı devre dışı bırakıp yeniden deneyin.'
|
ErrFileAIAccountInUse: 'Bu model hizmeti dosya yönetimi yapay zekası tarafından kullanılıyor. Ayarlardan hesabı değiştirin veya yapay zekayı devre dışı bırakıp yeniden deneyin.'
|
||||||
ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
|
ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
|
||||||
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
|
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'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI istegi basarisiz oldu: {{ .err }}'
|
|||||||
|
|
||||||
# AI ağ geçidi
|
# AI ağ geçidi
|
||||||
ErrAIProxyServiceNotInstalled: '{{ .name }} servisi kurulu değil'
|
ErrAIProxyServiceNotInstalled: '{{ .name }} servisi kurulu değil'
|
||||||
ErrAIProxyModelAccountNotFound: 'Model hesabı {{ .id }} bulunamadı'
|
ErrAIProxyModelAccountNotFound: 'Model hizmeti {{ .id }} bulunamadı'
|
||||||
ErrAIProxyModelMapRequired: 'Bu alan zorunludur.'
|
ErrAIProxyModelMapRequired: 'Bu alan zorunludur.'
|
||||||
ErrAIProxyModelMapInvalid: 'Model eşlemesi geçerli bir JSON nesnesi olmalıdır: {{ .err }}'
|
ErrAIProxyModelMapInvalid: 'Model eşlemesi geçerli bir JSON nesnesi olmalıdır: {{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: 'Model eşlemesi boş olamaz'
|
ErrAIProxyModelMapEmpty: 'Model eşlemesi boş olamaz'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
|||||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||||
AIProviderArk: 'Volcengine Ark'
|
AIProviderArk: 'Volcengine Ark'
|
||||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||||
|
AIProvider1PanelGateway: '1Panel AI Ağ Geçidi'
|
||||||
|
|
||||||
# Firewall
|
# Firewall
|
||||||
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin'
|
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER zinciri bulunamadı. Docker’ı yeniden başlatıp tekrar deneyin'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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."
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "Bu ada sahip ancak içeriği farklı bir PHP uzantı şablonu zaten mevcut; üzerine yazılmadı"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "Model ve tokenizer dizinleri farklı olmalı ve biri diğerini içermemelidir"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '無法找到要求的資源{{ .err }}'
|
|||||||
|
|
||||||
# 快速跳轉
|
# 快速跳轉
|
||||||
ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
|
ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
|
||||||
ErrMaxQuickJump: '最多可設定四個快速跳轉入口!'
|
ErrMaxQuickJump: '最多可設定五個快速跳轉入口!'
|
||||||
|
|
||||||
# 通用
|
# 通用
|
||||||
ErrShutDown: '指令被手動結束!'
|
ErrShutDown: '指令被手動結束!'
|
||||||
@@ -70,14 +70,14 @@ Decrypt: '解密'
|
|||||||
|
|
||||||
# 智慧體
|
# 智慧體
|
||||||
ErrAgentAccountBound: '該帳號已綁定到智能體,無法刪除,請重試。'
|
ErrAgentAccountBound: '該帳號已綁定到智能體,無法刪除,請重試。'
|
||||||
ErrTerminalAIAccountInUse: '該模型帳號正被終端 AI 使用,請在終端 AI 設定中更換帳號或關閉終端 AI 後再試。'
|
ErrTerminalAIAccountInUse: '該模型服務正被終端 AI 使用,請在終端 AI 設定中更換帳號或關閉終端 AI 後再試。'
|
||||||
ErrFileAIAccountInUse: '該模型帳號正被檔案管理 AI 使用,請在檔案管理 AI 設定中更換帳號或關閉檔案管理 AI 後再試。'
|
ErrFileAIAccountInUse: '該模型服務正被檔案管理 AI 使用,請在檔案管理 AI 設定中更換帳號或關閉檔案管理 AI 後再試。'
|
||||||
ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }},請重試'
|
ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }},請重試'
|
||||||
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
|
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 自動'
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI 請求失敗:{{ .err }}'
|
|||||||
|
|
||||||
# AI 閘道
|
# AI 閘道
|
||||||
ErrAIProxyServiceNotInstalled: '{{ .name }} 服務未安裝'
|
ErrAIProxyServiceNotInstalled: '{{ .name }} 服務未安裝'
|
||||||
ErrAIProxyModelAccountNotFound: '模型帳號 {{ .id }} 不存在'
|
ErrAIProxyModelAccountNotFound: '模型服務 {{ .id }} 不存在'
|
||||||
ErrAIProxyModelMapRequired: '請填寫必填項'
|
ErrAIProxyModelMapRequired: '請填寫必填項'
|
||||||
ErrAIProxyModelMapInvalid: '模型映射必須是合法的 JSON 物件:{{ .err }}'
|
ErrAIProxyModelMapInvalid: '模型映射必須是合法的 JSON 物件:{{ .err }}'
|
||||||
ErrAIProxyModelMapEmpty: '模型映射不能為空'
|
ErrAIProxyModelMapEmpty: '模型映射不能為空'
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: '阿里雲百煉'
|
|||||||
AIProviderBailianCodingPlan: '阿里雲百煉 Coding Plan'
|
AIProviderBailianCodingPlan: '阿里雲百煉 Coding Plan'
|
||||||
AIProviderArk: '火山方舟'
|
AIProviderArk: '火山方舟'
|
||||||
AIProviderArkCodingPlan: '火山方舟 Coding Plan'
|
AIProviderArkCodingPlan: '火山方舟 Coding Plan'
|
||||||
|
AIProvider1PanelGateway: '1Panel AI 閘道'
|
||||||
|
|
||||||
# 防火牆
|
# 防火牆
|
||||||
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
|
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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: "目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器"
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "同名 PHP 擴充模版已存在且內容不同,未覆蓋"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "模型目錄和分詞器目錄不能相同或互相包含"
|
||||||
|
|||||||
+24
-6
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: "无法找到请求的资源 {{ .err }}"
|
|||||||
|
|
||||||
# 快速跳转
|
# 快速跳转
|
||||||
ErrMinQuickJump: "至少设置 1 个快速跳转入口"
|
ErrMinQuickJump: "至少设置 1 个快速跳转入口"
|
||||||
ErrMaxQuickJump: "最多设置 4 个快速跳转入口"
|
ErrMaxQuickJump: "最多设置 5 个快速跳转入口"
|
||||||
|
|
||||||
# 通用
|
# 通用
|
||||||
ErrShutDown: "命令已手动终止"
|
ErrShutDown: "命令已手动终止"
|
||||||
@@ -70,14 +70,14 @@ Decrypt: "解密"
|
|||||||
|
|
||||||
# 智能体
|
# 智能体
|
||||||
ErrAgentAccountBound: "该账号已绑定智能体,无法删除"
|
ErrAgentAccountBound: "该账号已绑定智能体,无法删除"
|
||||||
ErrTerminalAIAccountInUse: "该模型账号正被终端 AI 使用,请在终端 AI 设置中更换账号或关闭终端 AI 后重试"
|
ErrTerminalAIAccountInUse: "该模型服务正被终端 AI 使用,请在终端 AI 设置中更换账号或关闭终端 AI 后重试"
|
||||||
ErrFileAIAccountInUse: "该模型账号正被文件管理 AI 使用,请在文件管理 AI 设置中更换账号或关闭文件管理 AI 后重试"
|
ErrFileAIAccountInUse: "该模型服务正被文件管理 AI 使用,请在文件管理 AI 设置中更换账号或关闭文件管理 AI 后重试"
|
||||||
ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
|
ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
|
||||||
ErrAgentProviderNotSupported: "不支持该智能体提供商"
|
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 自动"
|
||||||
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
|
|||||||
|
|
||||||
# AI 网关
|
# AI 网关
|
||||||
ErrAIProxyServiceNotInstalled: "{{ .name }} 服务未安装"
|
ErrAIProxyServiceNotInstalled: "{{ .name }} 服务未安装"
|
||||||
ErrAIProxyModelAccountNotFound: "模型账号 {{ .id }} 不存在"
|
ErrAIProxyModelAccountNotFound: "模型服务 {{ .id }} 不存在"
|
||||||
ErrAIProxyModelMapRequired: "请填写必填项"
|
ErrAIProxyModelMapRequired: "请填写必填项"
|
||||||
ErrAIProxyModelMapInvalid: "模型映射必须是合法的 JSON 对象:{{ .err }}"
|
ErrAIProxyModelMapInvalid: "模型映射必须是合法的 JSON 对象:{{ .err }}"
|
||||||
ErrAIProxyModelMapEmpty: "模型映射不能为空"
|
ErrAIProxyModelMapEmpty: "模型映射不能为空"
|
||||||
@@ -686,6 +687,7 @@ AIProviderBailian: "阿里云百炼"
|
|||||||
AIProviderBailianCodingPlan: "阿里云百炼 Coding Plan"
|
AIProviderBailianCodingPlan: "阿里云百炼 Coding Plan"
|
||||||
AIProviderArk: "火山方舟"
|
AIProviderArk: "火山方舟"
|
||||||
AIProviderArkCodingPlan: "火山方舟 Coding Plan"
|
AIProviderArkCodingPlan: "火山方舟 Coding Plan"
|
||||||
|
AIProvider1PanelGateway: '1Panel AI 网关'
|
||||||
|
|
||||||
# 防火墙
|
# 防火墙
|
||||||
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
|
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
|
||||||
@@ -735,7 +737,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 +804,19 @@ 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: "当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器"
|
||||||
|
|
||||||
|
ErrPHPExtensionsConflict: "同名 PHP 扩展模版已存在且内容不同,未覆盖"
|
||||||
|
|
||||||
|
ErrModelTokenizerDirOverlap: "模型目录和分词器目录不能相同或互相包含"
|
||||||
|
|||||||
@@ -34,6 +34,5 @@ func Init() {
|
|||||||
global.Dir.SSLLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/ssl"))
|
global.Dir.SSLLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/ssl"))
|
||||||
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
|
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
|
||||||
global.Dir.ConvertLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/convert"))
|
global.Dir.ConvertLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/convert"))
|
||||||
global.Dir.TensorRTLLMDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/ai/tensorrt_llm"))
|
|
||||||
global.Dir.FirewallDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/firewall"))
|
global.Dir.FirewallDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/firewall"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,38 +18,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
global.LOG.Info("agent hook: init global data start")
|
|
||||||
initGlobalData()
|
initGlobalData()
|
||||||
global.LOG.Info("agent hook: init global data done")
|
|
||||||
global.LOG.Info("agent hook: handle cronjob status start")
|
|
||||||
handleCronjobStatus()
|
handleCronjobStatus()
|
||||||
global.LOG.Info("agent hook: handle cronjob status done")
|
|
||||||
global.LOG.Info("agent hook: handle clam status start")
|
|
||||||
handleClamStatus()
|
handleClamStatus()
|
||||||
global.LOG.Info("agent hook: handle clam status done")
|
|
||||||
global.LOG.Info("agent hook: handle record status start")
|
|
||||||
handleRecordStatus()
|
handleRecordStatus()
|
||||||
global.LOG.Info("agent hook: handle record status done")
|
|
||||||
global.LOG.Info("agent hook: handle snapshot status start")
|
|
||||||
handleSnapStatus()
|
handleSnapStatus()
|
||||||
global.LOG.Info("agent hook: handle snapshot status done")
|
|
||||||
global.LOG.Info("agent hook: handle ollama model status start")
|
|
||||||
handleOllamaModelStatus()
|
handleOllamaModelStatus()
|
||||||
global.LOG.Info("agent hook: handle ollama model status done")
|
|
||||||
|
|
||||||
global.LOG.Info("agent hook: load local dir start")
|
|
||||||
loadLocalDir()
|
loadLocalDir()
|
||||||
global.LOG.Info("agent hook: load local dir done")
|
|
||||||
|
|
||||||
global.LOG.Info("agent hook: init docker config start")
|
|
||||||
initDockerConf()
|
initDockerConf()
|
||||||
global.LOG.Info("agent hook: init docker config done")
|
|
||||||
global.LOG.Info("agent hook: init alert task start")
|
|
||||||
initAlertTask()
|
initAlertTask()
|
||||||
global.LOG.Info("agent hook: init alert task done")
|
|
||||||
global.LOG.Info("agent hook: init monitor db start")
|
|
||||||
initMonitorDB()
|
initMonitorDB()
|
||||||
global.LOG.Info("agent hook: init monitor db done")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGlobalData() {
|
func initGlobalData() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,11 +58,9 @@ func agentDBMigrations() []*gormigrate.Migration {
|
|||||||
migrations.AddTimeoutForClam,
|
migrations.AddTimeoutForClam,
|
||||||
migrations.UpdateCronjobSpec,
|
migrations.UpdateCronjobSpec,
|
||||||
migrations.UpdateWebsiteSSLAddColumn,
|
migrations.UpdateWebsiteSSLAddColumn,
|
||||||
migrations.AddTensorRTLLMModel,
|
|
||||||
migrations.UpdateMonitorInterval,
|
migrations.UpdateMonitorInterval,
|
||||||
migrations.AddMonitorProcess,
|
migrations.AddMonitorProcess,
|
||||||
migrations.UpdateCronJob,
|
migrations.UpdateCronJob,
|
||||||
migrations.UpdateTensorrtLLM,
|
|
||||||
migrations.AddCommonDescription,
|
migrations.AddCommonDescription,
|
||||||
migrations.UpdateDatabase,
|
migrations.UpdateDatabase,
|
||||||
migrations.AddGPUMonitor,
|
migrations.AddGPUMonitor,
|
||||||
@@ -119,6 +117,11 @@ func agentDBMigrations() []*gormigrate.Migration {
|
|||||||
migrations.AddQuickDirectory,
|
migrations.AddQuickDirectory,
|
||||||
migrations.AddLocalModelAccountLink,
|
migrations.AddLocalModelAccountLink,
|
||||||
migrations.AddOllamaModelAccountSync,
|
migrations.AddOllamaModelAccountSync,
|
||||||
|
migrations.AddContainerImageCheckUpdates,
|
||||||
|
migrations.AddModelQuickJump,
|
||||||
|
migrations.MigrateVMDatabase,
|
||||||
|
migrations.UpdateModelServicesQuickJumpRouter,
|
||||||
|
migrations.AddAgentAccountVerificationResult,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -976,13 +976,6 @@ var UpdateWebsiteSSLAddColumn = &gormigrate.Migration{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var AddTensorRTLLMModel = &gormigrate.Migration{
|
|
||||||
ID: "20251018-add-tensorrt-llm-model",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
return tx.AutoMigrate(&model.TensorRTLLM{})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var UpdateMonitorInterval = &gormigrate.Migration{
|
var UpdateMonitorInterval = &gormigrate.Migration{
|
||||||
ID: "20251026-update-monitor-interval",
|
ID: "20251026-update-monitor-interval",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
@@ -1021,13 +1014,6 @@ var UpdateCronJob = &gormigrate.Migration{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var UpdateTensorrtLLM = &gormigrate.Migration{
|
|
||||||
ID: "20251110-update-tensorrt-llm",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
return tx.AutoMigrate(&model.TensorRTLLM{})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var AddCommonDescription = &gormigrate.Migration{
|
var AddCommonDescription = &gormigrate.Migration{
|
||||||
ID: "20251117-add-common-description",
|
ID: "20251117-add-common-description",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
@@ -1939,3 +1925,35 @@ 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var UpdateModelServicesQuickJumpRouter = &gormigrate.Migration{
|
||||||
|
ID: "20261105-update-model-services-quick-jump-router",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return tx.Model(&model.QuickJump{}).
|
||||||
|
Where("name = ? AND router IN ?", "ModelServices", []string{"/ai/model", "/ai/model/account"}).
|
||||||
|
Update("router", "/ai/model-services").Error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var AddAgentAccountVerificationResult = &gormigrate.Migration{
|
||||||
|
ID: "20261106-add-agent-account-verification-settings",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
if err := tx.AutoMigrate(&model.AgentAccount{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Model(&model.AgentAccount{}).Where("validate_availability IS NULL").Update("validate_availability", true).Error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user