Compare commits

...
Author SHA1 Message Date
Yuki Guo fd462e39e8 Merge branch 'dev-v2' into pr@dev-v2@fix_page_cache 2026-11-06 18:04:44 +08:00
ssongliu d7783e72cb fix(ai): clarify benchmark image pull and model directory hints (#14319)
* fix(ai): clarify benchmark image pull and model directory hints

* feat(ai): update model and tokenizer directory guidance
2026-11-06 18:02:26 +08:00
guoyuqi e8ac89dcd2 fix : page caching settings 2026-11-06 17:39:49 +08:00
ssongliu 8a68d30dcb feat(ai): refine model service details and container log streaming (#14317) 2026-11-06 16:00:40 +08:00
王贺 66e9862cba feat(ai): add benchmark setup guidance and image pull navigation (#14318) 2026-11-06 15:57:34 +08:00
ssongliu 23a54653f8 fix(ai): persist model verification settings and reset lifecycle status (#14312)
* fix(ai): persist model verification settings and reset lifecycle status

* fix(ai): reset model verification before upgrade cutover
2026-11-06 00:44:50 +08:00
ssongliu 56ecd56c6d feat(ai): refine model lists and sync status hints (#14309) 2026-11-05 18:29:01 +08:00
ssongliu 024a246a46 refactor(ai): align menu order and simplify model routes (#14308) 2026-11-05 17:08:15 +08:00
ssongliu fe3953963e refactor(ai): move model preparation to XPack and reorganize model views (#14306) 2026-11-05 16:27:06 +08:00
Yuki Guoandssongliu d658395188 feat(sync): sync php extended template (#14299)
* feat(sync): sync php extended template

* fix(i18n): complete resource sync translations

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-11-05 15:06:12 +08:00
ssongliu 51bae946e1 feat(ai): refine model service terminology and provider setup (#14305) 2026-11-05 14:58:17 +08:00
Yuki Guo 56c7981b3a fix(docker): check image update (#14302) 2026-11-05 14:03:55 +08:00
ssongliu 730dbbab5b fix(vllm): distinguish empty metrics from collection failures (#14303) 2026-11-04 20:42:58 +08:00
ssongliu 2e9c1794f2 feat: separate model services into a standalone menu (#14300) 2026-11-04 17:23:21 +08:00
ssongliu d1059c7356 feat(ai): link model downloads to recipe search (#14298) 2026-11-04 16:43:29 +08:00
ssongliu b07afd3785 feat(ai): improve model preparation and download progress refresh (#14297) 2026-11-04 16:11:16 +08:00
ssongliu 89787a55ee feat(ai): refine model deployment UI and support GB10 upgrades (#14295) 2026-11-04 12:28:30 +08:00
ssongliu 5b9aaa7d7a fix(ai): use default buttons for additional model counts (#14294) 2026-11-04 10:02:09 +08:00
蘭 3a30f69b91 fix(mcp): unify website binding error feedback and fix status refresh and retries (#14290) 2026-11-04 09:54:19 +08:00
ssongliu 30381a3c68 feat(ai): streamline deployments and add local model selection (#14293)
* feat(ai): split model management and add weight provider filters

* feat(ai): streamline deployments and add local model selection
2026-11-03 23:54:22 +08:00
Kadxy 37170ae81a fix(openresty): report brotli as pending when the http.d include can be added (#14285)
Installs that enabled ngx_brotli before v2.3.0 have no http.d include in
nginx.conf, because the include is only inserted when a module is toggled
or built. The settings page reported this as "cannot write the managed
configuration" and skipped the brotli save, so the save path that inserts
the include was never reached, even on a stock nginx.conf.

The settings response now tells the two cases apart with an in-memory
insertion check that writes nothing:

- include missing but insertable, or managed file missing: managedPending,
  the page explains the values take effect after saving and saves normally
- no safe insertion point: managedUnavailable, unchanged

The save path now inserts the include through ensureNginxHTTPIncludeActive,
the same function module activation uses, so both produce identical
results. A sentinel error keeps "no insertion point" mapped to
ErrBrotliUnsupported while I/O failures are still returned as-is.
2026-11-03 18:58:24 +08:00
ssongliu daae8c43f8 refactor: remove Fxplay and TensorRT-LLM support (#14287) 2026-11-03 14:38:15 +08:00
ssongliu 133a02741a feat(vm): promote virtual machines to a top-level menu (#14286)
Migrate existing VM menus directly to the top level and preload permission-filtered menus on login. Coordinate menu refreshes during node switching and retain only sidebar preferences in persisted state.
2026-11-03 11:41:08 +08:00
ssongliu fac15e3292 fix(ai): refine model service forms and Ollama drawers (#14283) 2026-11-03 00:07:38 +08:00
蘭 e29679768b feat: add MCP error metadata handling and improve source policy resolution (#14270)
* feat: add MCP error metadata handling and improve source policy resolution

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

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

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

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

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

Co-authored-by: zhengkunwang223 <31820853+zhengkunwang223@users.noreply.github.com>
2026-10-30 23:31:05 +08:00
ssongliu 7156addf79 feat(ai): refine model pools, navigation and GPU deployment configuration (#14257) 2026-10-30 23:00:23 +08:00
ssongliu c6329a62db feat(dashboard): refine monitoring, quick access and installation messages (#14256) 2026-10-30 22:46:20 +08:00
guoyuqi 9644d5429f refactor: reuse base settings for image update proxy checks 2026-10-30 17:42:24 +08:00
guoyuqi 8b6328fd04 feat(container): add image update checks with scheduling and task logs 2026-10-29 19:02:44 +08:00
guoyuqi e9c2162921 feat(container): add image update checks with scheduling and task logs 2026-10-29 18:25:32 +08:00
393 changed files with 36717 additions and 8030 deletions
+2
View File
@@ -79,3 +79,5 @@ opencode.json
superpowers
.worktrees/
.codex/
/agent/utils/vm/helper/bin/1panel-kvm*
+5 -2
View File
@@ -32,7 +32,10 @@ build_core_on_linux:
cd $(CORE_PATH) \
&& 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) \
&& 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) \
&& 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) \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(AGENT_NAME) $(AGENT_MAIN)
+31 -11
View File
@@ -380,7 +380,7 @@ func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
}
// @Tags AI
// @Summary Get model account providers
// @Summary Get model service providers
// @Success 200 {array} dto.ProviderInfo
// @Security ApiKeyAuth
// @Security Timestamp
@@ -395,7 +395,7 @@ func (b *BaseApi) GetAgentProviders(c *gin.Context) {
}
// @Tags AI
// @Summary Create model account
// @Summary Create model service
// @Accept json
// @Param request body dto.AgentAccountCreateReq true "request"
// @Success 200
@@ -415,7 +415,7 @@ func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Update model account
// @Summary Update model service
// @Accept json
// @Param request body dto.AgentAccountUpdateReq true "request"
// @Success 200
@@ -435,7 +435,7 @@ func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Page model accounts
// @Summary Page model services
// @Accept json
// @Param request body dto.AgentAccountSearch true "request"
// @Success 200 {object} dto.PageResult
@@ -459,7 +459,7 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
}
// @Tags AI
// @Summary Count model accounts by provider
// @Summary Count model services by provider
// @Accept json
// @Param request body dto.AgentAccountProviderCountReq true "request"
// @Success 200 {object} map[string]int64
@@ -480,7 +480,7 @@ func (b *BaseApi) CountAgentAccountsByProviders(c *gin.Context) {
}
// @Tags AI
// @Summary List model account models
// @Summary List model service models
// @Accept json
// @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel
@@ -500,6 +500,26 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
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
// @Summary Discover custom provider models
// @Accept json
@@ -522,7 +542,7 @@ func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
}
// @Tags AI
// @Summary Create model account model
// @Summary Create model service model
// @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200
@@ -542,7 +562,7 @@ func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Update model account model
// @Summary Update model service model
// @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200
@@ -562,7 +582,7 @@ func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Delete model account model
// @Summary Delete model service model
// @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200
@@ -582,7 +602,7 @@ func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Verify model account
// @Summary Verify model service
// @Accept json
// @Param request body dto.AgentAccountVerifyReq true "request"
// @Success 200
@@ -602,7 +622,7 @@ func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Delete model account
// @Summary Delete model service
// @Accept json
// @Param request body dto.AgentAccountDeleteReq true "request"
// @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)
}
+1 -1
View File
@@ -11,6 +11,7 @@ var ApiGroupApp = new(ApiGroup)
type BaseApi struct{}
var (
vmService = service.NewIVMService()
dashboardService = service.NewIDashboardService()
appService = service.NewIAppService()
@@ -19,7 +20,6 @@ var (
aiToolService = service.NewIAIToolService()
mcpServerService = service.NewIMcpServerService()
tensorrtLLMService = service.NewITensorRTLLMService()
agentService = service.NewIAgentService()
containerService = service.NewIContainerService()
+26 -4
View File
@@ -12,7 +12,6 @@ import (
"sync"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -23,7 +22,7 @@ import (
)
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{
Code: code,
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) {
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
applyErrorMetadata(ctx, code, msgKey, err)
res := dto.Response{
Code: code,
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{}) {
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
applyErrorMetadata(ctx, code, msgKey, err)
res := dto.Response{
Code: code,
Data: data,
@@ -209,3 +208,26 @@ func SetOperationLogValues(c *gin.Context, values map[string]string) {
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
}
}
const errorMetadataKey = "error_response_metadata_policies"
// ErrorMetadataPolicy adds transport metadata before the existing error response is written.
type ErrorMetadataPolicy func(*gin.Context, int, string, error)
func WithErrorMetadata(policies ...ErrorMetadataPolicy) gin.HandlerFunc {
configured := append([]ErrorMetadataPolicy(nil), policies...)
return func(c *gin.Context) {
c.Set(errorMetadataKey, configured)
c.Next()
}
}
func applyErrorMetadata(c *gin.Context, code int, messageKey string, err error) {
value, _ := c.Get(errorMetadataKey)
policies, _ := value.([]ErrorMetadataPolicy)
for _, policy := range policies {
if policy != nil {
policy(c, code, messageKey, err)
}
}
}
+31
View File
@@ -1,8 +1,12 @@
package v2
import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
_ "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" // Swagger response schema.
"github.com/gin-gonic/gin"
)
@@ -225,3 +229,30 @@ func (b *BaseApi) ImageLoad(c *gin.Context) {
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)
}
+43
View File
@@ -103,3 +103,46 @@ func (b *BaseApi) DeletePHPExtensions(c *gin.Context) {
}
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)
}
-112
View File
@@ -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
+16 -3
View File
@@ -304,12 +304,18 @@ type AgentAccountModel struct {
RecordID uint `json:"recordId"`
ID string `json:"id"`
Name string `json:"name"`
IsVerifyModel bool `json:"isVerifyModel,omitempty"`
}
type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"`
}
type AgentAccountVerifyModelUpdateReq struct {
AccountID uint `json:"accountId" validate:"required"`
RecordID uint `json:"recordId" validate:"required"`
}
type AgentAccountModelDiscoverReq struct {
Provider string `json:"provider" validate:"required"`
BaseURL string `json:"baseURL" validate:"required"`
@@ -318,6 +324,7 @@ type AgentAccountModelDiscoverReq struct {
}
type AgentAccountModelCreateReq struct {
TaskID string `json:"taskID"`
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
}
@@ -328,6 +335,7 @@ type AgentAccountModelUpdateReq struct {
}
type AgentAccountModelDeleteReq struct {
DeleteFiles bool `json:"deleteFiles"`
AccountID uint `json:"accountId" validate:"required"`
RecordID uint `json:"recordId" validate:"required"`
}
@@ -358,13 +366,15 @@ type AgentAccountUpdateReq struct {
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
Models []AgentAccountModel `json:"models"`
}
type AgentAccountVerifyReq struct {
Provider string `json:"provider" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
ID uint `json:"id"`
Provider string `json:"provider" validate:"required_without=ID"`
APIKey string `json:"apiKey" validate:"required_without=ID"`
BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
APIType string `json:"apiType" validate:"required_without=ID"`
AuthMode string `json:"authMode"`
Model string `json:"model"`
}
@@ -399,7 +409,10 @@ type AgentAccountInfo struct {
APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
VerifiedAt *time.Time `json:"verifiedAt"`
VerifyError string `json:"verifyError"`
Verified bool `json:"verified"`
ValidateAvailability bool `json:"validateAvailability"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
}
+3
View File
@@ -21,6 +21,9 @@ type InspectReq struct {
}
type ContainerInfo struct {
LatestVersion string `json:"latestVersion"`
TargetImage string `json:"targetImage"`
HasUpdate bool `json:"hasUpdate"`
ContainerID string `json:"containerID"`
Name string `json:"name"`
ImageId string `json:"imageID"`
+6
View File
@@ -2,6 +2,12 @@ package dto
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 {
PageInfo
Name string `json:"name"`
+9
View File
@@ -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
View File
@@ -11,4 +11,5 @@ type MCPWebsiteBinding struct {
Upstream string `json:"upstream"`
CoreHost string `json:"coreHost"`
ProxyKey string `json:"proxyKey"`
DeleteWebsite bool `json:"deleteWebsite,omitempty"`
}
+1
View File
@@ -5,6 +5,7 @@ import "github.com/1Panel-dev/1Panel/agent/app/dto"
type PHPExtensionsSearch struct {
dto.PageInfo
All bool `json:"all"`
Name string `json:"name"`
}
type PHPExtensionsCreate struct {
-41
View File
@@ -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"`
}
+2
View File
@@ -51,6 +51,7 @@ type TagDTO struct {
}
type AppInstalledCheck struct {
TaskID string `json:"taskID"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
IsExist bool `json:"isExist"`
@@ -107,6 +108,7 @@ type AppDetail struct {
}
type AppInstallDTO struct {
TaskID string `json:"taskID"`
ID uint `json:"id"`
Name string `json:"name"`
AppID uint `json:"appID"`
+1
View File
@@ -14,4 +14,5 @@ type MCPWebsiteBinding struct {
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
Definitive bool `json:"definitive,omitempty"`
WebsiteDeleted bool `json:"websiteDeleted,omitempty"`
}
+5 -1
View File
@@ -20,11 +20,15 @@ type NginxParam struct {
// NginxBrotliRes carries the brotli settings together with where they live.
// 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.
// 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.
type NginxBrotliRes struct {
Params []NginxParam `json:"params"`
ManagedExternally bool `json:"managedExternally"`
ManagedPending bool `json:"managedPending"`
ManagedUnavailable bool `json:"managedUnavailable"`
}
-27
View File
@@ -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"`
}
+447
View File
@@ -0,0 +1,447 @@
package dto
import (
"time"
)
type VMSearchResult struct {
PageResult
Limit int `json:"limit"` // Maximum VM count for the target node; 0 means unlimited.
}
type VMCheckItem struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Message string `json:"message"`
IsEnable bool `json:"isEnable"`
IsExist bool `json:"isExist"`
IsActive bool `json:"isActive"`
}
type VMEnvironment struct {
Arch string `json:"arch"`
ArchSupported bool `json:"archSupported"`
Ready bool `json:"ready"`
HelperExist bool `json:"helperExist"`
HelperMessage string `json:"helperMessage"`
KVMExist bool `json:"kvmExist"`
KVMMessage string `json:"kvmMessage"`
LibvirtExist bool `json:"libvirtExist"`
LibvirtActive bool `json:"libvirtActive"`
LibvirtMessage string `json:"libvirtMessage"`
StorageExist bool `json:"storageExist"`
StorageMessage string `json:"storageMessage"`
}
type VMHealthCheckItem struct {
Level string `json:"level"`
Check string `json:"check"`
Reason string `json:"reason,omitempty"`
Params map[string]string `json:"params,omitempty"`
Detail string `json:"detail,omitempty"`
FixCommand string `json:"fixCommand,omitempty"`
}
type VMHealthCheckResult struct {
Pass int `json:"pass"`
Warn int `json:"warn"`
Fail int `json:"fail"`
Items []VMHealthCheckItem `json:"items"`
}
type VMCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
CPU int `json:"cpu" validate:"required,min=1"`
Memory int64 `json:"memory" validate:"required,min=1"`
DiskSize int64 `json:"diskSize" validate:"required,min=1"`
OSType string `json:"osType" validate:"omitempty,oneof=windows linux other"`
DiskBus string `json:"diskBus" validate:"omitempty,oneof=sata virtio"`
TemplateID uint `json:"templateID"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths"`
ImagePath string `json:"imagePath"`
Network string `json:"network" validate:"required,vm_common"`
BootOrder string `json:"bootOrder"`
StartAfterCreate bool `json:"startAfterCreate"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMTemplateCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
TemplateName string `json:"templateName" validate:"required,vm_name"`
StoragePool string `json:"storagePool"`
Description string `json:"description"`
}
type VMTemplateDelete struct {
ID uint `json:"id" validate:"required"`
DeleteDisk bool `json:"deleteDisk"`
TaskID string `json:"taskID"`
}
type VMTemplateInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
SourceVM string `json:"sourceVM"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
IsExist bool `json:"isExist"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
Description string `json:"description"`
}
type VMUpdate struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
CPU int `json:"cpu" validate:"required,min=1"`
Memory int64 `json:"memory" validate:"required,min=1"`
ISOPaths []string `json:"isoPaths"`
Network string `json:"network" validate:"required,vm_common"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMDiskResize struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
Target string `json:"target" validate:"required,vm_common"`
SizeGiB int64 `json:"sizeGiB" validate:"required,min=1,max=8388607"`
}
type VMRename struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
NewName string `json:"newName" validate:"required,vm_name"`
}
type VMDelete struct {
TaskID string `json:"taskID"`
ID uint `json:"id" validate:"required"`
Force bool `json:"force"`
}
type VMOperate struct {
Name string `json:"name" validate:"required,vm_name"`
Operate string `json:"operate" validate:"required,oneof=start shutdown reboot destroy suspend resume"`
}
type VMInfoReq struct {
Name string `json:"name" validate:"required,vm_name"`
CheckDisks bool `json:"checkDisks"`
}
type VMReq struct {
Name string `json:"name" validate:"required,vm_name"`
}
type VMTaskResp struct {
TaskID string `json:"taskID"`
}
type VMOrphanItem struct {
Type string `json:"type" validate:"required,oneof=orphan_domain_disk orphan_snapshot partial_file"`
Path string `json:"path" validate:"required"`
Size int64 `json:"size"`
ModifiedAt int64 `json:"modifiedAt"`
}
type VMOrphanClean struct {
Items []VMOrphanItem `json:"items" validate:"required,min=1,dive"`
}
type VMSnapshotReq struct {
TaskID string `json:"taskID"`
Name string `json:"name" validate:"required,vm_name"`
SnapshotName string `json:"snapshotName" validate:"required,vm_name"`
Description string `json:"description"`
}
type VMSnapshotSearch struct {
Name string `json:"name" validate:"required,vm_name"`
}
type VMSnapshotInfo struct {
Name string `json:"name"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
State string `json:"state"`
Parent string `json:"parent"`
Path string `json:"path"`
Size int64 `json:"size"`
Current bool `json:"current"`
Children int `json:"children"`
}
type VMIsoSearch struct {
PageInfo
Type string `json:"type" validate:"required"`
Info string `json:"info"`
}
type VMIsoCreate struct {
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Description string `json:"description"`
}
type VMIsoUpdate struct {
ID uint `json:"id" validate:"required"`
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Description string `json:"description"`
}
type VMIsoImportStart struct {
Type string `json:"type" validate:"required"`
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Status string `json:"status" validate:"required,oneof=uploading downloading"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"`
Description string `json:"description"`
}
type VMIsoImportUpdate struct {
ID uint `json:"id" validate:"required"`
Status string `json:"status" validate:"required,oneof=uploading downloading ready failed"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType" validate:"omitempty,oneof=upload download"`
}
type VMIsoDelete struct {
ID uint `json:"id" validate:"required"`
DeleteFile bool `json:"deleteFile"`
}
type VMIsoInfo struct {
ID uint `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
IsExist bool `json:"isExist"`
Size int64 `json:"size"`
Status string `json:"status"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType"`
CreatedAt time.Time `json:"createdAt"`
Description string `json:"description"`
}
type VMResourceInfo struct {
Type string `json:"type"`
Name string `json:"name"`
SubType string `json:"subType"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
Path string `json:"path"`
Status string `json:"status"`
AutoStart bool `json:"autoStart"`
Capacity uint64 `json:"capacity"`
Available uint64 `json:"available"`
}
type VMResourceDelete struct {
ID uint `json:"id" validate:"required"`
Force bool `json:"force"`
}
type VMNetworkCreate struct {
Name string `json:"name" validate:"required,vm_common"`
Type string `json:"type" validate:"required,oneof=nat bridge macvtap"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface" validate:"required_if=Type macvtap"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMNetworkUpdate struct {
ID uint `json:"id" validate:"required"`
AutoStart *bool `json:"autoStart"`
Status *string `json:"status"`
Description *string `json:"description"`
}
type VMNetworkItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
Status string `json:"status"`
VMCount int `json:"vmCount"`
VMs []VMResourceRef `json:"vms"`
Registered bool `json:"registered"`
UsedIPs []VMNetworkIPUsage `json:"usedIPs"`
Error string `json:"error"`
}
type VMResourceRef struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type VMNetworkIPUsage struct {
IP string `json:"ip"`
VMName string `json:"vmName"`
MAC string `json:"mac"`
Source string `json:"source"`
}
type VMNetworkInterfaceItem struct {
Name string `json:"name"`
Addresses []string `json:"addresses"`
}
type VMNetworkBridgeItem struct {
Name string `json:"name"`
Recommended bool `json:"recommended"`
Description string `json:"description"`
}
type VMStorageCreate struct {
Name string `json:"name" validate:"required,vm_common"`
Type string `json:"type" validate:"required,oneof=dir"`
Path string `json:"path" validate:"required"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
}
type VMStorageUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"omitempty,vm_common"`
Path string `json:"path"`
AutoStart *bool `json:"autoStart"`
Status *string `json:"status"`
Description *string `json:"description"`
}
type VMStorageItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
AutoStart bool `json:"autoStart"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
Status string `json:"status"`
Capacity uint64 `json:"capacity"`
Available uint64 `json:"available"`
VMCount int `json:"vmCount"`
VMs []VMResourceRef `json:"vms"`
Error string `json:"error"`
}
type VMInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
UUID string `json:"uuid"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths"`
ImagePath string `json:"imagePath"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Disks []VMDisk `json:"disks"`
Interfaces []VMInterface `json:"interfaces"`
Graphics *VMGraphics `json:"graphics"`
TaskID string `json:"taskID"`
CreateTaskID string `json:"createTaskID"`
Description string `json:"description"`
}
type VMDisk struct {
Capacity int64 `json:"capacity"`
Format string `json:"format"`
ReadOnly bool `json:"readOnly"`
Shareable bool `json:"shareable"`
ResizeBlocked string `json:"resizeBlocked"`
Device string `json:"device"`
Type string `json:"type"`
Source string `json:"source"`
Target string `json:"target"`
Bus string `json:"bus"`
}
type VMInterface struct {
Type string `json:"type"`
Source string `json:"source"`
Model string `json:"model"`
MAC string `json:"mac"`
IP string `json:"ip"`
}
type VMGraphics struct {
Type string `json:"type"`
Port string `json:"port"`
Listen string `json:"listen"`
}
type VMStats struct {
Name string `json:"name"`
Status string `json:"status"`
Timestamp int64 `json:"timestamp"`
CPUTime uint64 `json:"cpuTime"`
VCPU int `json:"vcpu"`
Memory VMMemoryStats `json:"memory"`
Disk VMDiskStats `json:"disk"`
Network VMNetworkStats `json:"network"`
}
type VMMemoryStats struct {
AvailableKiB uint64 `json:"availableKiB"`
UnusedKiB uint64 `json:"unusedKiB"`
UsableKiB uint64 `json:"usableKiB"`
RSSKiB uint64 `json:"rssKiB"`
ActualKiB uint64 `json:"actualKiB"`
}
type VMDiskStats struct {
ReadBytes uint64 `json:"readBytes"`
WriteBytes uint64 `json:"writeBytes"`
ReadReqs uint64 `json:"readReqs"`
WriteReqs uint64 `json:"writeReqs"`
}
type VMNetworkStats struct {
RxBytes uint64 `json:"rxBytes"`
TxBytes uint64 `json:"txBytes"`
RxPackets uint64 `json:"rxPackets"`
TxPackets uint64 `json:"txPackets"`
}
+5
View File
@@ -1,5 +1,7 @@
package model
import "time"
type AgentAccount struct {
BaseModel
AppInstallID uint `json:"appInstallId"`
@@ -11,7 +13,10 @@ type AgentAccount struct {
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
RememberAPIKey bool `json:"rememberApiKey"`
VerifiedAt *time.Time `json:"verifiedAt"`
VerifyError string `json:"verifyError"`
Verified bool `json:"verified"`
ValidateAvailability bool `json:"validateAvailability"`
Remark string `json:"remark"`
MasterAccountID uint `json:"masterAccountId" gorm:"index"`
}
+1
View File
@@ -17,6 +17,7 @@ type AppInstall struct {
Param string `json:"param"`
Env string `json:"env"`
DockerCompose string `json:"dockerCompose" `
TaskID string `json:"taskID" gorm:"-"`
Status string `json:"status" gorm:"not null"`
Description string `json:"description"`
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" }
-14
View File
@@ -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"`
}
+81
View File
@@ -0,0 +1,81 @@
package model
type VirtualMachine struct {
BaseModel
Name string `json:"name"`
UUID string `json:"uuid"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
ISOPaths []string `json:"isoPaths" gorm:"serializer:json"`
ImagePath string `json:"imagePath"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
TaskID string `json:"taskID"`
CreateTaskID string `json:"createTaskID"`
Description string `json:"description"`
}
type VMTemplate struct {
BaseModel
Name string `json:"name"`
SourceVM string `json:"sourceVM"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
DiskSize int64 `json:"diskSize"`
DiskBus string `json:"diskBus"`
DiskPath string `json:"diskPath"`
StoragePool string `json:"storagePool"`
Network string `json:"network"`
BootOrder string `json:"bootOrder"`
Description string `json:"description"`
}
type VMIso struct {
BaseModel
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
Status string `json:"status"`
Error string `json:"error"`
TempFileID string `json:"tempFileID"`
TempFileType string `json:"tempFileType"`
Description string `json:"description"`
}
type VMNetwork struct {
BaseModel
Name string `json:"name"`
Type string `json:"type"`
BridgeName string `json:"bridgeName"`
SourceInterface string `json:"sourceInterface"`
CIDR string `json:"cidr" gorm:"column:cidr"`
Gateway string `json:"gateway"`
DHCPStart string `json:"dhcpStart"`
DHCPEnd string `json:"dhcpEnd"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Description string `json:"description"`
Error string `json:"error"`
}
type VMStorage struct {
BaseModel
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
AutoStart bool `json:"autoStart"`
Status string `json:"status"`
Description string `json:"description"`
Error string `json:"error"`
}
+5 -1
View File
@@ -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": {
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
Key: "llmman", DisplayName: "llmman", Sort: 75, DefaultAPIType: "openai-responses",
APIConfigs: []APIConfig{
{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},
@@ -59,6 +59,10 @@ var catalog = map[string]Meta{
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"),
},
"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": {
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
APIConfigs: []APIConfig{
-9
View File
@@ -32,15 +32,6 @@ const (
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 {
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
+35 -2
View File
@@ -16,10 +16,13 @@ type IAgentAccountRepo interface {
WithByMasterAccountID(masterID uint) DBOption
Create(account *model.AgentAccount) error
Save(account *model.AgentAccount) error
UpdateVerification(account *model.AgentAccount) error
ResetVerificationByAppInstallID(appInstallID uint) error
DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error)
Count() (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 {
@@ -57,6 +60,30 @@ func (a AgentAccountRepo) Save(account *model.AgentAccount) 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 {
return getDb().Delete(&model.AgentAccount{}, id).Error
}
@@ -69,6 +96,12 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
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) {
normalizedProviders := normalizeProviders(providers)
counts := make(map[string]int64, len(normalizedProviders))
@@ -99,7 +132,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
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 {
if err := tx.Save(account).Error; err != nil {
return err
+24 -1
View File
@@ -1,6 +1,9 @@
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{}
@@ -9,7 +12,9 @@ type IAgentAccountModelRepo interface {
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
Create(item *model.AgentAccountModel) error
Save(item *model.AgentAccountModel) error
SaveWithAccount(item *model.AgentAccountModel, account *model.AgentAccount) error
DeleteByID(id uint) error
DeleteWithAccount(id uint, account *model.AgentAccount) error
Delete(opts ...DBOption) error
}
@@ -41,10 +46,28 @@ func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) 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 {
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 {
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
}
+6
View File
@@ -31,6 +31,7 @@ type IAppInstallRepo interface {
GetFirst(opts ...DBOption) (model.AppInstall, error)
Create(ctx context.Context, install *model.AppInstall) error
Save(ctx context.Context, install *model.AppInstall) error
UpdateStatus(install *model.AppInstall) error
DeleteBy(opts ...DBOption) error
Delete(ctx context.Context, install model.AppInstall) error
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
@@ -45,6 +46,11 @@ func NewIAppInstallRepo() IAppInstallRepo {
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 {
return func(g *gorm.DB) *gorm.DB {
return g.Where("app_detail_id in (?)", detailIds)
+1 -1
View File
@@ -86,10 +86,10 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
}
if !withAll && len(quicks) == 0 {
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: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
{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"},
}
}
+7
View File
@@ -17,6 +17,7 @@ type TaskRepo struct {
type ITaskRepo interface {
Save(ctx context.Context, task *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)
Update(ctx context.Context, task *model.Task) error
UpdateRunningTaskToFailed(opts ...DBOption) error
@@ -98,6 +99,12 @@ func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
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) {
var tasks []model.Task
db := getTaskDb(opts...).Model(&model.Task{})
-56
View File
@@ -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
}
+315
View File
@@ -0,0 +1,315 @@
package repo
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VMRepo struct{}
type IVMRepo interface {
Get(opts ...DBOption) (model.VirtualMachine, error)
List() ([]model.VirtualMachine, error)
Count() (int64, error)
Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error)
Create(vm *model.VirtualMachine) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
WithByNetwork(network string) DBOption
WithByStorage(storage string) DBOption
GetTemplate(opts ...DBOption) (model.VMTemplate, error)
ListTemplates(opts ...DBOption) ([]model.VMTemplate, error)
CreateTemplate(item *model.VMTemplate) error
UpdateTemplateSource(oldName string, newName string) error
DeleteTemplate(opts ...DBOption) error
GetIso(opts ...DBOption) (model.VMIso, error)
PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error)
ListIsos(opts ...DBOption) ([]model.VMIso, error)
CreateIso(iso *model.VMIso) error
UpdateIso(id uint, vars map[string]interface{}) error
DeleteIso(opts ...DBOption) error
GetNetwork(opts ...DBOption) (model.VMNetwork, error)
PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error)
ListNetworks(opts ...DBOption) ([]model.VMNetwork, error)
CreateNetwork(network *model.VMNetwork) error
UpdateNetwork(id uint, vars map[string]interface{}) error
DeleteNetwork(opts ...DBOption) error
GetStorage(opts ...DBOption) (model.VMStorage, error)
PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error)
ListStorages(opts ...DBOption) ([]model.VMStorage, error)
CreateStorage(storage *model.VMStorage) error
UpdateStorage(id uint, vars map[string]interface{}) error
DeleteStorage(opts ...DBOption) error
}
func NewIVMRepo() IVMRepo {
return &VMRepo{}
}
func (v *VMRepo) Get(opts ...DBOption) (model.VirtualMachine, error) {
var vm model.VirtualMachine
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&vm).Error
return vm, err
}
func (v *VMRepo) List() ([]model.VirtualMachine, error) {
var vms []model.VirtualMachine
err := global.DB.Order("created_at desc").Find(&vms).Error
return vms, err
}
func (v *VMRepo) Count() (int64, error) {
var count int64
err := global.DB.Model(&model.VirtualMachine{}).Count(&count).Error
return count, err
}
func (v *VMRepo) Page(page, pageSize int, opts ...DBOption) (int64, []model.VirtualMachine, error) {
var vms []model.VirtualMachine
var total int64
db := global.DB.Model(&model.VirtualMachine{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&vms).Error
return total, vms, err
}
func (v *VMRepo) Create(vm *model.VirtualMachine) error {
return global.DB.Create(vm).Error
}
func (v *VMRepo) Update(id uint, vars map[string]interface{}) error {
if paths, ok := vars["iso_paths"].([]string); ok {
data, err := json.Marshal(paths)
if err != nil {
return err
}
vars["iso_paths"] = string(data)
}
return global.DB.Model(&model.VirtualMachine{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VirtualMachine{}).Error
}
func (v *VMRepo) WithByNetwork(network string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("network = ?", network)
}
}
func (v *VMRepo) WithByStorage(storage string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("storage_pool = ?", storage)
}
}
func (v *VMRepo) GetTemplate(opts ...DBOption) (model.VMTemplate, error) {
var item model.VMTemplate
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&item).Error
return item, err
}
func (v *VMRepo) ListTemplates(opts ...DBOption) ([]model.VMTemplate, error) {
var items []model.VMTemplate
db := global.DB.Model(&model.VMTemplate{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&items).Error
return items, err
}
func (v *VMRepo) CreateTemplate(item *model.VMTemplate) error {
return global.DB.Create(item).Error
}
func (v *VMRepo) UpdateTemplateSource(oldName string, newName string) error {
return global.DB.Model(&model.VMTemplate{}).Where("source_vm = ?", oldName).Update("source_vm", newName).Error
}
func (v *VMRepo) DeleteTemplate(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMTemplate{}).Error
}
func (v *VMRepo) GetIso(opts ...DBOption) (model.VMIso, error) {
var iso model.VMIso
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&iso).Error
return iso, err
}
func (v *VMRepo) PageIso(page, pageSize int, opts ...DBOption) (int64, []model.VMIso, error) {
var isos []model.VMIso
var total int64
db := global.DB.Model(&model.VMIso{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&isos).Error
return total, isos, err
}
func (v *VMRepo) ListIsos(opts ...DBOption) ([]model.VMIso, error) {
var isos []model.VMIso
db := global.DB.Model(&model.VMIso{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&isos).Error
return isos, err
}
func (v *VMRepo) CreateIso(iso *model.VMIso) error {
return global.DB.Create(iso).Error
}
func (v *VMRepo) UpdateIso(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMIso{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteIso(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMIso{}).Error
}
func (v *VMRepo) GetNetwork(opts ...DBOption) (model.VMNetwork, error) {
var network model.VMNetwork
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&network).Error
return network, err
}
func (v *VMRepo) PageNetwork(page, pageSize int, opts ...DBOption) (int64, []model.VMNetwork, error) {
var networks []model.VMNetwork
var total int64
db := global.DB.Model(&model.VMNetwork{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&networks).Error
return total, networks, err
}
func (v *VMRepo) ListNetworks(opts ...DBOption) ([]model.VMNetwork, error) {
var networks []model.VMNetwork
db := global.DB.Model(&model.VMNetwork{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&networks).Error
return networks, err
}
func (v *VMRepo) CreateNetwork(network *model.VMNetwork) error {
return global.DB.Create(network).Error
}
func (v *VMRepo) UpdateNetwork(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMNetwork{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteNetwork(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMNetwork{}).Error
}
func (v *VMRepo) GetStorage(opts ...DBOption) (model.VMStorage, error) {
var storage model.VMStorage
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&storage).Error
return storage, err
}
func (v *VMRepo) PageStorage(page, pageSize int, opts ...DBOption) (int64, []model.VMStorage, error) {
var storages []model.VMStorage
var total int64
db := global.DB.Model(&model.VMStorage{})
for _, opt := range opts {
db = opt(db)
}
err := db.Count(&total).Error
if err != nil {
return 0, nil, err
}
err = db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&storages).Error
return total, storages, err
}
func (v *VMRepo) ListStorages(opts ...DBOption) ([]model.VMStorage, error) {
var storages []model.VMStorage
db := global.DB.Model(&model.VMStorage{})
for _, opt := range opts {
db = opt(db)
}
err := db.Order("created_at desc").Order("id desc").Find(&storages).Error
return storages, err
}
func (v *VMRepo) CreateStorage(storage *model.VMStorage) error {
return global.DB.Create(storage).Error
}
func (v *VMRepo) UpdateStorage(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.VMStorage{}).Where("id = ?", id).Updates(vars).Error
}
func (v *VMRepo) DeleteStorage(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.VMStorage{}).Error
}
+288 -25
View File
@@ -79,6 +79,7 @@ type IAgentService interface {
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
UpdateAccountVerifyModel(req dto.AgentAccountVerifyModelUpdateReq) error
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
@@ -184,7 +185,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
storedModel := ""
var allowedOrigins []string
var account *model.AgentAccount
var installHooks *appInstallHooks
var installHooks *AppInstallHooks
var dashboardAuth agentDashboardAuth
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
@@ -195,7 +196,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if err != nil {
return nil, err
}
if !account.Verified {
if account.ValidateAvailability && !account.Verified {
return nil, buserr.New("ErrAgentAccountNotVerified")
}
provider = account.Provider
@@ -224,14 +225,14 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if token == "" {
token = generateToken()
}
installHooks = &appInstallHooks{
installHooks = &AppInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error {
return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
},
}
} else if agentType == constant.AppHermesAgent {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
installHooks = &appInstallHooks{
installHooks = &AppInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error {
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
return err
@@ -298,7 +299,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
DockerCompose: req.DockerCompose,
},
}
appInstall, err := AppService{}.installWithHooks(installReq, false, installHooks)
appInstall, err := AppService{}.InstallWithHooks(installReq, false, installHooks)
if err != nil {
return nil, err
}
@@ -1041,8 +1042,6 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
}
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
provider := req.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
return err
@@ -1060,6 +1059,11 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
if err != nil {
return err
}
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
return err
}
account := &model.AgentAccount{
Provider: resolvedInput.Provider,
Name: req.Name,
@@ -1069,9 +1073,14 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
APIType: resolvedInput.APIType,
AuthMode: resolvedInput.AuthMode,
VerifyModel: verifyModel,
Verified: true,
Verified: validateAvailability,
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 := tx.Create(account).Error; err != nil {
return err
@@ -1088,8 +1097,6 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
}
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@@ -1105,10 +1112,25 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if err != nil {
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
if strings.TrimSpace(requestedVerifyModel) == "" {
if _, ok := findAgentAccountModelForProvider(provider, models, account.VerifyModel); ok {
requestedVerifyModel = account.VerifyModel
}
}
verifyModel := ""
if account.AppInstallID == 0 || len(models) > 0 {
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
@@ -1116,11 +1138,51 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
return err
}
}
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
validateAvailability := account.ValidateAvailability
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 {
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.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey
@@ -1129,9 +1191,17 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.AuthMode = resolvedInput.AuthMode
account.VerifyModel = verifyModel
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
}
terminalai.InvalidateTerminalRuntimeCache()
@@ -1203,6 +1273,9 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
AuthMode: item.AuthMode,
VerifyModel: item.VerifyModel,
Verified: item.Verified,
ValidateAvailability: item.ValidateAvailability,
VerifiedAt: item.VerifiedAt,
VerifyError: item.VerifyError,
Remark: item.Remark,
CreatedAt: item.CreatedAt,
})
@@ -1240,7 +1313,51 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
if err != nil {
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) {
@@ -1275,6 +1392,36 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
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
record := &model.AgentAccountModel{
AccountID: account.ID,
@@ -1282,7 +1429,8 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
Name: nextModel.Name,
SortOrder: sortOrder,
}
if err := agentAccountModelRepo.Create(record); err != nil {
err = agentAccountModelRepo.Create(record)
if err != nil {
return err
}
return a.syncAgentsByAccount(account)
@@ -1305,6 +1453,15 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil {
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
for _, item := range models {
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 {
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
record.Model = nextModel.ID
record.Name = nextModel.Name
if err := agentAccountModelRepo.Save(record); err != nil {
return err
verifyModel := account.VerifyModel
if verifyModel == "" && len(models) > 0 {
verifyModel = models[0].ID
}
if sameProviderModelID(account.Provider, verifyModel, previousModelID) && !sameProviderModelID(account.Provider, previousModelID, nextModel.ID) {
if account.ValidateAvailability {
account.Verified = false
account.VerifiedAt = nil
account.VerifyError = ""
}
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
account.VerifyModel = nextModel.ID
if err := agentAccountRepo.Save(account); err != nil {
return err
err = agentAccountModelRepo.SaveWithAccount(record, account)
} else {
err = agentAccountModelRepo.Save(record)
}
if err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache()
@@ -1343,6 +1517,10 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
}
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))
if err != nil {
return err
@@ -1351,13 +1529,17 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err != nil {
return err
}
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
return buserr.New("ErrAgentVerifyModelInUse")
}
models, err := loadAgentAccountModels(account)
if err != nil {
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))
for _, item := range models {
if item.RecordID == req.RecordID {
@@ -1368,7 +1550,51 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
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
}
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 {
if req.ID == 0 {
_, 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 {
+134 -8
View File
@@ -1,20 +1,26 @@
package service
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
"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") {
return nil, fmt.Errorf("invalid local model installation")
}
@@ -30,7 +36,7 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
return nil, err
}
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) {
@@ -52,9 +58,13 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
if install.App.Key == "ollama" {
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)
if apiKey != nil && account.APIKey != strings.TrimSpace(*apiKey) {
account.APIKey = strings.TrimSpace(*apiKey)
accountChanged = true
}
account.AppInstallID = install.ID
if updateBaseURL {
account.BaseURL = baseURL
@@ -71,6 +81,11 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
account.VerifyModel = ""
accountChanged = true
}
if accountChanged && account.ValidateAvailability {
account.Verified = false
account.VerifiedAt = nil
account.VerifyError = ""
}
var previous []model.AgentAccountModel
if account.ID != 0 {
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 {
err = agentAccountRepo.SaveLocal(account, rows)
err = agentAccountRepo.SaveWithModels(account, rows)
} else if accountChanged {
err = agentAccountRepo.Save(account)
} else {
@@ -132,10 +147,85 @@ func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBase
for _, item := range models {
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
}
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) {
addressType := install.ModelAccountBaseURLType
if addressType == "" {
@@ -166,12 +256,12 @@ func CleanupLocalModelAccount(installID uint) {
}
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
if err != nil {
global.LOG.Errorf("load local model accounts: %v", err)
global.LOG.Errorf("load local model services: %v", err)
return
}
for _, account := range accounts {
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":
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
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
case "localhost":
@@ -200,3 +290,39 @@ func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, cont
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")
}
+27 -4
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"reflect"
"regexp"
"slices"
"strconv"
@@ -63,6 +64,21 @@ func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) erro
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) {
var settings []model.Setting
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
}
modelID = strings.TrimSpace(modelID)
if modelID == "" && !allowEmptyModel {
if modelID == "" && (validateAvailability || !allowEmptyModel) {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
}
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
if validateAvailability {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err
}
@@ -1328,7 +1343,7 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil {
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")
}
if len(requested) > 0 {
@@ -1517,6 +1532,14 @@ func sameProviderModelID(provider, left, right string) bool {
}
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
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
}
+24 -18
View File
@@ -117,6 +117,7 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
}
taskItem, err := task.NewTaskWithOps(fmt.Sprintf("ollama-model-%s", req.Name), task.TaskPull, task.TaskScopeAI, req.TaskID, info.ID)
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)
return err
}
@@ -126,19 +127,17 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
}, nil)
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
itemSize, err := loadModelSize(info.Name, containerName)
if len(itemSize) != 0 {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
} else {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
if err != nil {
return err
}
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)
if err := taskItem.Execute(); err != nil {
_ = 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
}
@@ -167,6 +166,9 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
if modelInfo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if modelInfo.Status == constant.StatusWaiting {
return buserr.New("TaskIsExecuting")
}
containerName, err := LoadContainerName()
if err != nil {
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)
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)
return err
}
@@ -185,19 +188,17 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
}, nil)
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
itemSize, err := loadModelSize(modelInfo.Name, containerName)
if len(itemSize) != 0 {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
} else {
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
if err != nil {
return err
}
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)
if err := taskItem.Execute(); err != nil {
_ = 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
}
@@ -219,11 +220,16 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
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)
_ = os.Remove(logItem)
}
return SyncOllamaModelAccount(false)
return nil
}
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
+23 -8
View File
@@ -58,7 +58,8 @@ type IAppService interface {
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
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) {
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 {
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
return
@@ -589,6 +590,20 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
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)
if err != nil {
appInstall.Status = constant.StatusInstallErr
@@ -629,10 +644,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err
}
updateToolApp(appInstall)
if app.Key == "ollama" && appInstall.SyncModelAccount {
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
return err
}
return nil
}
@@ -642,7 +653,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
_ = 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() {
if taskErr := installTask.Execute(); taskErr != nil {
@@ -223,6 +223,79 @@ func TestExplicitConfigComposeDetectsCustomAndReusesCurrentCache(t *testing.T) {
}
}
func TestRemovedCurrentComposeDoesNotBlockUpgrade(t *testing.T) {
for _, custom := range []bool{false, true} {
t.Run(fmt.Sprintf("custom=%v", custom), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/demo/1.0/docker-compose.yml" {
w.WriteHeader(http.StatusNotFound)
return
}
version := strings.Split(r.URL.Path, "/")[2]
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
}))
t.Cleanup(server.Close)
db, _, install, details := composeUpgradeDB(t, server.URL)
if err := db.Model(&details[1]).UpdateColumn("status", constant.AppTakeDown).Error; err != nil {
t.Fatal(err)
}
if custom {
install.DockerCompose += " environment:\n USER_SETTING: preserve\n"
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
t.Fatal(err)
}
}
service := NewIAppInstalledService()
config, err := service.GetParams(install.ID)
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || !config.IsEdit {
t.Fatalf("removed template must preserve the instance and warn about custom configuration: %+v, %v", config, err)
}
var current model.AppDetail
if err := db.First(&current, details[1].ID).Error; err != nil || current.DockerCompose != "" {
t.Fatalf("instance Compose was cached as the official template: %+v, %v", current, err)
}
noCompose := false
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
if err != nil || len(versions) != 2 {
t.Fatalf("removed current version blocked available versions: %+v, %v", versions, err)
}
for _, version := range versions {
previews, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: version.Version})
if err != nil {
t.Fatal(err)
}
for _, preview := range previews {
if preview.Version == version.Version && !strings.Contains(preview.DockerCompose, "demo:"+version.Version) {
t.Fatalf("upgrade did not use the selected new template: %+v", preview)
}
}
}
var stored model.AppInstall
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Version != install.Version || stored.Status != install.Status {
t.Fatalf("upgrade preview modified the running instance: %+v, %v", stored, err)
}
})
}
}
func TestRemovedCurrentComposeRejectsInvalidInstanceCompose(t *testing.T) {
for _, compose := range []string{"", "services: []", "services: ["} {
t.Run(compose, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(server.Close)
db, _, install, _ := composeUpgradeDB(t, server.URL)
if err := db.Model(&install).UpdateColumn("docker_compose", compose).Error; err != nil {
t.Fatal(err)
}
if config, err := NewIAppInstalledService().GetParams(install.ID); err == nil || config != nil {
t.Fatalf("invalid instance Compose was accepted: %+v, %v", config, err)
}
})
}
}
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+115 -35
View File
@@ -12,7 +12,6 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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 {
total = int64(len(installDTOs))
}
@@ -168,6 +170,7 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
res.Version = appInstall.Version
res.CreatedAt = appInstall.CreatedAt
res.Status = appInstall.Status
res.TaskID = appInstall.TaskID
res.AppInstallID = appInstall.ID
res.IsExist = true
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 {
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 {
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
if err != nil {
@@ -269,12 +287,12 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return buserr.New("ErrInstallDirNotFound")
}
dockerComposePath := install.GetComposePath()
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
return operateAppWithLifecycleScripts(install, req, nil)
if (req.UseLifecycleScripts || install.App.Key == "ollama" || install.App.Key == "vllm") && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
return operateAppTask(install, req, nil)
}
switch req.Operate {
case constant.Rebuild:
return rebuildApp(install)
return rebuildApp(install, req.TaskID, nil)
case constant.Start:
out, err := compose.Up(dockerComposePath)
if err != nil {
@@ -303,10 +321,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
TaskID: req.TaskID,
UseLifecycleScripts: req.UseLifecycleScripts,
}
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
return err
}
return nil
return deleteAppInstall(deleteReq)
case constant.Sync:
return syncAppInstallStatus(&install, true)
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 {
taskType := task.TaskUpdate
func operateAppTask(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
var taskType string
switch req.Operate {
case constant.Start:
taskType = "Run"
install.Status = constant.StatusStarting
case constant.Restart:
taskType = task.TaskRestart
install.Status = constant.StatusRestarting
case constant.Stop:
taskType = "Stop"
install.Status = constant.StatusWaiting
default:
return errors.New("lifecycle script operation not supported")
}
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)
if err != nil {
@@ -358,20 +378,37 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
}
return err
}
ready := make(chan error, 1)
started := make(chan struct{})
operation := string(req.Operate)
operationTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
func(t *task.Task) error {
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
if req.Operate == constant.Start || req.Operate == constant.Restart {
if err := resetLocalModelAccountVerification(install); err != nil {
return err
}
close(started)
ready <- nil
}
if req.UseLifecycleScripts {
if err := runScript(t, &install, operation); err != nil {
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 {
install.Status = constant.StatusStopped
install.Message = ""
@@ -391,14 +428,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
},
nil,
0,
time.Hour,
0,
)
go func() {
if taskErr := operationTask.Execute(); taskErr != nil {
select {
case <-started:
default:
ready <- taskErr
taskErr := operationTask.Execute()
if taskErr == nil {
return
}
if onFailure != nil {
@@ -408,9 +442,8 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
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 {
@@ -435,10 +468,27 @@ func (a *AppInstallService) UpdateSort(req request.AppInstallSort) 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))
if err != nil {
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
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
if ok {
@@ -577,24 +627,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
failed.Message = operationErr.Error()
_ = 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 {
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
err = operateAppTask(installed, request.AppInstalledOperate{
InstallId: installed.ID,
Operate: constant.Restart,
TaskID: req.TaskID,
UseLifecycleScripts: true,
}, restoreConfig)
} else {
err = rebuildApp(installed)
err = rebuildApp(installed, req.TaskID, restoreConfig)
}
if err != nil {
restoreConfig(err)
return err
}
if !req.UseLifecycleScripts {
installed.Status = constant.StatusRunning
_ = appInstallRepo.Save(context.Background(), &installed)
}
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
@@ -634,9 +692,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
}
if installed.App.Key == "ollama" && installed.SyncModelAccount {
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
}
return nil
}
@@ -1062,8 +1117,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
rawCompose := install.DockerCompose
if install.App.Key != vllmAppKeyForUpgrade {
if err := ensureAppDetailCompose(install.App, &detail); err != nil {
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)
if err != nil {
return nil, err
@@ -1079,6 +1144,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
}
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
operationTask, err := taskRepo.GetExecutingTask(task.TaskScopeApp, appInstall.ID)
if err != nil {
return err
}
appInstall.TaskID = operationTask.ID
if operationTask.ID != "" {
appInstall.Status = localEngineTaskStatus(operationTask.Operate, appInstall.Status)
return nil
}
force = true
} else {
operation := ""
switch appInstall.Status {
case constant.StatusInstalling:
@@ -1089,8 +1166,10 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
operation = task.TaskBuild
case constant.StatusUninstalling:
operation = task.TaskUninstall
case constant.StatusStarting, constant.StatusWaiting:
operation = task.TaskUpdate
case constant.StatusStarting:
operation = "Run"
case constant.StatusWaiting:
operation = "Stop"
case constant.StatusRestarting:
operation = task.TaskRestart
}
@@ -1116,6 +1195,7 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
return nil
}
}
}
cli, err := docker.NewClient()
if err != nil {
return err
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
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 {
+27
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm"
)
@@ -74,3 +75,29 @@ func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string,
}
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
}
}
+13 -7
View File
@@ -92,12 +92,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
if install.Status == constant.StatusUpgrading {
if install.Status == constant.StatusUpgrading && install.App.Key != "ollama" && install.App.Key != "vllm" {
return buserr.New("TaskIsExecuting")
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting")
}
@@ -107,6 +104,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
appUpgradeLocks.Delete(install.ID)
}
}()
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
feishuLockPath := ""
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 {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeCandidate(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM upgrade version is not supported")
}
if install.Version == detail.Version {
return errors.New("two version is same")
@@ -185,7 +185,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if ctx.rollbackErr != nil {
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
upgradeTask.Task.ErrorMsg = taskErr.Error()
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
_ = taskRepo.Update(context.Background(), upgradeTask.Task)
}
if !ctx.stopAttempted || ctx.rollbackErr == nil {
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)
envs[vllmImageEnvKey] = image
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(u.detail.Version)), vllmGB10VersionPrefix) {
envs["DSPARK_VLLM_IMAGE"] = image
}
paramBytes, marshalErr := json.Marshal(envs)
if marshalErr != nil {
return marshalErr
@@ -404,6 +407,9 @@ func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.A
}
func (u *appUpgradeContext) cutover(t *task.Task) error {
if err := resetLocalModelAccountVerification(u.original); err != nil {
return err
}
u.stopAttempted = true
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
+79 -24
View File
@@ -498,7 +498,11 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
}
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() {
if err := uninstallTask.Execute(); err != nil {
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
_ = appInstallRepo.Save(context.Background(), &appInstall)
go func() {
dockerComposePath := appInstall.GetComposePath()
out, err := compose.Down(dockerComposePath)
if err != nil {
_ = handleErr(appInstall, err, out)
return
if err := appInstallRepo.Save(context.Background(), &appInstall); err != nil {
return err
}
out, err = compose.Up(appInstall.GetComposePath())
buildTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskBuild, task.TaskScopeApp, taskID, appInstall.ID)
if err != nil {
_ = handleErr(appInstall, err, out)
return
return handleErr(appInstall, err, "")
}
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)
}
if err != nil {
return handleErr(appInstall, err, out)
}
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)
if err != nil {
_ = handleErr(appInstall, err, out)
return
return handleErr(appInstall, err, out)
}
appInstall.ContainerName = strings.Join(containerNames, ",")
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
}
@@ -1477,7 +1502,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
}
appInstall.Status = constant.StatusError
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
if appInstall.App.Key == "ollama" || appInstall.App.Key == "vllm" {
_ = appInstallRepo.UpdateStatus(appInstall)
} else {
_ = appInstallRepo.Save(context.Background(), appInstall)
}
return
}
notFoundNames := make([]string, 0)
@@ -1536,7 +1565,11 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
appInstall.Message = msg
appInstall.Status = constant.StatusUnHealthy
}
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) {
@@ -1544,22 +1577,43 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res []response.AppInstallDTO
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()
if err == nil {
if err != nil {
return nil, err
}
defer cli.Close()
containers, err := cli.ListAllContainers()
if err == nil {
if err != nil {
return nil, err
}
containersMap = make(map[string]container.Summary, len(containers))
for _, contain := range containers {
containersMap[contain.Names[0]] = contain
}
}
}
}
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 {
return nil, err
}
@@ -1568,7 +1622,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
continue
}
if sync && !doNotNeedSync(installed) {
if sync && !localEngine && !doNotNeedSync(installed) {
synAppInstall(containersMap, &installed, false)
}
@@ -1582,6 +1636,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
AppDetailID: installed.AppDetailId,
Version: installed.Version,
Status: installed.Status,
TaskID: installed.TaskID,
Message: installed.Message,
HttpPort: installed.HttpPort,
HttpsPort: installed.HttpsPort,
@@ -2041,7 +2096,7 @@ func getAppTags(appID uint, lang string) ([]response.TagDTO, error) {
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 {
return hooks.PrepareSiteDir(app, appDetail, req, t)
}
+30 -15
View File
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
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++ {
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
if install.ID > 0 {
@@ -851,7 +874,7 @@ func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan
return false
case message, ok := <-messages:
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
}
// 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
go func() {
<-done
@@ -943,29 +964,23 @@ func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<-
}
}()
buffer := make([]byte, 32*1024)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
if len(line) > 0 {
line = strings.TrimSuffix(line, "\n")
n, err := stdout.Read(buffer)
if n > 0 {
select {
case messageChan <- line:
case messageChan <- string(buffer[:n]):
case <-done:
return
}
}
if err != nil {
if err == io.EOF {
break
}
errorChan <- fmt.Errorf("reader error: %v", err)
return
}
line = strings.TrimSuffix(line, "\n")
select {
case messageChan <- line:
case <-done:
return
}
}
_ = 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)
}
+5 -1
View File
@@ -163,6 +163,11 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
upgradeErrors = append(upgradeErrors, 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 {
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)
}
const (
containerUpgradePullTimeout = 30 * time.Minute
containerUpgradeReplaceTimeout = 15 * time.Minute
+7 -1
View File
@@ -414,7 +414,7 @@ func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
if showCount == 0 {
return buserr.New("ErrMinQuickJump")
}
if showCount > 4 {
if showCount > 5 {
return buserr.New("ErrMaxQuickJump")
}
@@ -709,6 +709,12 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
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":
count, err := launcherRepo.CountQuickDirectories()
if err != nil {
+1 -1
View File
@@ -3,6 +3,7 @@ package service
import "github.com/1Panel-dev/1Panel/agent/app/repo"
var (
vmRepo = repo.NewIVMRepo()
appRepo = repo.NewIAppRepo()
appTagRepo = repo.NewIAppTagRepo()
appDetailRepo = repo.NewIAppDetailRepo()
@@ -14,7 +15,6 @@ var (
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
+17
View File
@@ -24,6 +24,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"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/container"
"github.com/docker/docker/api/types/image"
@@ -35,6 +37,7 @@ import (
type ImageService struct{}
type IImageService interface {
SearchRemoteImages(ctx context.Context, req dto.ImageRemoteSearch) (dockerhub.SearchResult, error)
Page(req dto.PageImage) (int64, interface{}, error)
List() ([]dto.Options, error)
ListAll() ([]dto.ImageInfo, error)
@@ -560,3 +563,17 @@ func checkUsed(imageID string, containers []container.Summary) bool {
}
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)
}
+345 -10
View File
@@ -17,14 +17,17 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"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/constant"
"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/parser"
"github.com/docker/docker/api/types/container"
@@ -52,6 +55,7 @@ func init() {
type MCPWebsiteBindingService struct {
createWebsite func(request.WebsiteCreate) error
deleteWebsite func(request.WebsiteDelete) error
updateHTTPS func(context.Context, request.WebsiteHTTPSOp) error
applyInclude func(model.Website) error
removeInclude func(model.Website) error
@@ -63,6 +67,7 @@ type MCPWebsiteBindingService struct {
func NewMCPWebsiteBindingService() *MCPWebsiteBindingService {
s := &MCPWebsiteBindingService{
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 {
if req.Enable {
current, err := NewIWebsiteService().GetWebsiteHTTPS(req.WebsiteID)
@@ -148,6 +153,14 @@ type mcpWebsiteOwner struct {
RequestID string `json:"requestId"`
RequestHash string `json:"requestHash"`
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 {
@@ -236,6 +249,9 @@ func mcpWebsiteFailure(reason string, definitive, conflict bool) response.MCPWeb
if conflict {
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}
}
@@ -461,30 +477,248 @@ func mcpWebsiteReadProxy(path string) ([]byte, error) {
}
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 {
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 {
host, port, err := mcpWebsiteDomain(req.Domain)
if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID {
return false
}
if port == 0 {
explicitPort := port != 0
if !explicitPort || req.EnableSSL {
installed, installErr := s.openresty()
if installErr != nil {
return false
}
if !explicitPort {
port = installed.HttpPort
if req.EnableSSL {
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 {
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
}
}
@@ -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" {
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" {
return response.MCPWebsiteBinding{State: "unbound"}
}
@@ -639,6 +876,17 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
site, err := s.load(req)
if errors.Is(err, gorm.ErrRecordNotFound) {
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" {
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" {
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()
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
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)
}
domain, port, _ := mcpWebsiteDomain(req.Domain)
explicitPort := port != 0
if port == 0 {
port = installed.HttpPort
}
domainSSL := req.EnableSSL && explicitPort
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port, SSL: domainSSL}}}
if req.EnableSSL {
port = installed.HttpsPort
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 {
var business buserr.BusinessError
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
return mcpWebsiteFailure("domain_conflict", true, true)
} else if conflictErr != nil && !errors.Is(conflictErr, gorm.ErrRecordNotFound) {
}
return mcpWebsiteFailure("website_unavailable", false, false)
}
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
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)
}
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)
}
if ownerErr != nil {
if req.DeleteWebsite {
return mcpWebsiteFailure("website_ownership_changed", false, true)
}
if (req.Operation != "bind" && req.Operation != "unbind") || site.Remark != mcpWebsiteRemark(req) {
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) {
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" {
return s.status(req, site, owner)
}
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.RequestHash != operationHash {
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)
}
}
if req.Operation == "bind" && owner.BindHash != mcpWebsiteBindHash(req) {
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) {
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)
}
}
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
mutationStarted = true
if err = mcpWebsiteWriteOwner(site, owner); err != nil {
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)
}
if req.Operation == "update" {
+65 -59
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -9,6 +10,7 @@ import (
"strings"
"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/compose-spec/compose-go/v2/interpolation"
"github.com/compose-spec/compose-go/v2/template"
@@ -16,77 +18,73 @@ import (
func LoadModelGPUDevices() dto.ModelGPUDevices {
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern)
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern)
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern)
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} {
ok, client := accelerator.New()
if !ok {
return result
}
snapshot, err := client.Collect(context.Background())
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
return result
}
result.Warnings = append(result.Warnings, snapshot.Info.Warnings...)
for _, monitored := range snapshot.Devices {
if monitored.ParentID != "" {
continue
}
device := dto.ModelGPUDevice{Available: true}
devicePath := ""
switch {
case monitored.GPU != nil:
if monitored.Vendor == "nvidia" && !re.GetRegex(re.NvidiaGPUUUIDPattern).MatchString(monitored.GPU.UUID) {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID from nvidia-smi: %s", monitored.GPU.UUID))
continue
}
device.ID = monitored.ID
device.Name = fmt.Sprintf("GPU %d · %s", monitored.Index, monitored.Name)
device.Type = monitored.Vendor
device.UUID = monitored.GPU.UUID
case monitored.XPU != nil:
devicePath = monitored.XPU.Basic.DrmDevice
if !re.GetRegex(re.IntelGPUDevicePathPattern).MatchString(devicePath) {
directory := filepath.Join("/sys/bus/pci/devices", filepath.Base(monitored.BusID), "drm")
entries, err := os.ReadDir(directory)
if err != nil {
if !os.IsNotExist(err) {
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
}
devicePath = ""
for _, entry := range entries {
var device dto.ModelGPUDevice
var devicePath string
switch directory {
case "/proc/driver/nvidia/gpus":
informationPath := filepath.Join(directory, entry.Name(), "information")
content, err := os.ReadFile(informationPath)
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
fields := map[string]string{}
for _, line := range strings.Split(string(content), "\n") {
key, value, found := strings.Cut(line, ":")
if found {
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
uuid := fields["GPU UUID"]
minor, err := strconv.Atoi(fields["Device Minor"])
if !uuidPattern.MatchString(uuid) || err != nil || minor < 0 {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID or device minor in %s", informationPath))
continue
}
name := fields["Model"]
if name == "" {
name = "NVIDIA"
}
device = dto.ModelGPUDevice{ID: "nvidia:" + uuid, Name: name + " · " + entry.Name(), Type: "nvidia", UUID: uuid}
devicePath = filepath.Join("/dev", "nvidia"+strconv.Itoa(minor))
case "/sys/class/drm":
if !renderPattern.MatchString(entry.Name()) {
continue
}
vendor, err := os.ReadFile(filepath.Join(directory, entry.Name(), "device/vendor"))
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
if strings.TrimSpace(string(vendor)) != "0x8086" {
continue
}
if re.GetRegex(re.IntelGPURenderNamePattern).MatchString(entry.Name()) {
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()) {
break
}
}
if devicePath == "" {
result.Warnings = append(result.Warnings, fmt.Sprintf("Intel GPU %d has no render device", monitored.Index))
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)
device.Available = err == nil && info.Mode()&os.ModeCharDevice != 0
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
} else {
device.Available = info.Mode()&os.ModeCharDevice != 0
}
}
result.Devices = append(result.Devices, device)
}
}
return result
}
@@ -649,9 +647,17 @@ func resolveModelGPUValue(value string, params map[string]interface{}) (string,
func loadModelAscendDeviceIDs() []string {
ids := []string{}
for _, device := range LoadModelGPUDevices().Devices {
if device.Type == "ascend" && device.Available {
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:"))
entries, err := os.ReadDir("/dev")
if err != nil {
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 {
+5
View File
@@ -286,6 +286,11 @@ func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous
return point, err
}
point.CreatedAt = time.Now()
if len(metrics) == 0 {
point.Status = "no_data"
point.RawMetrics = "{}"
return point, nil
}
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
+17 -1
View File
@@ -45,6 +45,10 @@ var (
// nginxHTTPBlockStartRe locates the http block opening, the fallback
// insertion point, and captures its indentation.
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.
@@ -96,7 +100,19 @@ func insertNginxHTTPInclude(content string) (string, error) {
indent := content[m[2]:m[3]] + " "
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
+8 -1
View File
@@ -1,6 +1,7 @@
package service
import (
"errors"
"strings"
"testing"
)
@@ -83,9 +84,15 @@ func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(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")
}
// 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) {
+35 -23
View File
@@ -224,12 +224,9 @@ func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
}
res := &response.NginxBrotliRes{
ManagedExternally: managedExternally,
// Without the include, values the panel would write would never reach
// nginx, so they are reported as unavailable rather than shown as if
// they were in effect.
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
res := &response.NginxBrotliRes{ManagedExternally: managedExternally}
if !managedExternally {
res.ManagedPending, res.ManagedUnavailable = nginxBrotliManagedState(install)
}
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
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
}
// 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
// in any of the files nginx loads them from.
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
@@ -313,26 +332,19 @@ func updateNginxBrotliParams(params []dto.NginxParam) error {
return updateUserNginxBrotliParams(install, values)
}
// A managed write needs the include. Installations missing it are
// upgraded in place here; when nginx.conf cannot be edited safely the
// write is refused with an actionable error instead of writing values
// nginx would never load.
if !nginxHTTPIncludePresent(install) {
configPath := nginxMainConfigPath(install)
content, readErr := os.ReadFile(configPath)
if readErr != nil {
return readErr
}
updated, insErr := insertNginxHTTPInclude(string(content))
if insErr != nil {
// upgraded in place here, through the same path that enabling the
// module takes; when nginx.conf cannot be edited safely the write is
// refused with an actionable error instead of writing values nginx
// would never load.
_, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
if includeErr != nil {
if errors.Is(includeErr, errNginxHTTPIncludeNoInsertionPoint) {
return buserr.New("ErrBrotliUnsupported")
}
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
return err
return includeErr
}
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
return err
}
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
if confSnapshot != nil {
if err = nginxCheckAndReload(string(confSnapshot), nginxMainConfigPath(install), install.ContainerName); err != nil {
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")
}
}
+53 -1
View File
@@ -1,13 +1,22 @@
package service
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/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/global"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
var phpExtensionsMu sync.Mutex
type PHPExtensionsService struct {
}
@@ -17,6 +26,9 @@ type IPHPExtensionsService interface {
Create(req request.PHPExtensionsCreate) error
Update(req request.PHPExtensionsUpdate) 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 {
@@ -30,7 +42,7 @@ func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []re
err error
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 {
return 0, nil, err
}
@@ -61,6 +73,8 @@ func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) {
}
func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
exist, _ := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
if exist.ID > 0 {
return buserr.New("ErrNameIsExist")
@@ -73,6 +87,8 @@ func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
}
func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
exist, err := phpExtensionsRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@@ -82,5 +98,41 @@ func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
}
func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
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)
}
-445
View File
@@ -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 ""
}
+12 -1
View File
@@ -58,6 +58,9 @@ func resolveVllmVersionFamily(version, image string) string {
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
if strings.HasPrefix(normalized, vllmGB10VersionPrefix) {
return strings.TrimSpace(trimmed[len(vllmGB10VersionPrefix):])
}
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
@@ -69,6 +72,9 @@ func trimVllmVersionFamily(version string) string {
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix) {
return "1panel/vllm-gb10-dspark:" + tag
}
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
@@ -83,6 +89,11 @@ func buildDefaultVllmImageByVersion(version string) string {
}
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)
targetFamily := resolveVllmVersionFamily(targetVersion, "")
return currentFamily == targetFamily
@@ -102,7 +113,7 @@ func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string)
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
return false
}
if common.CompareVersion(targetVersion, currentVersion) {
if common.CompareVersion(trimVllmVersionFamily(targetVersion), trimVllmVersionFamily(currentVersion)) {
return true
}
return !hasVllmVersionFamilyPrefix(currentVersion) &&
+171
View File
@@ -0,0 +1,171 @@
package service
import (
"os"
"path/filepath"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/jinzhu/copier"
)
func (v *VMService) SearchIso(req dto.SearchPageWithType) (int64, interface{}, error) {
options := []repo.DBOption{repo.WithOrderDesc("created_at"), repo.WithOrderDesc("id")}
if req.Type != "" {
options = append(options, repo.WithByType(req.Type))
}
if req.Info != "" {
options = append(options, repo.WithByLikeName(req.Info))
}
total, isos, err := vmRepo.PageIso(req.Page, req.PageSize, options...)
if err != nil {
return 0, nil, err
}
dtoIsos := make([]dto.VMIsoInfo, 0, len(isos))
for _, iso := range isos {
var item dto.VMIsoInfo
if err := copier.Copy(&item, &iso); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
}
item.IsExist, item.Size = loadVMResourceFileInfo(item.Path)
dtoIsos = append(dtoIsos, item)
}
return total, dtoIsos, err
}
func (v *VMService) ListIso() ([]dto.VMIsoInfo, error) {
isos, err := vmRepo.ListIsos()
if err != nil {
return nil, err
}
dtoIsos := make([]dto.VMIsoInfo, 0, len(isos))
for _, iso := range isos {
if iso.Status == "uploading" || iso.Status == "downloading" || iso.Status == "failed" {
continue
}
var item dto.VMIsoInfo
if err := copier.Copy(&item, &iso); err != nil {
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
}
item.IsExist, item.Size = loadVMResourceFileInfo(item.Path)
dtoIsos = append(dtoIsos, item)
}
return dtoIsos, nil
}
func (v *VMService) CreateIso(req dto.VMIsoCreate) error {
isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name))
if isoItem.ID != 0 {
return buserr.New("ErrRecordExist")
}
exists, _ := loadVMResourceFileInfo(req.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
return vmRepo.CreateIso(&model.VMIso{
Type: req.Type, Name: req.Name, Path: req.Path, Status: "ready", Description: req.Description,
})
}
func (v *VMService) StartIsoImport(req dto.VMIsoImportStart) (uint, error) {
isoItem, _ := vmRepo.GetIso(repo.WithByName(req.Name))
if isoItem.ID != 0 {
return 0, buserr.New("ErrRecordExist")
}
item := model.VMIso{Type: req.Type, Name: req.Name, Path: req.Path, Status: req.Status, TempFileID: req.TempFileID, TempFileType: req.TempFileType, Description: req.Description}
if err := vmRepo.CreateIso(&item); err != nil {
return 0, err
}
return item.ID, nil
}
func (v *VMService) UpdateIsoImport(req dto.VMIsoImportUpdate) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil || isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if isoItem.Status != "uploading" && isoItem.Status != "downloading" {
return buserr.WithDetail("ErrVMInvalidInput", "ISO import is not active", nil)
}
if (isoItem.Status == "uploading" && req.Status == "downloading") ||
(isoItem.Status == "downloading" && req.Status == "uploading") {
return buserr.WithDetail("ErrVMInvalidInput", "ISO import type cannot be changed", nil)
}
vars := map[string]interface{}{"status": req.Status, "error": req.Error}
if req.TempFileID != "" {
vars["temp_file_id"] = req.TempFileID
}
if req.TempFileType != "" {
vars["temp_file_type"] = req.TempFileType
}
if req.Status == "ready" {
exists, _ := loadVMResourceFileInfo(isoItem.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
vars["error"] = ""
}
if req.Status == "ready" {
vars["temp_file_id"] = ""
vars["temp_file_type"] = ""
}
return vmRepo.UpdateIso(req.ID, vars)
}
func (v *VMService) UpdateIso(req dto.VMIsoUpdate) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil {
return err
}
if isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
exists, _ := loadVMResourceFileInfo(req.Path)
if !exists {
return buserr.WithDetail("ErrResourceNotExists", "Resource file does not exist", nil)
}
return vmRepo.UpdateIso(req.ID, map[string]interface{}{
"name": req.Name, "type": req.Type, "path": req.Path, "description": req.Description,
})
}
func (v *VMService) DeleteIso(req dto.VMIsoDelete) error {
isoItem, err := vmRepo.GetIso(repo.WithByID(req.ID))
if err != nil {
return err
}
if isoItem.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if req.DeleteFile && isoItem.Path != "" {
exists, _ := loadVMResourceFileInfo(isoItem.Path)
if exists {
if err := os.Remove(isoItem.Path); err != nil && !os.IsNotExist(err) {
return err
}
}
}
if isoItem.TempFileType == "upload" && validTempFileID(isoItem.TempFileID) {
tmpPath := filepath.Join(global.CONF.Base.InstallDir, "1panel", "tmp", "upload", isoItem.TempFileID)
if err := os.RemoveAll(tmpPath); err != nil {
return err
}
}
return vmRepo.DeleteIso(repo.WithByID(req.ID))
}
func loadVMResourceFileInfo(filePath string) (bool, int64) {
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
return false, 0
}
return true, info.Size()
}
func validTempFileID(value string) bool {
return value != "" && filepath.Base(value) == value && !strings.ContainsAny(value, `/\\`)
}
+464
View File
@@ -0,0 +1,464 @@
package service
import (
"context"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/vm/types"
"gorm.io/gorm"
)
func (v *VMService) ListNetworks() ([]dto.VMNetworkItem, error) {
records, err := vmRepo.ListNetworks()
if err != nil {
return nil, err
}
items := v.networkRecordsToItems(records)
return items, nil
}
func (v *VMService) ListNetworkBridges() ([]dto.VMNetworkBridgeItem, error) {
return listNetworkBridges("/sys/class/net")
}
func (v *VMService) ListNetworkInterfaces() ([]dto.VMNetworkInterfaceItem, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
return listMacvtapInterfaces("/sys/class/net", interfaces)
}
func (v *VMService) CreateNetwork(req dto.VMNetworkCreate) error {
if req.Type == "macvtap" {
interfaces, err := v.ListNetworkInterfaces()
if err != nil {
return err
}
found := false
for _, item := range interfaces {
if item.Name == req.SourceInterface {
found = true
break
}
}
if !found {
return fmt.Errorf("unavailable macvtap source interface: %s", req.SourceInterface)
}
} else {
req.SourceInterface = ""
}
if req.Type != "nat" {
req.CIDR, req.Gateway, req.DHCPStart, req.DHCPEnd = "", "", "", ""
}
if req.Type == "macvtap" {
req.BridgeName = ""
}
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
unlock, err := vmResourceLocks.lock(ctx, "network", req.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByName(req.Name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if itemNetwork.ID != 0 {
return buserr.New("ErrRecordExist")
}
if req.Type == "bridge" && isManagedNetworkBridge(req.BridgeName) {
return buserr.New("ErrVMBridgeManaged")
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
if _, exists, err := v.findVMResourceWithContext(ctx, "network", req.Name); err != nil {
return err
} else if exists {
return buserr.New("ErrRecordExist")
}
record := model.VMNetwork{
Name: req.Name, Type: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description,
}
if err := vmRepo.CreateNetwork(&record); err != nil {
return err
}
if err := v.provider.CreateResource(ctx, types.VMResourceCreate{
Type: "network", Name: req.Name, SubType: req.Type, BridgeName: req.BridgeName, SourceInterface: req.SourceInterface,
CIDR: req.CIDR, Gateway: req.Gateway, DHCPStart: req.DHCPStart, DHCPEnd: req.DHCPEnd,
AutoStart: req.AutoStart,
}); err != nil {
if updateErr := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusFailed, "error": err.Error(),
}); updateErr != nil {
return errors.Join(err, fmt.Errorf("save network creation failure: %w", updateErr))
}
return err
}
return vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusRunning, "error": "",
})
}
func (v *VMService) SyncNetworks() error {
revision, idle := vmResourceLocks.snapshot("network")
if !idle {
return nil
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(context.Background(), "network")
if err != nil {
return err
}
return vmResourceLocks.applySnapshot("network", revision, func() error {
records, err := vmRepo.ListNetworks()
if err != nil {
return err
}
liveByName := map[string]dto.VMResourceInfo{}
for _, live := range liveItems {
if live.Name == "" {
continue
}
liveByName[live.Name] = live
}
recordsByName := map[string]model.VMNetwork{}
for _, record := range records {
if vmResourceStatusEquals(record.Status, vmResourceCreating) {
record.Status = constant.StatusFailed
record.Error = "Network creation was interrupted before its result was saved"
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": record.Status, "error": record.Error,
}); err != nil {
return err
}
}
recordsByName[record.Name] = record
}
for name, live := range liveByName {
vars := map[string]interface{}{
"type": live.SubType,
"bridge_name": live.BridgeName,
"source_interface": live.SourceInterface,
"cidr": live.CIDR,
"gateway": live.Gateway,
"dhcp_start": live.DHCPStart,
"dhcp_end": live.DHCPEnd,
"auto_start": live.AutoStart,
"status": normalizeVMResourceStatus(live.Status),
}
if record, ok := recordsByName[name]; ok {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if err := vmRepo.UpdateNetwork(record.ID, vars); err != nil {
return err
}
continue
}
if err := vmRepo.CreateNetwork(&model.VMNetwork{
Name: live.Name, Type: live.SubType, BridgeName: live.BridgeName, SourceInterface: live.SourceInterface,
CIDR: live.CIDR, Gateway: live.Gateway, DHCPStart: live.DHCPStart, DHCPEnd: live.DHCPEnd,
AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status),
}); err != nil {
return err
}
}
for _, record := range recordsByName {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if _, ok := liveByName[record.Name]; !ok {
if err := vmRepo.UpdateNetwork(record.ID, map[string]interface{}{
"status": constant.StatusDeleted,
}); err != nil {
return err
}
}
}
return nil
})
}
func (v *VMService) UpdateNetwork(req dto.VMNetworkUpdate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
unlock, err := vmResourceLocks.lock(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if itemNetwork.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
vars := map[string]interface{}{}
if req.Description != nil {
vars["description"] = *req.Description
}
if req.AutoStart != nil || req.Status != nil {
if vmResourceCreationIncomplete(itemNetwork.Status) {
return buserr.WithDetail("ErrInvalidParams", "network creation has not succeeded", nil)
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
live, ok, err := v.findVMResourceWithContext(ctx, "network", itemNetwork.Name)
if err != nil {
return err
}
if !ok {
_ = vmRepo.UpdateNetwork(req.ID, map[string]interface{}{"status": constant.StatusDeleted})
return buserr.WithDetail("ErrResourceNotExists", "Network does not exist", nil)
}
if live.Status != "" {
vars["status"] = normalizeVMResourceStatus(live.Status)
}
}
if req.AutoStart != nil {
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, AutoStart: req.AutoStart,
}); err != nil {
return err
}
vars["auto_start"] = *req.AutoStart
}
if req.Status != nil {
status := normalizeVMResourceStatus(*req.Status)
if !validVMResourceOperateStatus(status) {
return buserr.WithDetail("ErrInvalidParams", status, nil)
}
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "network", Name: itemNetwork.Name, Status: &status,
}); err != nil {
return err
}
vars["status"] = status
}
if len(vars) == 0 {
return nil
}
return vmRepo.UpdateNetwork(req.ID, vars)
}
func (v *VMService) DeleteNetwork(req dto.VMResourceDelete) error {
ctx, cancel := vmResourceOperationContext(req.Force)
defer cancel()
itemNetwork, err := vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrRecordNotFound")
}
return err
}
unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "network", itemNetwork.Name, req.Force)
if err != nil {
return err
}
defer unlock()
itemNetwork, err = vmRepo.GetNetwork(repo.WithByID(req.ID))
if err != nil {
return err
}
if len(loadVMResourceRefs("network")[itemNetwork.Name]) > 0 {
return buserr.New("ErrVMNetworkInUse")
}
if cleanup {
if err := v.deleteVMResource(ctx, "network", itemNetwork.Name, req.Force); err != nil {
return err
}
}
return vmRepo.DeleteNetwork(repo.WithByID(req.ID))
}
func (v *VMService) networkRecordsToItems(records []model.VMNetwork) []dto.VMNetworkItem {
items := make([]dto.VMNetworkItem, 0, len(records))
vmRefs := loadVMResourceRefs("network")
usedIPs := v.loadVMNetworkIPUsages(records)
for _, record := range records {
status := normalizeVMResourceStatus(record.Status)
items = append(items, dto.VMNetworkItem{
ID: record.ID, Name: record.Name, Type: record.Type,
BridgeName: record.BridgeName, SourceInterface: record.SourceInterface, CIDR: record.CIDR, Gateway: record.Gateway,
DHCPStart: record.DHCPStart, DHCPEnd: record.DHCPEnd, AutoStart: record.AutoStart,
Description: record.Description, CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
Status: status, Error: record.Error, VMCount: len(vmRefs[record.Name]), VMs: vmRefs[record.Name],
Registered: true, UsedIPs: usedIPs[record.Name],
})
}
return items
}
func (v *VMService) loadVMNetworkIPUsages(records []model.VMNetwork) map[string][]dto.VMNetworkIPUsage {
usages := map[string][]dto.VMNetworkIPUsage{}
if !v.helperExists() {
return usages
}
domains, err := v.provider.List(context.Background())
if err != nil {
return usages
}
vmRecords, err := vmRepo.List()
if err != nil {
return usages
}
vmRecordsByName := map[string]model.VirtualMachine{}
for _, record := range vmRecords {
vmRecordsByName[record.Name] = record
}
networkNames := map[string]struct{}{}
bridgeToNetwork := map[string]string{}
for _, record := range records {
networkNames[record.Name] = struct{}{}
if record.BridgeName != "" {
bridgeToNetwork[record.BridgeName] = record.Name
}
}
for _, domain := range domains {
if record, ok := vmRecordsByName[domain.Name]; ok {
mergeVMMetadata(&domain, record)
}
for _, item := range domain.Interfaces {
ip := strings.TrimSpace(item.IP)
if ip == "" {
continue
}
networkName := resolveVMInterfaceNetwork(item.Source, domain.Network, networkNames, bridgeToNetwork)
if networkName == "" {
continue
}
usages[networkName] = append(usages[networkName], dto.VMNetworkIPUsage{
IP: ip, VMName: domain.Name, MAC: item.MAC, Source: item.Source,
})
}
}
for name := range usages {
sort.Slice(usages[name], func(i, j int) bool {
if usages[name][i].IP != usages[name][j].IP {
return usages[name][i].IP < usages[name][j].IP
}
return strings.ToLower(usages[name][i].VMName) < strings.ToLower(usages[name][j].VMName)
})
}
return usages
}
func resolveVMInterfaceNetwork(source, vmNetwork string, networkNames map[string]struct{}, bridgeToNetwork map[string]string) string {
if _, ok := networkNames[source]; ok {
return source
}
if _, ok := networkNames[vmNetwork]; ok {
return vmNetwork
}
return bridgeToNetwork[source]
}
func listNetworkBridges(netClassPath string) ([]dto.VMNetworkBridgeItem, error) {
entries, err := os.ReadDir(netClassPath)
if err != nil {
if os.IsNotExist(err) {
return []dto.VMNetworkBridgeItem{}, nil
}
return nil, err
}
items := make([]dto.VMNetworkBridgeItem, 0)
for _, entry := range entries {
name := entry.Name()
netPath := netClassPath + "/" + name
info, err := os.Stat(netPath)
if err != nil || !info.IsDir() {
continue
}
if _, err := os.Stat(netPath + "/bridge"); err != nil {
continue
}
item := dto.VMNetworkBridgeItem{
Name: name,
Recommended: true,
}
switch {
case name == "docker0" || strings.HasPrefix(name, "br-"):
item.Recommended = false
item.Description = "docker"
case name == "virbr0" || strings.HasPrefix(name, "virbr"):
item.Recommended = false
item.Description = "libvirt"
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Recommended != items[j].Recommended {
return items[i].Recommended
}
return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
})
return items, nil
}
func isManagedNetworkBridge(name string) bool {
return name == "docker0" || strings.HasPrefix(name, "br-") || name == "virbr0" || strings.HasPrefix(name, "virbr")
}
func listMacvtapInterfaces(netClassPath string, interfaces []net.Interface) ([]dto.VMNetworkInterfaceItem, error) {
items := make([]dto.VMNetworkInterfaceItem, 0)
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) != 6 {
continue
}
netPath := filepath.Join(netClassPath, iface.Name)
if _, err := os.Stat(filepath.Join(netPath, "device")); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
continue
}
excluded := false
for _, name := range []string{"master", "wireless", "bridge"} {
if _, err := os.Stat(filepath.Join(netPath, name)); err == nil {
excluded = true
break
} else if !os.IsNotExist(err) {
return nil, err
}
}
if excluded {
continue
}
addresses, err := iface.Addrs()
if err != nil {
return nil, err
}
item := dto.VMNetworkInterfaceItem{Name: iface.Name, Addresses: []string{}}
for _, address := range addresses {
item.Addresses = append(item.Addresses, address.String())
}
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
return items, nil
}
+36
View File
@@ -0,0 +1,36 @@
package service
import (
"context"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
)
func (v *VMService) deleteVMResource(ctx context.Context, resourceType, name string, force bool) error {
if err := v.deleteVMResourceWithContext(ctx, resourceType, name); err != nil {
if !force {
return err
}
if global.LOG != nil {
global.LOG.Warnf("[xpack] force delete VM %s %q metadata after resource cleanup failed: %v", resourceType, name, err)
}
}
return nil
}
func (v *VMService) deleteVMResourceWithContext(ctx context.Context, resourceType, name string) error {
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
items, err := v.provider.ListResources(ctx, resourceType)
if err != nil {
return err
}
for _, item := range items {
if item.Name == name {
return v.provider.DeleteResource(ctx, resourceType, name)
}
}
return nil
}
+111
View File
@@ -0,0 +1,111 @@
package service
import (
"context"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
)
type vmResourceKey struct{ kind, name string }
type vmResourceGate struct {
token chan struct{}
refs int
}
type vmResourceLockSet struct {
mu sync.Mutex
gates map[vmResourceKey]*vmResourceGate
active map[string]int
revision map[string]uint64
}
var vmResourceLocks = newVMResourceLockSet()
func newVMResourceLockSet() *vmResourceLockSet {
return &vmResourceLockSet{gates: make(map[vmResourceKey]*vmResourceGate), active: make(map[string]int), revision: make(map[string]uint64)}
}
func vmResourceOperationContext(force bool) (context.Context, context.CancelFunc) {
timeout := 2 * time.Minute
if force {
timeout = 10 * time.Second
}
return context.WithTimeout(context.Background(), timeout)
}
func (s *vmResourceLockSet) lock(ctx context.Context, kind, name string) (func(), error) {
key := vmResourceKey{kind, name}
s.mu.Lock()
gate := s.gates[key]
if gate == nil {
gate = &vmResourceGate{token: make(chan struct{}, 1)}
gate.token <- struct{}{}
s.gates[key] = gate
}
gate.refs++
s.mu.Unlock()
select {
case <-ctx.Done():
s.mu.Lock()
s.dropRef(key, gate)
s.mu.Unlock()
return nil, ctx.Err()
case <-gate.token:
}
s.mu.Lock()
if err := ctx.Err(); err != nil {
gate.token <- struct{}{}
s.dropRef(key, gate)
s.mu.Unlock()
return nil, err
}
s.active[kind]++
s.revision[kind]++
s.mu.Unlock()
return func() {
s.mu.Lock()
s.active[kind]--
s.revision[kind]++
gate.token <- struct{}{}
s.dropRef(key, gate)
s.mu.Unlock()
}, nil
}
func (s *vmResourceLockSet) dropRef(key vmResourceKey, gate *vmResourceGate) {
gate.refs--
if gate.refs == 0 {
delete(s.gates, key)
}
}
func (s *vmResourceLockSet) lockForDelete(ctx context.Context, kind, name string, force bool) (func(), bool, error) {
unlock, err := s.lock(ctx, kind, name)
if err == nil {
return unlock, true, nil
}
if !force {
return nil, false, err
}
if global.LOG != nil {
global.LOG.Warnf("[xpack] force delete VM %s %q metadata after waiting for resource lock failed: %v", kind, name, err)
}
s.mu.Lock()
s.revision[kind]++
return func() { s.revision[kind]++; s.mu.Unlock() }, false, nil
}
func (s *vmResourceLockSet) snapshot(kind string) (uint64, bool) {
s.mu.Lock()
defer s.mu.Unlock()
return s.revision[kind], s.active[kind] == 0
}
func (s *vmResourceLockSet) applySnapshot(kind string, revision uint64, apply func() error) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.active[kind] != 0 || s.revision[kind] != revision {
return nil
}
defer func() { s.revision[kind]++ }()
return apply()
}
+55
View File
@@ -0,0 +1,55 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
)
const vmResourceCreating = "creating"
func vmResourceCreationIncomplete(status string) bool {
return vmResourceStatusEquals(status, vmResourceCreating) || vmResourceStatusEquals(status, constant.StatusFailed)
}
func normalizeVMResourceStatus(status string) string {
status = strings.TrimSpace(status)
if status == "" {
return constant.StatusUnknown
}
switch strings.ToLower(status) {
case vmResourceCreating:
return vmResourceCreating
case strings.ToLower(constant.StatusFailed):
return constant.StatusFailed
case strings.ToLower(constant.StatusRunning):
return constant.StatusRunning
case strings.ToLower(constant.StatusInactive):
return constant.StatusInactive
case strings.ToLower(constant.StatusDeleted):
return constant.StatusDeleted
case strings.ToLower(constant.StatusUnknown):
return constant.StatusUnknown
case strings.ToLower(constant.StatusBuilding):
return constant.StatusBuilding
case strings.ToLower(constant.StatusDegraded):
return constant.StatusDegraded
case strings.ToLower(constant.StatusInaccessible):
return constant.StatusInaccessible
default:
return status
}
}
func vmResourceStatusEquals(status string, target string) bool {
return strings.EqualFold(strings.TrimSpace(status), target)
}
func vmResourceExists(status string) bool {
status = normalizeVMResourceStatus(status)
return !vmResourceStatusEquals(status, constant.StatusDeleted)
}
func validVMResourceOperateStatus(status string) bool {
return vmResourceStatusEquals(status, constant.StatusRunning) || vmResourceStatusEquals(status, constant.StatusInactive)
}
+403
View File
@@ -0,0 +1,403 @@
package service
import (
"context"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/vm/types"
"gorm.io/gorm"
)
func (v *VMService) ListStorages() ([]dto.VMStorageItem, error) {
records, err := vmRepo.ListStorages()
if err != nil {
return nil, err
}
items := v.mergeStorageItems(records)
return items, nil
}
func (v *VMService) CreateStorage(req dto.VMStorageCreate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
unlock, err := vmResourceLocks.lock(ctx, "storagePool", req.Name)
if err != nil {
return err
}
defer unlock()
req.Path = strings.TrimSpace(req.Path)
unlockPath, err := vmResourceLocks.lock(ctx, "storagePath", normalizeStoragePath(req.Path))
if err != nil {
return err
}
defer unlockPath()
itemStorage, err := vmRepo.GetStorage(repo.WithByName(req.Name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if itemStorage.ID != 0 {
return buserr.New("ErrRecordExist")
}
records, err := vmRepo.ListStorages()
if err != nil {
return err
}
for _, record := range records {
if sameStoragePath(record.Path, req.Path) {
return buserr.WithName("ErrVMStoragePathInUse", record.Name)
}
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(ctx, "storagePool")
if err != nil {
return err
}
for _, live := range liveItems {
if live.Name == req.Name {
return buserr.New("ErrRecordExist")
}
if sameStoragePath(live.Path, req.Path) {
return buserr.WithName("ErrVMStoragePathInUse", live.Name)
}
}
record := model.VMStorage{
Name: req.Name, Type: req.Type, Path: req.Path,
AutoStart: req.AutoStart, Status: vmResourceCreating, Description: req.Description,
}
if err := vmRepo.CreateStorage(&record); err != nil {
return err
}
if err := v.provider.CreateResource(ctx, types.VMResourceCreate{
Type: "storagePool", Name: req.Name, SubType: req.Type, Path: req.Path, AutoStart: req.AutoStart,
}); err != nil {
if updateErr := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusFailed, "error": err.Error(),
}); updateErr != nil {
return errors.Join(err, fmt.Errorf("save storage pool creation failure: %w", updateErr))
}
return err
}
return vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusRunning, "error": "",
})
}
func sameStoragePath(left, right string) bool {
left = normalizeStoragePath(left)
right = normalizeStoragePath(right)
return left != "" && left == right
}
func normalizeStoragePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
return filepath.Clean(path)
}
func (v *VMService) SyncStorages() error {
revision, idle := vmResourceLocks.snapshot("storagePool")
if !idle {
return nil
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
liveItems, err := v.provider.ListResources(context.Background(), "storagePool")
if err != nil {
return err
}
return vmResourceLocks.applySnapshot("storagePool", revision, func() error {
records, err := vmRepo.ListStorages()
if err != nil {
return err
}
liveByName := map[string]dto.VMResourceInfo{}
for _, live := range liveItems {
if live.Name == "" {
continue
}
liveByName[live.Name] = live
}
recordsByName := map[string]model.VMStorage{}
for _, record := range records {
if vmResourceStatusEquals(record.Status, vmResourceCreating) {
record.Status = constant.StatusFailed
record.Error = "Storage pool creation was interrupted before its result was saved"
if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": record.Status, "error": record.Error,
}); err != nil {
return err
}
}
recordsByName[record.Name] = record
}
for name, live := range liveByName {
vars := map[string]interface{}{
"type": live.SubType,
"path": live.Path,
"auto_start": live.AutoStart,
"status": normalizeVMResourceStatus(live.Status),
}
if record, ok := recordsByName[name]; ok {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if err := vmRepo.UpdateStorage(record.ID, vars); err != nil {
return err
}
continue
}
if err := vmRepo.CreateStorage(&model.VMStorage{
Name: live.Name, Type: live.SubType, Path: live.Path,
AutoStart: live.AutoStart, Status: normalizeVMResourceStatus(live.Status),
}); err != nil {
return err
}
}
for _, record := range recordsByName {
if vmResourceStatusEquals(record.Status, constant.StatusFailed) {
continue
}
if _, ok := liveByName[record.Name]; !ok {
if err := vmRepo.UpdateStorage(record.ID, map[string]interface{}{
"status": constant.StatusDeleted,
}); err != nil {
return err
}
}
}
return nil
})
}
func (v *VMService) UpdateStorage(req dto.VMStorageUpdate) error {
ctx, cancel := vmResourceOperationContext(false)
defer cancel()
itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
unlock, err := vmResourceLocks.lock(ctx, "storagePool", itemStorage.Name)
if err != nil {
return err
}
defer unlock()
itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
if itemStorage.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
vars := map[string]interface{}{}
if req.Name != "" && req.Name != itemStorage.Name {
return buserr.New("ErrVMStorageNameCannotChange")
}
if req.Path != "" && req.Path != itemStorage.Path {
return buserr.New("ErrVMStoragePathCannotChange")
}
if req.Description != nil {
vars["description"] = *req.Description
}
if req.AutoStart != nil || req.Status != nil {
if vmResourceCreationIncomplete(itemStorage.Status) {
return buserr.WithDetail("ErrInvalidParams", "storage pool creation has not succeeded", nil)
}
if !v.helperExists() {
return buserr.New("ErrVMHelperNotFound")
}
live, ok, err := v.findVMResourceWithContext(ctx, "storagePool", itemStorage.Name)
if err != nil {
return err
}
if !ok {
_ = vmRepo.UpdateStorage(req.ID, map[string]interface{}{"status": constant.StatusDeleted})
return buserr.WithDetail("ErrResourceNotExists", "Storage pool does not exist", nil)
}
if live.Status != "" {
vars["status"] = normalizeVMResourceStatus(live.Status)
}
}
if req.AutoStart != nil {
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "storagePool", Name: itemStorage.Name, AutoStart: req.AutoStart,
}); err != nil {
return err
}
vars["auto_start"] = *req.AutoStart
}
if req.Status != nil {
status := normalizeVMResourceStatus(*req.Status)
if !validVMResourceOperateStatus(status) {
return buserr.WithDetail("ErrInvalidParams", status, nil)
}
if err := v.provider.UpdateResource(ctx, types.VMResourceUpdate{
Type: "storagePool", Name: itemStorage.Name, Status: &status,
}); err != nil {
return err
}
vars["status"] = status
}
if len(vars) == 0 {
return nil
}
return vmRepo.UpdateStorage(req.ID, vars)
}
func (v *VMService) DeleteStorage(req dto.VMResourceDelete) error {
ctx, cancel := vmResourceOperationContext(req.Force)
defer cancel()
itemStorage, err := vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrRecordNotFound")
}
return err
}
unlock, cleanup, err := vmResourceLocks.lockForDelete(ctx, "storagePool", itemStorage.Name, req.Force)
if err != nil {
return err
}
defer unlock()
itemStorage, err = vmRepo.GetStorage(repo.WithByID(req.ID))
if err != nil {
return err
}
if len(loadVMResourceRefs("storagePool")[itemStorage.Name]) > 0 {
return buserr.New("ErrVMStorageInUse")
}
if cleanup {
if err := v.deleteVMResource(ctx, "storagePool", itemStorage.Name, req.Force); err != nil {
return err
}
}
return vmRepo.DeleteStorage(repo.WithByID(req.ID))
}
func (v *VMService) mergeStorageItems(records []model.VMStorage) []dto.VMStorageItem {
itemsByValue := map[string]dto.VMStorageItem{}
for _, record := range records {
itemsByValue[record.Name] = dto.VMStorageItem{
ID: record.ID, Name: record.Name, Type: record.Type,
Path: record.Path, AutoStart: record.AutoStart, Description: record.Description,
CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
Status: normalizeVMResourceStatus(record.Status),
Error: record.Error,
}
}
if v.helperExists() {
if liveItems, err := v.provider.ListResources(context.Background(), "storagePool"); err == nil {
for _, live := range liveItems {
item, ok := itemsByValue[live.Name]
if !ok || vmResourceCreationIncomplete(item.Status) {
continue
}
item.Type = live.SubType
item.Status = normalizeVMResourceStatus(live.Status)
item.AutoStart = live.AutoStart
item.Capacity = live.Capacity
item.Available = live.Available
itemsByValue[live.Name] = item
}
}
}
vmRefs := loadVMResourceRefs("storagePool")
items := make([]dto.VMStorageItem, 0, len(records))
for _, record := range records {
item := itemsByValue[record.Name]
if item.Status == "" {
item.Status = constant.StatusDeleted
}
item.VMs = vmRefs[record.Name]
item.VMCount = len(item.VMs)
items = append(items, item)
}
return items
}
func loadVMResourceRefs(resourceType string) map[string][]dto.VMResourceRef {
records, err := vmRepo.List()
if err != nil {
return map[string][]dto.VMResourceRef{}
}
refs := map[string][]dto.VMResourceRef{}
for _, record := range records {
ref := dto.VMResourceRef{ID: record.ID, Name: record.Name}
switch resourceType {
case "network":
if record.Network != "" {
refs[record.Network] = append(refs[record.Network], ref)
}
case "storagePool":
if record.StoragePool != "" {
refs[record.StoragePool] = append(refs[record.StoragePool], ref)
}
}
}
for name := range refs {
sort.Slice(refs[name], func(i, j int) bool {
return strings.ToLower(refs[name][i].Name) < strings.ToLower(refs[name][j].Name)
})
}
return refs
}
func (v *VMService) fillVMStoragePools(items []dto.VMInfo) {
needsPools := false
for _, item := range items {
if item.StoragePool == "" && item.DiskPath != "" {
needsPools = true
break
}
}
if !needsPools {
return
}
pools, err := v.provider.ListResources(context.Background(), "storagePool")
if err != nil {
global.LOG.Warnf("[xpack] resolve VM storage pools failed, retaining metadata: %v", err)
return
}
for i := range items {
if items[i].StoragePool == "" {
items[i].StoragePool = storagePoolForDisk(items[i].DiskPath, pools)
}
}
}
func storagePoolForDisk(diskPath string, pools []dto.VMResourceInfo) string {
if !filepath.IsAbs(diskPath) {
return ""
}
poolName, bestLength := "", 0
for _, pool := range pools {
poolPath := normalizeStoragePath(pool.Path)
if pool.Name == "" || !filepath.IsAbs(poolPath) {
continue
}
rel, err := filepath.Rel(poolPath, diskPath)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
continue
}
if len(poolPath) > bestLength {
poolName, bestLength = pool.Name, len(poolPath)
} else if len(poolPath) == bestLength && poolName != pool.Name {
poolName = ""
}
}
return poolName
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -280,6 +280,8 @@ func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp {
}
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
alias := create.Alias
if alias == "default" {
return buserr.New("ErrDefaultAlias")
@@ -377,6 +379,11 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
website.HttpConfig = sslReq.HttpConfig
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)
@@ -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) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return nil, err
@@ -1012,6 +1021,18 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH
res response.WebsiteHTTPS
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 {
return nil, err
}
+11
View File
@@ -14,6 +14,8 @@ import (
)
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
websiteListenMutation.Lock()
defer websiteListenMutation.Unlock()
var (
domainModels []model.WebsiteDomain
addPorts []int
@@ -31,6 +33,15 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
if err != nil {
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() {
if err := ensureFirewallPorts(addPorts); err != nil {
global.LOG.Errorf("allow website firewall ports failed: %v", err)
+74
View File
@@ -0,0 +1,74 @@
package service
import (
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
var websiteListenMutation sync.Mutex
func websiteListenPorts(website model.Website, httpPort, httpsPort int) map[int]struct{} {
ports := make(map[int]struct{})
for _, domain := range website.Domains {
if domain.Port > 0 {
ports[domain.Port] = struct{}{}
}
}
if website.Protocol == constant.ProtocolHTTPS {
plan := buildWebsiteTLSPlan(website.Domains, httpPort, httpsPort)
for _, port := range plan.httpsPorts {
if port > 0 {
ports[port] = struct{}{}
}
}
}
return ports
}
func checkWebsiteListenConflict(website model.Website, httpPort, httpsPort int) error {
return checkWebsiteListenPorts(website, websiteListenPorts(website, httpPort, httpsPort), httpPort, httpsPort)
}
func checkWebsiteListenPorts(website model.Website, ports map[int]struct{}, httpPort, httpsPort int) error {
if len(website.Domains) == 0 || len(ports) == 0 {
return nil
}
names := make(map[string]struct{}, len(website.Domains))
for _, domain := range website.Domains {
names[strings.ToLower(domain.Domain)] = struct{}{}
}
existing, err := websiteRepo.GetBy()
if err != nil {
return err
}
domains, err := websiteDomainRepo.GetBy()
if err != nil {
return err
}
byWebsite := make(map[uint][]model.WebsiteDomain)
for _, domain := range domains {
byWebsite[domain.WebsiteID] = append(byWebsite[domain.WebsiteID], domain)
}
for _, other := range existing {
if website.ID != 0 && other.ID == website.ID || other.Type == constant.Stream {
continue
}
other.Domains = byWebsite[other.ID]
for _, domain := range other.Domains {
if _, match := names[strings.ToLower(domain.Domain)]; !match {
continue
}
for port := range websiteListenPorts(other, httpPort, httpsPort) {
if _, occupied := ports[port]; occupied {
return buserr.WithName("ErrDomainIsUsed", other.PrimaryDomain)
}
}
break
}
}
return nil
}
-3
View File
@@ -6,6 +6,3 @@ import (
//go:embed compose.yml
var DefaultMcpCompose []byte
//go:embed llm-compose.yml
var DefaultTensorrtLLMCompose []byte
-30
View File
@@ -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
-1
View File
@@ -3,7 +3,6 @@ base:
mode: dev
is_demo: false
is_offline: false
is_fxplay: false
is_enterprise: false
log:
+130 -159
View File
@@ -8,6 +8,107 @@
"formatZH": "更新 Ollama 绑定域名 [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": {
"bodyKeys": [
"name"
@@ -53,44 +154,6 @@
"formatZH": "添加 Ollama 模型重试 [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": {
"bodyKeys": [
"title"
@@ -103,27 +166,27 @@
"/alert/config/del": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
"formatEN": "delete alert config [name]"
},
"/alert/config/status": {
"bodyKeys": [
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
"formatEN": "update alert config status [name][status]"
},
"/alert/config/update": {
"bodyKeys": [
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
"formatEN": "update alert config [displayName]"
},
"/alert/del": {
"bodyKeys": [
@@ -4971,98 +5034,6 @@
"formatZH": "更新离线告警日志 [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": {
"bodyKeys": [],
"paramKeys": [],
@@ -5211,7 +5182,7 @@
"formatZH": "更新 vLLM [name]",
"formatEN": "update vLLM [name]"
},
"/xpack/vms": {
"/vms": {
"bodyKeys": [
"name"
],
@@ -5220,7 +5191,7 @@
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"/vms/del": {
"bodyKeys": [
"id"
],
@@ -5238,7 +5209,7 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/xpack/vms/disks/resize": {
"/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
@@ -5249,14 +5220,14 @@
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/xpack/vms/iso": {
"/vms/iso": {
"bodyKeys": [
"name",
"type"
@@ -5266,7 +5237,7 @@
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"/vms/iso/del": {
"bodyKeys": [
"id"
],
@@ -5284,7 +5255,7 @@
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"/vms/iso/update": {
"bodyKeys": [
"name",
"type"
@@ -5294,7 +5265,7 @@
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"/vms/networks": {
"bodyKeys": [
"name",
"type"
@@ -5304,7 +5275,7 @@
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"/vms/networks/del": {
"bodyKeys": [
"id"
],
@@ -5322,7 +5293,7 @@
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"/vms/networks/update": {
"bodyKeys": [
"id"
],
@@ -5340,7 +5311,7 @@
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"/vms/operate": {
"bodyKeys": [
"name",
"operate"
@@ -5350,14 +5321,14 @@
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"/vms/rename": {
"bodyKeys": [
"name",
"newName"
@@ -5367,7 +5338,7 @@
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
@@ -5377,7 +5348,7 @@
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
@@ -5387,7 +5358,7 @@
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
@@ -5397,7 +5368,7 @@
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"/vms/storages": {
"bodyKeys": [
"name",
"type"
@@ -5407,7 +5378,7 @@
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"/vms/storages/del": {
"bodyKeys": [
"id"
],
@@ -5425,7 +5396,7 @@
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"/vms/storages/update": {
"bodyKeys": [
"id"
],
@@ -5443,14 +5414,14 @@
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"/vms/templates": {
"bodyKeys": [
"name",
"templateName"
@@ -5460,7 +5431,7 @@
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"/vms/templates/del": {
"bodyKeys": [
"id"
],
@@ -5478,7 +5449,7 @@
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"/vms/update": {
"bodyKeys": [
"name"
],
+5 -1
View File
@@ -77,7 +77,11 @@ func Run() {
global.LOG.Errorf("generate random hour failed: %v", err)
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())
}
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
@@ -0,0 +1,24 @@
package job
import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
)
type containerImageCheckUpdate struct{}
func NewContainerImageCheckUpdateJob() *containerImageCheckUpdate {
return &containerImageCheckUpdate{}
}
func (j *containerImageCheckUpdate) Run() {
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
global.LOG.Errorf("load container image update schedule: %v", err)
return
}
if status.Enabled {
if _, err := service.StartContainerImageCheckUpdate(); err != nil {
global.LOG.Errorf("start container image update check: %v", err)
}
}
}
-2
View File
@@ -8,7 +8,6 @@ type ServerConfig struct {
type Base struct {
Port string `mapstructure:"port"`
IsFxplay bool `mapstructure:"is_fxplay"`
IsEnterprise bool `mapstructure:"is_enterprise"`
Edition string `mapstructure:"edition"` // [ cn / intl]
Version string `mapstructure:"version"`
@@ -43,7 +42,6 @@ type SystemDir struct {
SSLLogDir string
McpDir string
ConvertLogDir string
TensorRTLLMDir string
FirewallDir string
}
-9
View File
@@ -69,9 +69,6 @@ func RepoURL() string {
if CONF.Base.IsEnterprise {
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" {
return "https://resource.fit2cloud.com/1panel/package/v2"
}
@@ -81,9 +78,6 @@ func ResourceURL() string {
if CONF.Base.IsEnterprise {
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" {
return "https://resource.fit2cloud.com/1panel/resource/v2"
}
@@ -93,9 +87,6 @@ func AppRepoURL() string {
if CONF.Base.IsEnterprise {
return "https://apps-assets.fit2cloud.com"
}
if CONF.Base.IsFxplay {
return "https://apps-assets.fit2cloud.com"
}
if CONF.Base.Edition != "intl" {
return "https://apps-assets.fit2cloud.com"
}
+2
View File
@@ -2,6 +2,8 @@ module github.com/1Panel-dev/1Panel/agent
go 1.27.3
godebug multipathtcp=0
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require (
+24 -6
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Requested resource not found: {{ .err }}'
# quick jump
ErrMinQuickJump: 'Set at least one quick jump entry'
ErrMaxQuickJump: 'At most 4 quick jump entries'
ErrMaxQuickJump: 'At most 5 quick jump entries'
# common
ErrShutDown: 'Command stopped manually'
@@ -70,14 +70,14 @@ Decrypt: "Decrypt"
# agent
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.'
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.'
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 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 }}'
ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified'
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'
ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it'
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'The certificate file does not exist'
ErrSSLKeyFormat: 'Private key file verification failed'
ErrSSLCertificateFormat: 'Invalid certificate format; use PEM'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be blank'
ErrOpenrestyInstall: "Please install OpenResty from the App Store first"
ErrOpenrestyNotFound: 'HTTP mode requires OpenResty'
ApplySSLStart: 'Starting certificate request. Domain: [{{ .domain }}], method: [{{ .type }}]'
dnsAccount: 'DNS Auto'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
# AI gateway
ErrAIProxyServiceNotInstalled: '{{ .name }} service is not installed'
ErrAIProxyModelAccountNotFound: 'Model account {{ .id }} not found'
ErrAIProxyModelAccountNotFound: 'Model service {{ .id }} not found'
ErrAIProxyModelMapRequired: 'This field is required.'
ErrAIProxyModelMapInvalid: 'Model mapping must be a valid JSON object: {{ .err }}'
ErrAIProxyModelMapEmpty: 'Model mapping cannot be empty'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: '1Panel AI Gateway'
# Firewall
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
ErrVMNotFound: "Virtual machine does not exist"
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 }}"
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
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."
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"
+24 -6
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'No se pudo encontrar el recurso solicitado {{ .err }}'
# salto rápido
ErrMinQuickJump: 'Agrega al menos una entrada rápida'
ErrMaxQuickJump: 'Máximo 4 entradas rápidas'
ErrMaxQuickJump: 'Máximo 5 entradas rápidas'
# común
ErrShutDown: 'Comando detenido manualmente'
@@ -70,14 +70,14 @@ Decrypt: 'Descifrar'
# 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.'
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.'
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: '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 }}'
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
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'
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'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'El archivo de certificado no existe'
ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada'
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
ErrOpenrestyInstall: "Instale primero OpenResty desde la tienda de aplicaciones"
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
dnsAccount: 'DNS Automático'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
# puerta de enlace de IA
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.'
ErrAIProxyModelMapInvalid: 'El mapeo de modelos debe ser un objeto JSON válido: {{ .err }}'
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'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'Puerta de enlace de IA de 1Panel'
# Firewall
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
ErrVMNotFound: "La máquina virtual no 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 }}"
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
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."
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'منبع درخواستی یافت نشد: {{ .err }}'
# پرش سریع
ErrMinQuickJump: 'حداقل یک ورودی پرش سریع تنظیم کنید'
ErrMaxQuickJump: 'حداکثر ۴ ورودی پرش سریع'
ErrMaxQuickJump: 'حداکثر ۵ ورودی پرش سریع'
# عمومی
ErrShutDown: 'دستور به صورت دستی متوقف شد'
@@ -70,14 +70,14 @@ Decrypt: "رمزگشایی"
# عامل
ErrAgentAccountBound: 'حساب به یک عامل متصل است و قابل حذف نیست'
ErrTerminalAIAccountInUse: 'این حساب مدل در حال حاضر توسط ترمینال هوش مصنوعی استفاده می‌شود. حساب را در تنظیمات ترمینال هوش مصنوعی تغییر دهید یا ترمینال هوش مصنوعی را غیرفعال کرده و دوباره تلاش کنید.'
ErrFileAIAccountInUse: 'این حساب مدل توسط هوش مصنوعی مدیریت فایل استفاده می‌شود. حساب را در تنظیمات آن تغییر دهید یا هوش مصنوعی مدیریت فایل را غیرفعال کرده و دوباره تلاش کنید.'
ErrTerminalAIAccountInUse: 'این سرویس مدل در حال حاضر توسط ترمینال هوش مصنوعی استفاده می‌شود. حساب را در تنظیمات ترمینال هوش مصنوعی تغییر دهید یا ترمینال هوش مصنوعی را غیرفعال کرده و دوباره تلاش کنید.'
ErrFileAIAccountInUse: 'این سرویس مدل توسط هوش مصنوعی مدیریت فایل استفاده می‌شود. حساب را در تنظیمات آن تغییر دهید یا هوش مصنوعی مدیریت فایل را غیرفعال کرده و دوباره تلاش کنید.'
ErrAgentAccountUnavailable: 'اتصال حساب در دسترس نیست: {{ .err }}'
ErrAgentProviderNotSupported: 'ارائه‌دهنده عامل پشتیبانی نمی‌شود'
ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید'
ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است'
ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید'
ErrAgentAccountSingleInitialModel: 'فقط یک مدل اولیه می‌تواند در هنگام ایجاد حساب پیکربندی شود'
ErrAgentAccountSingleInitialModel: 'برای این ارائه‌دهنده حداکثر یک مدل می‌توان پیکربندی کرد'
ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است'
ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده می‌شود. پیش از حذف، مدل بررسی دیگری انتخاب کنید'
ErrAgentFallbackModelDuplicate: 'مدل‌های جایگزین نمی‌توانند تکراری باشند'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'فایل گواهی وجود ندارد'
ErrSSLKeyFormat: 'تأیید فایل کلید خصوصی ناموفق بود'
ErrSSLCertificateFormat: 'فرمت گواهی نامعتبر است؛ از PEM استفاده کنید'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid یا EabHmacKey نمی‌توانند خالی باشند'
ErrOpenrestyInstall: "لطفاً ابتدا OpenResty را از فروشگاه برنامه‌ها نصب کنید"
ErrOpenrestyNotFound: 'حالت HTTP نیاز به OpenResty دارد'
ApplySSLStart: 'شروع درخواست گواهی. دامنه: [{{ .domain }}]، روش: [{{ .type }}]'
dnsAccount: 'خودکار DNS'
@@ -676,7 +677,7 @@ TerminalAIRequestFailed: 'درخواست هوش مصنوعی ناموفق بود
# دروازه هوش مصنوعی
ErrAIProxyServiceNotInstalled: 'سرویس {{ .name }} نصب نشده است'
ErrAIProxyModelAccountNotFound: 'حساب مدل {{ .id }} یافت نشد'
ErrAIProxyModelAccountNotFound: 'سرویس مدل {{ .id }} یافت نشد'
ErrAIProxyModelMapRequired: 'این فیلد الزامی است.'
ErrAIProxyModelMapInvalid: 'نگاشت مدل باید یک شی JSON معتبر باشد: {{ .err }}'
ErrAIProxyModelMapEmpty: 'نگاشت مدل نمی‌تواند خالی باشد'
@@ -687,6 +688,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'درگاه هوش مصنوعی 1Panel'
# Firewall
ErrDockerIptablesChainUnavailable: 'زنجیره DOCKER-USER داکر یافت نشد. داکر را راه‌اندازی مجدد کنید و دوباره تلاش کنید'
@@ -736,7 +738,7 @@ ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندار
# ماشین مجازی
ErrVMNotFound: "ماشین مجازی وجود ندارد"
ErrVMAlreadyExists: "ماشین مجازی از قبل وجود دارد"
ErrVMCountLimit: "در نسخه Professional می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه Enterprise ارتقا دهید و دوباره تلاش کنید."
ErrVMCountLimit: "در نسخه فعلی می‌توانید حداکثر {{ .limit }} ماشین مجازی ایجاد کنید. یک ماشین مجازی موجود را حذف کنید یا به نسخه بالاتر ارتقا دهید و دوباره تلاش کنید."
ErrVMImageAlreadyExists: "فایل ایمیج ماشین مجازی از قبل وجود دارد: {{ .detail }}"
ErrVMStoragePathInUse: "مسیر استخر ذخیره‌سازی قبلاً توسط [{{ .name }}] استفاده شده است"
ErrVMHelperNotFound: "1panel-kvm یافت نشد"
@@ -803,3 +805,19 @@ ErrFeishuUpgradeMounts: "این اتصال‌های ذخیره‌سازی کام
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '要求されたリソース {{ .err }} が見つかりま
# クイックジャンプ
ErrMinQuickJump: 'クイックアクセスを1件以上追加'
ErrMaxQuickJump: '最大4件のクイックアクセス'
ErrMaxQuickJump: '最大5件のクイックアクセス'
# 共通
ErrShutDown: 'コマンドは手動で停止されました'
@@ -70,14 +70,14 @@ Decrypt: '復号化'
# エージェント
ErrAgentAccountBound: 'アカウントはエージェントに紐づいています'
ErrTerminalAIAccountInUse: 'このモデルアカウントは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。'
ErrFileAIAccountInUse: 'このモデルアカウントはファイル管理 AI で使用中です。ファイル管理 AI の設定で別のアカウントへ切り替えるか、ファイル管理 AI を無効にしてから再試行してください。'
ErrTerminalAIAccountInUse: 'このモデルサービスは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。'
ErrFileAIAccountInUse: 'このモデルサービスはファイル管理 AI で使用中です。ファイル管理 AI の設定で別のアカウントへ切り替えるか、ファイル管理 AI を無効にしてから再試行してください。'
ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です'
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
ErrAgentAccountSingleInitialModel: 'このプロバイダーで設定できるモデルは最大 1 つです'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください'
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '証明書ファイルが存在しません'
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
ErrOpenrestyInstall: "先にアプリストアから OpenResty をインストールしてください"
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
dnsAccount: 'DNS 自動'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI リクエストに失敗しました: {{ .err }}'
# AIゲートウェイ
ErrAIProxyServiceNotInstalled: '{{ .name }} サービスはインストールされていません'
ErrAIProxyModelAccountNotFound: 'モデルアカウント {{ .id }} が見つかりません'
ErrAIProxyModelAccountNotFound: 'モデルサービス {{ .id }} が見つかりません'
ErrAIProxyModelMapRequired: 'この項目は必須です。'
ErrAIProxyModelMapInvalid: 'モデルマッピングは有効な JSON オブジェクトである必要があります: {{ .err }}'
ErrAIProxyModelMapEmpty: 'モデルマッピングは空にできません'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: '1Panel AI ゲートウェイ'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker の DOCKER-USER チェーンが見つかりません。Docker を再起動してから再試行してください'
@@ -735,7 +737,7 @@ ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
# 仮想マシン
ErrVMNotFound: "仮想マシンが存在しません"
ErrVMAlreadyExists: "仮想マシンは既に存在します"
ErrVMCountLimit: "Professional 版では最大 {{ .limit }} 台の仮想マシンを作成できます。既存の仮想マシンを削除するか、Enterprise 版にアップグレードしてから再試行してください。"
ErrVMCountLimit: "現在のエディションでは最大 {{ .limit }} 台の仮想マシンを作成できます。 既存の仮想マシンを削除するか、上位エディションにアップグレードしてから再試行してください。"
ErrVMImageAlreadyExists: "仮想マシンのイメージファイルは既に存在します: {{ .detail }}"
ErrVMStoragePathInUse: "ストレージプールのパスは既に [{{ .name }}] で使用されています"
ErrVMHelperNotFound: "1panel-kvm が見つかりません"
@@ -802,3 +804,19 @@ ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '요청한 리소스 {{ .err }}을 찾을 수 없습니다.'
# 빠른 이동
ErrMinQuickJump: '퀵 액세스를 하나 이상 추가하세요'
ErrMaxQuickJump: '퀵 액세스는 최대 4개'
ErrMaxQuickJump: '퀵 액세스는 최대 5개'
# 공통
ErrShutDown: '명령이 수동으로 중단되었습니다'
@@ -70,14 +70,14 @@ Decrypt: '복호화'
# 에이전트
ErrAgentAccountBound: '계정이 에이전트에 묶여 있습니다'
ErrTerminalAIAccountInUse: '이 모델 계정은 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.'
ErrFileAIAccountInUse: '이 모델 계정은 현재 파일 관리 AI에서 사용 중입니다. 파일 관리 AI 설정에서 다른 계정으로 변경하거나 파일 관리 AI를 비활성화한 뒤 다시 시도하세요.'
ErrTerminalAIAccountInUse: '이 모델 서비스는 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.'
ErrFileAIAccountInUse: '이 모델 서비스는 현재 파일 관리 AI에서 사용 중입니다. 파일 관리 AI 설정에서 다른 계정으로 변경하거나 파일 관리 AI를 비활성화한 뒤 다시 시도하세요.'
ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
ErrAgentAccountSingleInitialModel: '이 제공업체에는 모델을 최대 하나만 설정할 수 있습니다'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요'
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '인증서 파일이 존재하지 않습니다'
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
ErrOpenrestyInstall: "먼저 앱 스토어에서 OpenResty를 설치하세요"
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
dnsAccount: 'DNS 자동'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI 요청 실패: {{ .err }}'
# AI 게이트웨이
ErrAIProxyServiceNotInstalled: '{{ .name }} 서비스가 설치되어 있지 않습니다'
ErrAIProxyModelAccountNotFound: '모델 계정 {{ .id }}을(를) 찾을 수 없습니다'
ErrAIProxyModelAccountNotFound: '모델 서비스 {{ .id }}을(를) 찾을 수 없습니다'
ErrAIProxyModelMapRequired: '이 필드는 필수입니다.'
ErrAIProxyModelMapInvalid: '모델 매핑은 유효한 JSON 객체여야 합니다: {{ .err }}'
ErrAIProxyModelMapEmpty: '모델 매핑은 비워 둘 수 없습니다'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: '1Panel AI 게이트웨이'
# Firewall
ErrDockerIptablesChainUnavailable: 'Docker DOCKER-USER 체인을 찾을 수 없습니다. Docker를 다시 시작한 후 재시도하세요'
@@ -735,7 +737,7 @@ ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
# 가상 머신
ErrVMNotFound: "가상 머신이 존재하지 않습니다"
ErrVMAlreadyExists: "가상 머신이 이미 존재합니다"
ErrVMCountLimit: "Professional 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 Enterprise 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMCountLimit: "현재 버전에서는 가상 머신을 최대 {{ .limit }}대까지 생성할 수 있습니다. 기존 가상 머신을 삭제하거나 상위 버전으로 업그레이드한 후 다시 시도하세요."
ErrVMImageAlreadyExists: "가상 머신 이미지 파일이 이미 존재합니다: {{ .detail }}"
ErrVMStoragePathInUse: "스토리지 풀 경로가 이미 [{{ .name }}]에서 사용 중입니다"
ErrVMHelperNotFound: "1panel-kvm을 찾을 수 없습니다"
@@ -802,3 +804,19 @@ ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
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
View File
@@ -32,7 +32,7 @@ ErrApiConfigKeyTimeInvalid: 'ເວລາ (Timestamp) ຂອງ API ບໍ່ຖ
ErrShutDown: 'ຄຳສັ່ງຖືກຢຸດດ້ວຍຕົນເອງ'
ErrMinQuickJump: 'ກະລຸນາຕັ້ງຄ່າລາຍການທາງລັດຢ່າງໜ້ອຍໜຶ່ງລາຍການ'
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 4 ລາຍການ'
ErrMaxQuickJump: 'ສາມາດຕັ້ງຄ່າລາຍການທາງລັດໄດ້ສູງສຸດ 5 ລາຍການ'
#common
ErrUsernameIsExist: 'ຊື່ຜູ້ໃຊ້ມີຢູ່ໃນລະບົບແລ້ວ'
@@ -55,14 +55,14 @@ Decrypt: "ຖອດລະຫັດ"
#agent
ErrAgentAccountBound: 'ບັນຊີຖືກຜູກມັດກັບຕົວແທນ (Agent) ແລະບໍ່ສາມາດລຶບໄດ້'
ErrTerminalAIAccountInUse: 'ບັນຊີໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ Terminal AI. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ Terminal AI ຫຼື ປິດໃຊ້ງານ Terminal AI ແລ້ວລອງໃໝ່.'
ErrFileAIAccountInUse: 'ບັນຊີໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ AI ຈັດການໄຟລ໌. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ AI ຈັດການໄຟລ໌ ຫຼື ປິດໃຊ້ງານແລ້ວລອງໃໝ່.'
ErrTerminalAIAccountInUse: 'ບໍລິການໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ Terminal AI. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ Terminal AI ຫຼື ປິດໃຊ້ງານ Terminal AI ແລ້ວລອງໃໝ່.'
ErrFileAIAccountInUse: 'ບໍລິການໂມເດລນີ້ກຳລັງຖືກໃຊ້ໂດຍ AI ຈັດການໄຟລ໌. ກະລຸນາປ່ຽນບັນຊີໃນການຕັ້ງຄ່າ AI ຈັດການໄຟລ໌ ຫຼື ປິດໃຊ້ງານແລ້ວລອງໃໝ່.'
ErrAgentAccountUnavailable: 'ການເຊື່ອມຕໍ່ບັນຊີບໍ່ສາມາດໃຊ້ງານໄດ້: {{ .err }}'
ErrAgentProviderNotSupported: 'ບໍ່ຮອງຮັບຜູ້ໃຫ້ບໍລິການຕົວແທນນີ້'
ErrAgentAccountRequired: 'ກະລຸນາເລືອກບັນຊີຕົວແທນກ່ອນ'
ErrAgentAccountNotVerified: 'ບັນຊີຕົວແທນຍັງບໍ່ໄດ້ຮັບການຢືນຢັນ'
ErrAgentAccountModelsRequired: 'ກະລຸນາກຳນົດຄ່າໂມເດລຢ່າງໜ້ອຍໜຶ່ງຕົວ'
ErrAgentAccountSingleInitialModel: 'ສາມາດກຳນົດຄ່າໂມເດລເລີ່ມຕົ້ນໄດ້ພຽງຕົວດຽວໃນລະຫວ່າງການສ້າງບັນຊີ'
ErrAgentAccountSingleInitialModel: 'ຜູ້ໃຫ້ບໍລິການນີ້ສາມາດກຳນົດຄ່າໄດ້ສູງສຸດໜຶ່ງໂມເດວ'
ErrAgentModelNotInAccount: 'ໂມເດລບໍ່ໄດ້ຖືກກຳນົດຄ່າໃນບັນຊີທີ່ເລືອກ'
ErrAgentVerifyModelInUse: 'ໂມເດລນີ້ຖືກໃຊ້ສຳລັບກວດສອບບັນຊີ. ກະລຸນາເລືອກໂມເດລກວດສອບອື່ນກ່ອນລຶບ.'
ErrAgentFallbackModelDuplicate: 'ໂມເດລສຳຮອງ (Fallback) ບໍ່ສາມາດຊ້ຳກັນໄດ້'
@@ -242,6 +242,7 @@ ErrSSLCertificateNotFound: 'ບໍ່ມີໄຟລ໌ໃບຮັບຮອງ
ErrSSLKeyFormat: 'ການກວດສອບໄຟລ໌ກຸນແຈສ່ວນຕົວລົ້ມເຫຼວ'
ErrSSLCertificateFormat: 'ຮູບແບບໃບຮັບຮອງບໍ່ຖືກຕ້ອງ; ກະລຸນາໃຊ້ PEM'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ຫຼື EabHmacKey ບໍ່ສາມາດປະຫວ່າງໄດ້'
ErrOpenrestyInstall: "ກະລຸນາຕິດຕັ້ງ OpenResty ຈາກຮ້ານແອັບກ່ອນ"
ErrOpenrestyNotFound: 'ໂໝດ HTTP ຕ້ອງການ OpenResty'
ApplySSLStart: 'ກຳລັງເລີ່ມການຮ້ອງຂໍໃບຮັບຮອງ. ໂດເມນ: [{{ .domain }}], ວິທີການ: [{{ .type }}]'
dnsAccount: 'DNS ອັດຕະໂນມັດ'
@@ -666,7 +667,7 @@ FileAISearchEmptyDir: 'ບໍ່ພົບໄຟລ໌ ຫຼື ໂຟນເດ
# ai proxy
ErrAIProxyServiceNotInstalled: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງບໍລິການ {{ .name }}'
ErrAIProxyModelAccountNotFound: 'ບໍ່ພົບບັນຊີໂມເດລ {{ .id }}'
ErrAIProxyModelAccountNotFound: 'ບໍ່ພົບບໍລິການໂມເດລ {{ .id }}'
ErrAIProxyModelMapRequired: 'ຟິວ (Field) ນີ້ຈຳເປັນຕ້ອງມີ.'
ErrAIProxyModelMapInvalid: 'ການຈັບຄູ່ໂມເດລຕ້ອງເປັນວັດຖຸ JSON ທີ່ຖືກຕ້ອງ: {{ .err }}'
ErrAIProxyModelMapEmpty: 'ການຈັບຄູ່ໂມເດລບໍ່ສາມາດຫວ່າງເປົ່າໄດ້'
@@ -677,6 +678,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'ເກດເວ AI ຂອງ 1Panel'
# Firewall
ErrDockerIptablesChainUnavailable: 'ບໍ່ພົບ chain DOCKER-USER ຂອງ Docker. ກະລຸນາເລີ່ມ Docker ໃໝ່ແລ້ວລອງອີກຄັ້ງ'
@@ -726,7 +728,7 @@ ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນ
# virtual machine
ErrVMNotFound: "ບໍ່ມີເຄື່ອງສະເໝືອນ"
ErrVMAlreadyExists: "ມີເຄື່ອງສະເໝືອນຢູ່ແລ້ວ"
ErrVMCountLimit: "ລຸ້ນ Professional ສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດເປັນລຸ້ນ Enterprise ແລ້ວລອງອີກຄັ້ງ."
ErrVMCountLimit: "ລຸ້ນປັດຈຸບັນສາມາດສ້າງເຄື່ອງສະເໝືອນໄດ້ສູງສຸດ {{ .limit }} ເຄື່ອງ. ກະລຸນາລຶບເຄື່ອງສະເໝືອນທີ່ມີຢູ່ ຫຼື ອັບເກຣດລຸ້ນ ແລ້ວລອງອີກຄັ້ງ."
ErrVMImageAlreadyExists: "ມີອິມເມຈເຄື່ອງສະເໝືອນຢູ່ແລ້ວ: {{ .detail }}"
ErrVMStoragePathInUse: "ເສັ້ນທາງພູນຈັດເກັບຖືກໃຊ້ໂດຍ [{{ .name }}] ແລ້ວ"
ErrVMHelperNotFound: "ບໍ່ພົບ 1panel-kvm"
@@ -793,3 +795,19 @@ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Sumber yang diminta {{ .err }} tidak dapat ditemui'
# lompat pantas
ErrMinQuickJump: 'Tambah sekurang-kurangnya satu pintasan pantas'
ErrMaxQuickJump: 'Maksimum 4 pintasan pantas'
ErrMaxQuickJump: 'Maksimum 5 pintasan pantas'
# umum
ErrShutDown: 'Arahan diberhentikan manual'
@@ -70,14 +70,14 @@ Decrypt: 'Dekripsi'
# 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.'
ErrFileAIAccountInUse: 'Akaun model ini sedang digunakan oleh AI pengurusan fail. Tukar akaun dalam tetapannya atau nyahaktifkan AI tersebut, 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: '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 }}'
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
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'
ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya'
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Fail sijil tidak wujud'
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
ErrOpenrestyInstall: "Sila pasang OpenResty daripada gedung aplikasi terlebih dahulu"
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
dnsAccount: 'DNS automatik'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Permintaan AI gagal: {{ .err }}'
# gerbang AI
ErrAIProxyServiceNotInstalled: 'Perkhidmatan {{ .name }} belum dipasang'
ErrAIProxyModelAccountNotFound: 'Akaun model {{ .id }} tidak ditemui'
ErrAIProxyModelAccountNotFound: 'Perkhidmatan model {{ .id }} tidak ditemui'
ErrAIProxyModelMapRequired: 'Medan ini wajib diisi.'
ErrAIProxyModelMapInvalid: 'Pemetaan model mestilah objek JSON yang sah: {{ .err }}'
ErrAIProxyModelMapEmpty: 'Pemetaan model tidak boleh kosong'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'Gerbang AI 1Panel'
# Firewall
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
ErrVMNotFound: "Mesin maya tidak 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 }}"
ErrVMStoragePathInUse: "Laluan kolam storan sudah digunakan oleh [{{ .name }}]"
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."
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"
+24 -6
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'O recurso solicitado {{ .err }} não pôde ser encontrado'
# 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
ErrShutDown: 'Comando parado manualmente'
@@ -70,14 +70,14 @@ Decrypt: 'Descriptografar'
# 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.'
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.'
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: '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 }}'
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
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'
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'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'O arquivo de certificado não existe'
ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada'
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
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'
ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]'
dnsAccount: 'DNS Automático'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Falha na solicitação de AI: {{ .err }}'
# gateway de IA
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.'
ErrAIProxyModelMapInvalid: 'O mapeamento de modelos deve ser um objeto JSON válido: {{ .err }}'
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'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'Gateway de IA do 1Panel'
# Firewall
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
ErrVMNotFound: "Máquina virtual não 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 }}"
ErrVMStoragePathInUse: "O caminho do pool de armazenamento já é usado por [{{ .name }}]"
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."
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'Запрошенный ресурс {{ .err }} не най
# быстрый переход
ErrMinQuickJump: 'Добавьте хотя бы одну быструю ссылку'
ErrMaxQuickJump: 'Не более 4 быстрых ссылок'
ErrMaxQuickJump: 'Не более 5 быстрых ссылок'
# общее
ErrShutDown: 'Команда остановлена вручную'
@@ -70,14 +70,14 @@ Decrypt: 'Расшифровать'
# агент
ErrAgentAccountBound: 'Акаунт привязан к агенту'
ErrTerminalAIAccountInUse: 'Эта учетная запись модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.'
ErrFileAIAccountInUse: 'Эта учетная запись модели используется ИИ файлового менеджера. Смените учетную запись в его настройках или отключите ИИ и повторите попытку.'
ErrTerminalAIAccountInUse: 'Этот сервис модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.'
ErrFileAIAccountInUse: 'Этот сервис модели используется ИИ файлового менеджера. Смените учетную запись в его настройках или отключите ИИ и повторите попытку.'
ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступна: {{ .err }}'
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
ErrAgentAccountSingleInitialModel: 'Для этого провайдера можно настроить не более одной модели'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки'
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Файл сертификата не существ
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
ErrOpenrestyInstall: "Сначала установите OpenResty из магазина приложений"
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
dnsAccount: 'DNS Авто'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'Ошибка запроса AI: {{ .err }}'
# AI-шлюз
ErrAIProxyServiceNotInstalled: 'Сервис {{ .name }} не установлен'
ErrAIProxyModelAccountNotFound: 'Аккаунт модели {{ .id }} не найден'
ErrAIProxyModelAccountNotFound: 'Сервис модели {{ .id }} не найден'
ErrAIProxyModelMapRequired: 'Это поле обязательно.'
ErrAIProxyModelMapInvalid: 'Сопоставление моделей должно быть допустимым JSON-объектом: {{ .err }}'
ErrAIProxyModelMapEmpty: 'Сопоставление моделей не может быть пустым'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: 'ИИ-шлюз 1Panel'
# Firewall
ErrDockerIptablesChainUnavailable: 'Цепочка Docker DOCKER-USER не найдена. Перезапустите Docker и повторите попытку'
@@ -735,7 +737,7 @@ ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не с
# виртуальная машина
ErrVMNotFound: "Виртуальная машина не существует"
ErrVMAlreadyExists: "Виртуальная машина уже существует"
ErrVMCountLimit: "В редакции Professional можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на Enterprise и повторите попытку."
ErrVMCountLimit: "В текущей редакции можно создать не более {{ .limit }} виртуальных машин. Удалите существующую виртуальную машину или перейдите на старшую редакцию и повторите попытку."
ErrVMImageAlreadyExists: "Файл образа виртуальной машины уже существует: {{ .detail }}"
ErrVMStoragePathInUse: "Путь пула хранения уже используется [{{ .name }}]"
ErrVMHelperNotFound: "1panel-kvm не найден"
@@ -802,3 +804,19 @@ ErrFeishuUpgradeMounts: "Эти монтирования не поддержив
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: 'İstenen kaynak {{ .err }} bulunamadı'
# hızlı geçiş
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
ErrShutDown: 'Komut manuel durduruldu'
@@ -70,14 +70,14 @@ Decrypt: 'Şifre Çöz'
# ajan
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.'
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.'
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 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 }}'
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
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ı'
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'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: 'Sertifika dosyası mevcut değil'
ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız'
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
ErrOpenrestyInstall: "Lütfen önce uygulama mağazasından OpenResty yükleyin"
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
dnsAccount: 'DNS Otomatik'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI istegi basarisiz oldu: {{ .err }}'
# AI ağ geçidi
ErrAIProxyServiceNotInstalled: '{{ .name }} servisi kurulu değil'
ErrAIProxyModelAccountNotFound: 'Model hesabı {{ .id }} bulunamadı'
ErrAIProxyModelAccountNotFound: 'Model hizmeti {{ .id }} bulunamadı'
ErrAIProxyModelMapRequired: 'Bu alan zorunludur.'
ErrAIProxyModelMapInvalid: 'Model eşlemesi geçerli bir JSON nesnesi olmalıdır: {{ .err }}'
ErrAIProxyModelMapEmpty: 'Model eşlemesi boş olamaz'
@@ -686,6 +687,7 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
AIProviderArk: 'Volcengine Ark'
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
AIProvider1PanelGateway: '1Panel AI Ağ Geçidi'
# Firewall
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
ErrVMNotFound: "Sanal makine mevcut değil"
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 }}"
ErrVMStoragePathInUse: "Depolama havuzu yolu zaten [{{ .name }}] tarafından kullanılıyor"
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."
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"
+24 -6
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: '無法找到要求的資源{{ .err }}'
# 快速跳轉
ErrMinQuickJump: '請至少設定一個快速跳轉入口!'
ErrMaxQuickJump: '最多可設定四個快速跳轉入口!'
ErrMaxQuickJump: '最多可設定五個快速跳轉入口!'
# 通用
ErrShutDown: '指令被手動結束!'
@@ -70,14 +70,14 @@ Decrypt: '解密'
# 智慧體
ErrAgentAccountBound: '該帳號已綁定到智能體,無法刪除,請重試。'
ErrTerminalAIAccountInUse: '該模型帳號正被終端 AI 使用,請在終端 AI 設定中更換帳號或關閉終端 AI 後再試。'
ErrFileAIAccountInUse: '該模型帳號正被檔案管理 AI 使用,請在檔案管理 AI 設定中更換帳號或關閉檔案管理 AI 後再試。'
ErrTerminalAIAccountInUse: '該模型服務正被終端 AI 使用,請在終端 AI 設定中更換帳號或關閉終端 AI 後再試。'
ErrFileAIAccountInUse: '該模型服務正被檔案管理 AI 使用,請在檔案管理 AI 設定中更換帳號或關閉檔案管理 AI 後再試。'
ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }},請重試'
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
ErrAgentAccountModelsRequired: '請至少配置一個模型'
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
ErrAgentAccountSingleInitialModel: '目前提供商最多可設定一個模型'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除'
ErrAgentFallbackModelDuplicate: '備用模型不能重複'
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '憑證檔案不存在'
ErrSSLKeyFormat: '私鑰檔案驗證失敗'
ErrSSLCertificateFormat: '憑證檔案格式錯誤,請使用 pem 格式'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不可為空'
ErrOpenrestyInstall: "請先在應用程式商店安裝 OpenResty"
ErrOpenrestyNotFound: 'HTTP 模式需要先安裝 OpenResty'
ApplySSLStart: '開始申請憑證,網域名稱[{{ .domain }}] 申請方式[{{ .type }}]'
dnsAccount: 'DNS 自動'
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: 'AI 請求失敗:{{ .err }}'
# AI 閘道
ErrAIProxyServiceNotInstalled: '{{ .name }} 服務未安裝'
ErrAIProxyModelAccountNotFound: '模型帳號 {{ .id }} 不存在'
ErrAIProxyModelAccountNotFound: '模型服務 {{ .id }} 不存在'
ErrAIProxyModelMapRequired: '請填寫必填項'
ErrAIProxyModelMapInvalid: '模型映射必須是合法的 JSON 物件:{{ .err }}'
ErrAIProxyModelMapEmpty: '模型映射不能為空'
@@ -686,6 +687,7 @@ AIProviderBailian: '阿里雲百煉'
AIProviderBailianCodingPlan: '阿里雲百煉 Coding Plan'
AIProviderArk: '火山方舟'
AIProviderArkCodingPlan: '火山方舟 Coding Plan'
AIProvider1PanelGateway: '1Panel AI 閘道'
# 防火牆
ErrDockerIptablesChainUnavailable: '未偵測到 Docker 的 DOCKER-USER 鏈,請重新啟動 Docker 後再試'
@@ -735,7 +737,7 @@ ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
# 虛擬機
ErrVMNotFound: "虛擬機不存在"
ErrVMAlreadyExists: "虛擬機已存在"
ErrVMCountLimit: "專業版最多可建立 {{ .limit }} 台虛擬機,請刪除現有虛擬機或升級企業版後重試。"
ErrVMCountLimit: "目前版本最多可建立 {{ .limit }} 台虛擬機。 請刪除現有虛擬機或升級版本後重試。"
ErrVMImageAlreadyExists: "虛擬機映像檔已存在: {{ .detail }}"
ErrVMStoragePathInUse: "儲存池路徑已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在"
@@ -802,3 +804,19 @@ ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
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
View File
@@ -46,7 +46,7 @@ ErrHttpReqNotFound: "无法找到请求的资源 {{ .err }}"
# 快速跳转
ErrMinQuickJump: "至少设置 1 个快速跳转入口"
ErrMaxQuickJump: "最多设置 4 个快速跳转入口"
ErrMaxQuickJump: "最多设置 5 个快速跳转入口"
# 通用
ErrShutDown: "命令已手动终止"
@@ -70,14 +70,14 @@ Decrypt: "解密"
# 智能体
ErrAgentAccountBound: "该账号已绑定智能体,无法删除"
ErrTerminalAIAccountInUse: "该模型账号正被终端 AI 使用,请在终端 AI 设置中更换账号或关闭终端 AI 后重试"
ErrFileAIAccountInUse: "该模型账号正被文件管理 AI 使用,请在文件管理 AI 设置中更换账号或关闭文件管理 AI 后重试"
ErrTerminalAIAccountInUse: "该模型服务正被终端 AI 使用,请在终端 AI 设置中更换账号或关闭终端 AI 后重试"
ErrFileAIAccountInUse: "该模型服务正被文件管理 AI 使用,请在文件管理 AI 设置中更换账号或关闭文件管理 AI 后重试"
ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证"
ErrAgentAccountModelsRequired: "请至少配置一个模型"
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
ErrAgentAccountSingleInitialModel: '当前提供商最多配置一个模型'
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除"
ErrAgentFallbackModelDuplicate: "备用模型不能重复"
@@ -252,6 +252,7 @@ ErrSSLCertificateNotFound: '证书文件不存在'
ErrSSLKeyFormat: '私钥文件校验失败'
ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空'
ErrOpenrestyInstall: "请先在应用商店安装 OpenResty"
ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty'
ApplySSLStart: '开始申请证书,域名 [{{ .domain }}],方式 [{{ .type }}]'
dnsAccount: "DNS 自动"
@@ -675,7 +676,7 @@ TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
# AI 网关
ErrAIProxyServiceNotInstalled: "{{ .name }} 服务未安装"
ErrAIProxyModelAccountNotFound: "模型账号 {{ .id }} 不存在"
ErrAIProxyModelAccountNotFound: "模型服务 {{ .id }} 不存在"
ErrAIProxyModelMapRequired: "请填写必填项"
ErrAIProxyModelMapInvalid: "模型映射必须是合法的 JSON 对象:{{ .err }}"
ErrAIProxyModelMapEmpty: "模型映射不能为空"
@@ -686,6 +687,7 @@ AIProviderBailian: "阿里云百炼"
AIProviderBailianCodingPlan: "阿里云百炼 Coding Plan"
AIProviderArk: "火山方舟"
AIProviderArkCodingPlan: "火山方舟 Coding Plan"
AIProvider1PanelGateway: '1Panel AI 网关'
# 防火墙
ErrDockerIptablesChainUnavailable: "未检测到 Docker 的 DOCKER-USER 链,请重启 Docker 后重试"
@@ -735,7 +737,7 @@ ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
# 虚拟机
ErrVMNotFound: "虚拟机不存在"
ErrVMAlreadyExists: "虚拟机已存在"
ErrVMCountLimit: "专业版最多可创建 {{ .limit }} 台虚拟机,请删除已有虚拟机或升级企业版后重试。"
ErrVMCountLimit: "当前版本最多可创建 {{ .limit }} 台虚拟机。 请删除已有虚拟机或升级版本后重试。"
ErrVMImageAlreadyExists: "虚拟机镜像文件已存在: {{ .detail }}"
ErrVMStoragePathInUse: "存储池路径已被 [{{ .name }}] 使用"
ErrVMHelperNotFound: "1panel-kvm 不存在"
@@ -802,3 +804,19 @@ ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应
AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。"
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: "模型目录和分词器目录不能相同或互相包含"
-1
View File
@@ -34,6 +34,5 @@ func Init() {
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.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"))
}
-20
View File
@@ -18,38 +18,18 @@ import (
)
func Init() {
global.LOG.Info("agent hook: init global data start")
initGlobalData()
global.LOG.Info("agent hook: init global data done")
global.LOG.Info("agent hook: handle cronjob status start")
handleCronjobStatus()
global.LOG.Info("agent hook: handle cronjob status done")
global.LOG.Info("agent hook: handle clam status start")
handleClamStatus()
global.LOG.Info("agent hook: handle clam status done")
global.LOG.Info("agent hook: handle record status start")
handleRecordStatus()
global.LOG.Info("agent hook: handle record status done")
global.LOG.Info("agent hook: handle snapshot status start")
handleSnapStatus()
global.LOG.Info("agent hook: handle snapshot status done")
global.LOG.Info("agent hook: handle ollama model status start")
handleOllamaModelStatus()
global.LOG.Info("agent hook: handle ollama model status done")
global.LOG.Info("agent hook: load local dir start")
loadLocalDir()
global.LOG.Info("agent hook: load local dir done")
global.LOG.Info("agent hook: init docker config start")
initDockerConf()
global.LOG.Info("agent hook: init docker config done")
global.LOG.Info("agent hook: init alert task start")
initAlertTask()
global.LOG.Info("agent hook: init alert task done")
global.LOG.Info("agent hook: init monitor db start")
initMonitorDB()
global.LOG.Info("agent hook: init monitor db done")
}
func initGlobalData() {
+18
View File
@@ -0,0 +1,18 @@
package kvm
import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
vmHelper "github.com/1Panel-dev/1Panel/agent/utils/vm/helper"
)
func Init() {
if helperPath, embedded, err := vmHelper.EnsureEmbeddedHelper(); err != nil {
global.LOG.Warnf("sync embedded kvm helper failed, err: %v", err)
} else if embedded {
global.LOG.Infof("embedded kvm helper synced: %s", helperPath)
}
if err := service.NewIVMService().SyncResourcesOnStartup(); err != nil {
global.LOG.Warnf("sync VM resources on startup failed, err: %v", err)
}
}
+5 -2
View File
@@ -58,11 +58,9 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddTimeoutForClam,
migrations.UpdateCronjobSpec,
migrations.UpdateWebsiteSSLAddColumn,
migrations.AddTensorRTLLMModel,
migrations.UpdateMonitorInterval,
migrations.AddMonitorProcess,
migrations.UpdateCronJob,
migrations.UpdateTensorrtLLM,
migrations.AddCommonDescription,
migrations.UpdateDatabase,
migrations.AddGPUMonitor,
@@ -119,6 +117,11 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddQuickDirectory,
migrations.AddLocalModelAccountLink,
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
},
}
+32 -14
View File
@@ -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{
ID: "20251026-update-monitor-interval",
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{
ID: "20251117-add-common-description",
Migrate: func(tx *gorm.DB) error {
@@ -1939,3 +1925,35 @@ var AddOllamaModelAccountSync = &gormigrate.Migration{
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