mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-28 16:00:18 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1121758335 |
@@ -826,26 +826,6 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Switch to the bundled OpenClaw Feishu plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentFeishuSwitchReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/feishu/switch [post]
|
||||
func (b *BaseApi) SwitchAgentFeishuPlugin(c *gin.Context) {
|
||||
var req dto.AgentFeishuSwitchReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.SwitchFeishuPlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Telegram channel config
|
||||
// @Accept json
|
||||
|
||||
+16
-12
@@ -72,6 +72,22 @@ func (b *BaseApi) CloseOllamaModel(c *gin.Context) {
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Sync Ollama model list
|
||||
// @Success 200 {array} dto.OllamaModelDropList
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/ollama/model/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
|
||||
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
|
||||
list, err := aiToolService.Sync()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Ollama models
|
||||
// @Accept json
|
||||
@@ -98,18 +114,6 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
|
||||
var req dto.OllamaModelAccountSync
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := aiToolService.SyncAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Ollama models
|
||||
// @Accept json
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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) AIBenchmarkSources(c *gin.Context) {
|
||||
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, sources)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/update [post]
|
||||
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
|
||||
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
|
||||
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
var req dto.AlertConfigUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -321,15 +321,13 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/status [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||
var req dto.AlertConfigStatusUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -355,14 +353,13 @@ func loadAuditUser(c *gin.Context) string {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/del [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
|
||||
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
||||
var req dto.DeleteRequest
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
err := alertService.DeleteAlertConfig(req.ID)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -161,25 +160,10 @@ func (b *BaseApi) GetAppDetailByID(c *gin.Context) {
|
||||
// @Router /apps/install [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
||||
func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||
if mcpRequest && !trustedMCP {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var req request.AppInstallCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if mcpRequest {
|
||||
req.MCPPreserveWebsiteDir = true
|
||||
if value, exists := req.Params["WEBSITE_DIR"]; exists {
|
||||
directory, ok := value.(string)
|
||||
if !ok || directory == "" || directory != settingService.GetWebsiteDir() {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
install, err := appService.Install(req, true)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -340,25 +336,11 @@ func (b *BaseApi) UpdateAppConfig(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /apps/installed/info/:appInstallId [get]
|
||||
func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
||||
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||
if mcpRequest && !trustedMCP {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
appInstallId, err := helper.GetIntParamByKey(c, "appInstallId")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if mcpRequest {
|
||||
info, err := (&service.AppInstallService{}).GetAppInstallInfoForMCP(c.Request.Context(), appInstallId)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
return
|
||||
}
|
||||
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
total, list, err := containerService.Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,10 +249,9 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||
data, err := containerService.LoadStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -416,14 +415,9 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||
var ids []string
|
||||
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||
}
|
||||
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||
data, err := containerService.ContainerListStats()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) GetContainerImageCheckUpdateStatus(c *gin.Context) {
|
||||
status, err := service.GetContainerImageCheckUpdateStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, status)
|
||||
}
|
||||
func (b *BaseApi) ContainerImageCheckUpdate(c *gin.Context) {
|
||||
taskID, err := service.StartContainerImageCheckUpdate()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
status, err := service.GetContainerImageCheckUpdateStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
status.TaskID = taskID
|
||||
helper.SuccessWithData(c, status)
|
||||
}
|
||||
func (b *BaseApi) SetContainerImageCheckUpdateSchedule(c *gin.Context) {
|
||||
var req struct {
|
||||
Enabled *bool `json:"enabled" validate:"required"`
|
||||
}
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := service.SetContainerImageCheckUpdateSchedule(*req.Enabled); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
b.GetContainerImageCheckUpdateStatus(c)
|
||||
}
|
||||
@@ -2,12 +2,9 @@ package v2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -101,25 +98,11 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
|
||||
// @Param type path string true "type"
|
||||
// @Router /databases/db/list/:type [get]
|
||||
func (b *BaseApi) ListDatabase(c *gin.Context) {
|
||||
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||
if mcpRequest && !trustedMCP {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dbType, err := helper.GetStrParamByKey(c, "type")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if mcpRequest {
|
||||
list, err := (&service.DatabaseService{}).ListForMCP(c.Request.Context(), dbType)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
return
|
||||
}
|
||||
list, err := databaseService.List(dbType)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -3,12 +3,9 @@ package v2
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -22,11 +19,6 @@ import (
|
||||
// @Router /databases [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
||||
func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||
if mcpRequest && !trustedMCP {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var req dto.MysqlDBCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
@@ -40,16 +32,6 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
if mcpRequest {
|
||||
created, err := (&service.MysqlService{}).CreateForMCP(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
nodeID, _ := mcpapi.NodeID(c.Request)
|
||||
helper.SuccessWithData(c, map[string]any{"id": created.ID, "database": created.MysqlName, "name": created.Name, "nodeId": nodeID})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -531,25 +513,11 @@ func (b *BaseApi) LoadRemoteAccess(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /databases/status [post]
|
||||
func (b *BaseApi) LoadStatus(c *gin.Context) {
|
||||
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||
if mcpRequest && !trustedMCP {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var req dto.OperationWithNameAndType
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if mcpRequest {
|
||||
data, err := (&service.MysqlService{}).LoadStatusForMCP(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
return
|
||||
}
|
||||
data, err := mysqlService.LoadStatus(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -3,7 +3,6 @@ package helper
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -12,7 +11,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 +21,6 @@ import (
|
||||
)
|
||||
|
||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Message: "",
|
||||
@@ -34,7 +31,6 @@ 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)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
ErrorCode: businessCode,
|
||||
@@ -45,7 +41,6 @@ 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)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Data: data,
|
||||
@@ -189,23 +184,3 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
||||
intNum, _ := strconv.Atoi(idParam)
|
||||
return int32(intNum), nil
|
||||
}
|
||||
|
||||
// SetOperationLogValues supplies server-resolved values to Core's operation log.
|
||||
// Call before writing the response. Only pass public audit fields, never credentials.
|
||||
func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||
const header = "X-Op-Resolved"
|
||||
resolved := make(map[string]string)
|
||||
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
|
||||
_ = json.Unmarshal(data, &resolved)
|
||||
}
|
||||
if resolved == nil {
|
||||
resolved = make(map[string]string)
|
||||
}
|
||||
for key, value := range values {
|
||||
resolved[key] = value
|
||||
}
|
||||
data, err := json.Marshal(resolved)
|
||||
if err == nil {
|
||||
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary List quick directories
|
||||
// @Success 200 {array} model.QuickDirectory
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [get]
|
||||
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||
items, err := dashboardService.ListQuickDirectories()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, items)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Create quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [post]
|
||||
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.CreateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Update quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/update [post]
|
||||
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Delete quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/del [post]
|
||||
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -61,14 +61,13 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/del [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
|
||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||
var req request.RuntimeDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
name, err := runtimeService.Delete(req)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
err := runtimeService.Delete(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -402,7 +402,7 @@ func (b *BaseApi) ChangeDefaultServer(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/php/version [post]
|
||||
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
||||
var req request.WebsitePHPVersionReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"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/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -179,64 +176,3 @@ func (b *BaseApi) DownloadCAFile(c *gin.Context) {
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||
}
|
||||
|
||||
// @Tags Website CA
|
||||
// @Summary Upload website CA archive
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "ZIP containing ca.crt and ca.key"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/ca/upload [post]
|
||||
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
|
||||
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
|
||||
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||
return
|
||||
}
|
||||
if c.Request.MultipartForm != nil {
|
||||
defer c.Request.MultipartForm.RemoveAll()
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
|
||||
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||
return
|
||||
}
|
||||
stream, err := file.Open()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Website CA
|
||||
// @Summary Import website CA from PEM or server files
|
||||
// @Accept json
|
||||
// @Param request body request.WebsiteCAImport true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/ca/import [post]
|
||||
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
|
||||
var req request.WebsiteCAImport
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := websiteCAService.Import(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
+23
-47
@@ -9,8 +9,7 @@ type AgentCreateReq struct {
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
|
||||
AccessHost string `json:"accessHost"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
||||
Model string `json:"model"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -111,7 +110,6 @@ type AgentBatchOperateResult struct {
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
AccessHost string `json:"accessHost"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
@@ -386,7 +384,6 @@ type AgentAccountProviderCountReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ID uint `json:"id"`
|
||||
MasterAccountID uint `json:"masterAccountId"`
|
||||
Provider string `json:"provider"`
|
||||
@@ -433,53 +430,32 @@ type AgentFeishuConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentFeishuTransition struct {
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Streaming bool `json:"streaming"`
|
||||
GroupSessionScope string `json:"groupSessionScope"`
|
||||
RequireMention string `json:"requireMention"`
|
||||
}
|
||||
|
||||
type AgentFeishuSwitchReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
Streaming bool `json:"streaming"`
|
||||
GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender group_topic group_topic_sender"`
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false"`
|
||||
}
|
||||
|
||||
type AgentFeishuConfigUpdateReq struct {
|
||||
GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode" validate:"required"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode" validate:"required"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentFeishuConfig struct {
|
||||
BuiltinManaged bool `json:"builtinManaged"`
|
||||
LegacyConfigured bool `json:"legacyConfigured"`
|
||||
GroupSessionScope string `json:"groupSessionScope"`
|
||||
BuiltinAvailable bool `json:"builtinAvailable"`
|
||||
Legacy bool `json:"legacy"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentTelegramConfigReq struct {
|
||||
|
||||
+5
-6
@@ -14,17 +14,16 @@ type OllamaModelInfo struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type OllamaModelDropList struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OllamaModelName struct {
|
||||
Name string `json:"name"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type OllamaModelAccountSync struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
}
|
||||
|
||||
type OllamaBindDomain struct {
|
||||
Domain string `json:"domain" validate:"required"`
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package dto
|
||||
|
||||
type AIBenchmarkSource struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Models []string `json:"models"`
|
||||
Tokenizer string `json:"tokenizer"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -43,11 +43,9 @@ type AppOssConfig struct {
|
||||
}
|
||||
|
||||
type AppVersion struct {
|
||||
FeishuLegacy bool `json:"feishuLegacy,omitempty"`
|
||||
FeishuConfigured bool `json:"feishuConfigured,omitempty"`
|
||||
Version string `json:"version"`
|
||||
DetailId uint `json:"detailId"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
Version string `json:"version"`
|
||||
DetailId uint `json:"detailId"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
}
|
||||
|
||||
type AppList struct {
|
||||
|
||||
@@ -21,16 +21,13 @@ 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"`
|
||||
ImageName string `json:"imageName"`
|
||||
CreateTime string `json:"createTime"`
|
||||
State string `json:"state"`
|
||||
RunTime string `json:"runTime"`
|
||||
ContainerID string `json:"containerID"`
|
||||
Name string `json:"name"`
|
||||
ImageId string `json:"imageID"`
|
||||
ImageName string `json:"imageName"`
|
||||
CreateTime string `json:"createTime"`
|
||||
State string `json:"state"`
|
||||
RunTime string `json:"runTime"`
|
||||
|
||||
Network []string `json:"network"`
|
||||
Ports []string `json:"ports"`
|
||||
@@ -174,8 +171,7 @@ type ContainerItemStats struct {
|
||||
BuildCacheReclaimable int64 `json:"buildCacheReclaimable"`
|
||||
}
|
||||
type ContainerListStats struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
ContainerID string `json:"containerID"`
|
||||
|
||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||
SystemUsage uint64 `json:"systemUsage"`
|
||||
|
||||
@@ -31,27 +31,7 @@ type DashboardBase struct {
|
||||
}
|
||||
|
||||
type ChangeQuicks struct {
|
||||
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type QuickVisibility struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
IsShow bool `json:"isShow"`
|
||||
}
|
||||
|
||||
type QuickDirectoryCreate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
type QuickDirectoryDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Legacy bool `json:"legacy"`
|
||||
}
|
||||
|
||||
type QuickDirectoryUpdate struct {
|
||||
QuickDirectoryDelete
|
||||
QuickDirectoryCreate
|
||||
Quicks []QuickJump `json:"quicks"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
|
||||
@@ -26,7 +26,6 @@ type FirewallSubsystemStatus struct {
|
||||
}
|
||||
|
||||
type FirewallLifecycleOperation struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -145,11 +144,6 @@ type MonitorVLLMCurrent struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorVLLMCurrentData struct {
|
||||
model.MonitorVLLM
|
||||
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
type MonitorVLLMClean struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@ type AppInstallCreate struct {
|
||||
Services map[string]string `json:"services"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
|
||||
MCPPreserveWebsiteDir bool `json:"-"`
|
||||
|
||||
AppContainerConfig
|
||||
NodePushConfig
|
||||
}
|
||||
@@ -89,33 +83,31 @@ type AppBackupDelete struct {
|
||||
}
|
||||
|
||||
type AppInstalledOperate struct {
|
||||
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
BackupId uint `json:"backupId"`
|
||||
DetailId uint `json:"detailId"`
|
||||
Operate constant.AppOperate `json:"operate" validate:"required"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
DeleteBackup bool `json:"deleteBackup"`
|
||||
DeleteDB bool `json:"deleteDB"`
|
||||
Backup bool `json:"backup"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
TaskID string `json:"taskID"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
Favorite bool `json:"favorite"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
BackupId uint `json:"backupId"`
|
||||
DetailId uint `json:"detailId"`
|
||||
Operate constant.AppOperate `json:"operate" validate:"required"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
DeleteBackup bool `json:"deleteBackup"`
|
||||
DeleteDB bool `json:"deleteDB"`
|
||||
Backup bool `json:"backup"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
TaskID string `json:"taskID"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstallUpgrade struct {
|
||||
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||
InstallID uint `json:"installId"`
|
||||
DetailID uint `json:"detailId"`
|
||||
Backup bool `json:"backup"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
TaskID string `json:"taskID"`
|
||||
InstallID uint `json:"installId"`
|
||||
DetailID uint `json:"detailId"`
|
||||
Backup bool `json:"backup"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type AppInstallDelete struct {
|
||||
@@ -130,12 +122,9 @@ type AppInstallDelete struct {
|
||||
}
|
||||
|
||||
type AppInstalledUpdate struct {
|
||||
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
AppContainerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -182,12 +182,3 @@ type WebsiteSSLFileUpload struct {
|
||||
PushNode bool `json:"pushNode"`
|
||||
Nodes string `json:"nodes"`
|
||||
}
|
||||
|
||||
type WebsiteCAImport struct {
|
||||
Name string `json:"name" validate:"required,max=128"`
|
||||
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||
Certificate string `json:"certificate" validate:"required_if=Type paste"`
|
||||
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
|
||||
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
|
||||
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
|
||||
}
|
||||
|
||||
@@ -50,21 +50,19 @@ type TagDTO struct {
|
||||
}
|
||||
|
||||
type AppInstalledCheck struct {
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
IsExist bool `json:"isExist"`
|
||||
Name string `json:"name"`
|
||||
App string `json:"app"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastBackupAt string `json:"lastBackupAt"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ContainerName string `json:"containerName"`
|
||||
InstallPath string `json:"installPath"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
HttpsPort int `json:"httpsPort"`
|
||||
WebsiteDir string `json:"websiteDir"`
|
||||
IsExist bool `json:"isExist"`
|
||||
Name string `json:"name"`
|
||||
App string `json:"app"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastBackupAt string `json:"lastBackupAt"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ContainerName string `json:"containerName"`
|
||||
InstallPath string `json:"installPath"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
HttpsPort int `json:"httpsPort"`
|
||||
WebsiteDir string `json:"websiteDir"`
|
||||
}
|
||||
|
||||
type AppDetailDTO struct {
|
||||
@@ -186,13 +184,7 @@ type AppParam struct {
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
AppKey string `json:"appKey"`
|
||||
Name string `json:"name"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
SyncModelAccount bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
Params []AppParam `json:"params"`
|
||||
RawCompose string `json:"rawCompose"`
|
||||
Params []AppParam `json:"params"`
|
||||
RawCompose string `json:"rawCompose"`
|
||||
request.AppContainerConfig
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package model
|
||||
|
||||
type AgentAccount struct {
|
||||
BaseModel
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
|
||||
@@ -28,10 +28,6 @@ type AppInstall struct {
|
||||
Favorite bool `json:"favorite"`
|
||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||
|
||||
SyncModelAccount bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
|
||||
App App `json:"app" gorm:"-:migration"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,6 @@ type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type QuickDirectory struct {
|
||||
BaseModel
|
||||
Path string `json:"path"`
|
||||
Alias string `json:"alias"`
|
||||
Legacy bool `json:"legacy" gorm:"-"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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" }
|
||||
@@ -86,9 +86,7 @@ type MonitorVLLM struct {
|
||||
RawMetrics string `json:"-"`
|
||||
HistogramDeltas string `json:"-"`
|
||||
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||
SampleIntervalSeconds int `json:"-"`
|
||||
RequestDeltas string `json:"-"`
|
||||
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
|
||||
QueueTime *float64 `json:"queueTime"`
|
||||
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
|
||||
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
|
||||
|
||||
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
|
||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||
},
|
||||
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||
@@ -82,6 +82,7 @@ var catalog = map[string]Meta{
|
||||
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||
{ID: "glm-5", Name: "GLM-5"},
|
||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||
},
|
||||
@@ -113,7 +114,7 @@ var catalog = map[string]Meta{
|
||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||
},
|
||||
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
|
||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
||||
},
|
||||
"xiaomi": {
|
||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMiniMaxCatalog(t *testing.T) {
|
||||
meta, ok := Get("minimax")
|
||||
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
|
||||
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
|
||||
}
|
||||
if meta.DefaultAPIType != "anthropic-messages" {
|
||||
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
|
||||
}
|
||||
for _, provider := range All() {
|
||||
for _, model := range provider.Models {
|
||||
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
|
||||
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxVerificationRequests(t *testing.T) {
|
||||
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
|
||||
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
|
||||
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
|
||||
base, endpoint := region+"/v1", "/chat/completions"
|
||||
if protocol == "anthropic-messages" {
|
||||
base, endpoint = region+"/anthropic", "/v1/messages"
|
||||
}
|
||||
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
|
||||
if built.URL != base+endpoint {
|
||||
t.Fatalf("URL = %s", built.URL)
|
||||
}
|
||||
var captured map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
|
||||
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
|
||||
t.Error("incorrect API key authentication")
|
||||
}
|
||||
} else if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||
t.Error("incorrect bearer authentication")
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
|
||||
t.Fatalf("unexpected serialized body: %#v", captured)
|
||||
}
|
||||
if model == "MiniMax-M3.1-Flash" {
|
||||
if protocol == "anthropic-messages" {
|
||||
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
|
||||
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
|
||||
}
|
||||
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
|
||||
}
|
||||
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
|
||||
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
|
||||
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
|
||||
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
|
||||
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(request.Body, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
|
||||
t.Fatalf("changed explicit %s request: %#v", provider, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,15 +118,10 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
headers["x-api-key"] = apiKey
|
||||
}
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
body := map[string]interface{}{
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["thinking"] = map[string]string{"type": "adaptive"}
|
||||
body["output_config"] = map[string]string{"effort": "max"}
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
})
|
||||
case "openai-responses":
|
||||
request.URL = baseURL + "/responses"
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
@@ -136,13 +131,9 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["reasoning_effort"] = "max"
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
})
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -19,7 +18,6 @@ type IAgentAccountRepo interface {
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -99,25 +97,6 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) SaveLocal(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
|
||||
}
|
||||
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range models {
|
||||
models[i].AccountID = account.ID
|
||||
models[i].SortOrder = i + 1
|
||||
}
|
||||
return tx.Create(&models).Error
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeProviders(providers []string) []string {
|
||||
seen := make(map[string]struct{}, len(providers))
|
||||
result := make([]string, 0, len(providers))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
@@ -16,7 +14,6 @@ type IAiRepo interface {
|
||||
Create(cronjob *model.OllamaModel) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteAll(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIAiRepo() IAiRepo {
|
||||
@@ -70,7 +67,3 @@ func (u *AiRepo) Delete(opts ...DBOption) error {
|
||||
}
|
||||
return db.Delete(&model.OllamaModel{}).Error
|
||||
}
|
||||
|
||||
func (u *AiRepo) DeleteAll(ctx context.Context) error {
|
||||
return getTx(ctx).Where("1 = 1").Delete(&model.OllamaModel{}).Error
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package repo
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LauncherRepo struct{}
|
||||
@@ -18,11 +17,6 @@ type ILauncherRepo interface {
|
||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||
ListQuickJump(withAll bool) []model.QuickJump
|
||||
UpdateQuicks(quicks []model.QuickJump) error
|
||||
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||
CountQuickDirectories() (int64, error)
|
||||
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||
DeleteQuickDirectory(id uint, legacy bool) error
|
||||
}
|
||||
|
||||
func NewILauncherRepo() ILauncherRepo {
|
||||
@@ -101,73 +95,13 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
||||
for _, item := range quicks {
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"is_show": item.IsShow,
|
||||
"detail": item.Detail,
|
||||
"alias": item.Alias,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||
}
|
||||
|
||||
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||
items := make([]model.QuickDirectory, 0)
|
||||
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var quicks []model.QuickJump
|
||||
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, quick := range quicks {
|
||||
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||
var count, legacyCount int64
|
||||
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count + legacyCount, nil
|
||||
}
|
||||
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||
return global.DB.Create(item).Error
|
||||
}
|
||||
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||
var result *gorm.DB
|
||||
if item.Legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||
} else {
|
||||
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||
var result *gorm.DB
|
||||
if legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||
} else {
|
||||
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ type VLLMMonitorRepo struct{}
|
||||
|
||||
type VLLMHistoryPoint struct {
|
||||
model.MonitorVLLM
|
||||
Bucket int64
|
||||
CompletedRequests *float64
|
||||
Bucket int64
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||
@@ -78,19 +77,12 @@ func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
|
||||
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
||||
if group == "" || group == "requests" {
|
||||
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||
}
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
if group == "" || group == "overview" {
|
||||
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
|
||||
if seconds > 0 {
|
||||
count = "SUM(" + count + ")"
|
||||
}
|
||||
columns = append(columns, count+" AS completed_requests")
|
||||
}
|
||||
var points []VLLMHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||
return points, err
|
||||
|
||||
@@ -88,7 +88,6 @@ type IAgentService interface {
|
||||
|
||||
GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
|
||||
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
|
||||
SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error
|
||||
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
|
||||
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
|
||||
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
|
||||
@@ -143,9 +142,6 @@ const (
|
||||
|
||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
agentType := req.AgentType
|
||||
if err := validateNativeAgentCreate(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -241,8 +237,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
}
|
||||
} else if agentType == constant.AppCopaw {
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -251,12 +245,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.HostIP: "",
|
||||
}
|
||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||
}
|
||||
if agentType == constant.AppOctop {
|
||||
params["OCTOP_LOG_LEVEL"] = "info"
|
||||
}
|
||||
if agentType == constant.AppOpenclaw {
|
||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||
@@ -750,7 +738,7 @@ func requiresAgentAccount(agentType string) bool {
|
||||
}
|
||||
|
||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
@@ -1056,7 +1044,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
return err
|
||||
}
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1109,15 +1097,12 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
}
|
||||
verifyModel := ""
|
||||
if account.AppInstallID == 0 || len(models) > 0 {
|
||||
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
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)
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1131,7 +1116,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
if err := global.DB.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
@@ -1162,34 +1147,13 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
installIDs := make([]uint, 0, len(list))
|
||||
for _, account := range list {
|
||||
if account.AppInstallID != 0 {
|
||||
installIDs = append(installIDs, account.AppInstallID)
|
||||
}
|
||||
}
|
||||
localInstalls := make(map[uint]bool, len(installIDs))
|
||||
if len(installIDs) > 0 {
|
||||
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, install := range installs {
|
||||
localInstalls[install.ID] = true
|
||||
}
|
||||
}
|
||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
apiKey := ""
|
||||
if item.RememberAPIKey {
|
||||
apiKey = item.APIKey
|
||||
}
|
||||
appInstallID := uint(0)
|
||||
if localInstalls[item.AppInstallID] {
|
||||
appInstallID = item.AppInstallID
|
||||
}
|
||||
items = append(items, dto.AgentAccountInfo{
|
||||
AppInstallID: appInstallID,
|
||||
ID: item.ID,
|
||||
MasterAccountID: item.MasterAccountID,
|
||||
Provider: item.Provider,
|
||||
@@ -1387,27 +1351,13 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
||||
}
|
||||
|
||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.AppInstallID != 0 && account.Provider != "ollama" {
|
||||
_, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||
if err == nil {
|
||||
return buserr.WithDetail("ErrInvalidParams", "Delete the local inference engine instead", nil)
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var agentCount int64
|
||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||
@@ -1592,10 +1542,6 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -45,29 +45,9 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
result := extractFeishuConfig(conf)
|
||||
result.Installed, _ = checkPluginInstalled(install.GetPath(), "feishu")
|
||||
return &result, nil
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := extractFeishuConfig(conf)
|
||||
if !legacy {
|
||||
result = extractBundledFeishuConfig(conf)
|
||||
}
|
||||
result.BuiltinManaged = true
|
||||
result.BuiltinAvailable = hasBundledFeishu(install.ContainerName)
|
||||
result.Installed = result.BuiltinAvailable
|
||||
result.Legacy, result.LegacyConfigured = legacy, configured
|
||||
if legacy {
|
||||
result.GroupSessionScope = "group"
|
||||
if result.ThreadSession {
|
||||
result.GroupSessionScope = "group_topic"
|
||||
}
|
||||
}
|
||||
installed, _ := checkPluginInstalled(install.GetPath(), "feishu")
|
||||
result.Installed = installed
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -92,9 +72,6 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
|
||||
})
|
||||
})
|
||||
}
|
||||
if agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return saveBundledFeishuConfig(agent, install, req)
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
config := dto.AgentFeishuConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -381,9 +358,6 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -429,9 +403,6 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -472,9 +443,6 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -570,13 +538,10 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return &dto.AgentPluginStatus{Installed: hasBundledFeishu(install.ContainerName)}, nil
|
||||
}
|
||||
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,677 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"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/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
)
|
||||
|
||||
// Keep panel configuration writes out of the backup/activation/recovery window.
|
||||
var feishuSwitches sync.Map
|
||||
|
||||
func isOpenclawBundledFeishuVersion(version string) bool {
|
||||
return common.CompareAppVersion(version, "2026.10.7")
|
||||
}
|
||||
|
||||
func hasBundledFeishu(container string) bool {
|
||||
image, err := feishuContainerImage(container)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = bundledFeishuImage(image)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func usesBundledFeishu(conf map[string]interface{}) bool {
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
for _, key := range []string{"threadSession", "replyMode"} {
|
||||
if _, old := f[key]; old {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if _, old := f["streaming"].(bool); old {
|
||||
return false
|
||||
}
|
||||
entries := childMap(childMap(conf, "plugins"), "entries")
|
||||
if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) {
|
||||
return false
|
||||
}
|
||||
_, streaming := f["streaming"].(map[string]interface{})
|
||||
_, selected := entries["feishu"]
|
||||
return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming)
|
||||
}
|
||||
|
||||
func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool {
|
||||
legacy, _, err := classifyLegacyFeishu(conf, installPath)
|
||||
return err != nil || legacy
|
||||
}
|
||||
|
||||
func selectBundledFeishu(conf map[string]interface{}, enabled bool) {
|
||||
plugins := ensureChildMap(conf, "plugins")
|
||||
entries := ensureChildMap(plugins, "entries")
|
||||
if legacy, ok := entries["openclaw-lark"]; ok {
|
||||
if entry, ok := legacy.(map[string]interface{}); ok {
|
||||
entry["enabled"] = false
|
||||
}
|
||||
}
|
||||
ensureChildMap(entries, "feishu")["enabled"] = enabled
|
||||
// Preserve an absent allowlist; creating one would disable unrelated plugins.
|
||||
if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 {
|
||||
ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" })
|
||||
if !slices.Contains(ids, "feishu") {
|
||||
ids = append(ids, "feishu")
|
||||
}
|
||||
plugins["allow"] = ids
|
||||
}
|
||||
if deny, ok := plugins["deny"]; ok {
|
||||
plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" })
|
||||
}
|
||||
}
|
||||
|
||||
func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) }
|
||||
|
||||
func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error {
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
if len(f) == 0 {
|
||||
if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil {
|
||||
if _, ok := raw.(map[string]interface{}); !ok {
|
||||
return feishuConfigError("channels.feishu")
|
||||
}
|
||||
}
|
||||
f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||
f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist"
|
||||
}
|
||||
if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convertFeishuAccess(f, "channels.feishu"); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(f, "threadSession")
|
||||
delete(f, "replyMode")
|
||||
f["streaming"] = map[string]interface{}{"mode": "off"}
|
||||
if req.Streaming {
|
||||
f["streaming"] = map[string]interface{}{"mode": "partial"}
|
||||
}
|
||||
f["groupSessionScope"] = req.GroupSessionScope
|
||||
f["requireMention"] = req.RequireMention == "true"
|
||||
delete(f, "topicSessionMode")
|
||||
// Preserve legacy defaults explicitly; bundled defaults are more restrictive.
|
||||
if _, ok := f["groupPolicy"]; !ok {
|
||||
f["groupPolicy"] = "open"
|
||||
}
|
||||
entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries")
|
||||
ensureChildMap(entries, "openclaw-lark")["enabled"] = false
|
||||
selectBundledFeishu(conf, extractBoolValue(f["enabled"], true))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Limit automatic conversion to the shared channel fields; custom plugin options require review.
|
||||
func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error {
|
||||
allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread")
|
||||
if group {
|
||||
allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread")
|
||||
}
|
||||
for _, key := range sortedChildKeys(f) {
|
||||
if !slices.Contains(allowed, key) {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
if enabled, exists := f["enabled"]; exists {
|
||||
if _, ok := enabled.(bool); !ok {
|
||||
return feishuConfigError(field + ".enabled")
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"dmPolicy", "groupPolicy"} {
|
||||
if raw, exists := f[key]; exists {
|
||||
value, ok := raw.(string)
|
||||
options := []string{"open", "allowlist", "disabled"}
|
||||
if key == "dmPolicy" {
|
||||
options = append(options, "pairing")
|
||||
}
|
||||
if !ok || !slices.Contains(options, value) {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} {
|
||||
if raw, exists := f[key]; exists {
|
||||
values, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
for _, value := range values {
|
||||
if _, ok := value.(string); !ok {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, section := range []string{"accounts", "groups"} {
|
||||
raw, exists := f[section]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
entries, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + "." + section)
|
||||
}
|
||||
for _, id := range sortedChildKeys(entries) {
|
||||
entry, ok := entries[id].(map[string]interface{})
|
||||
name := field + "." + section + "." + id
|
||||
if !ok {
|
||||
return feishuConfigError(name)
|
||||
}
|
||||
if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertFeishuAccess(f map[string]interface{}, field string) error {
|
||||
if raw, ok := f["groupAllowFrom"]; ok {
|
||||
values, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
groups, senders := []string{}, []string{}
|
||||
for _, value := range values {
|
||||
id, ok := value.(string)
|
||||
if !ok {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(id, "oc_"):
|
||||
groups = append(groups, id)
|
||||
case strings.HasPrefix(id, "ou_"):
|
||||
senders = append(senders, id)
|
||||
default:
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
}
|
||||
// A sender-only legacy allowlist has different group admission semantics.
|
||||
if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
f["groupAllowFrom"] = groups
|
||||
if len(senders) > 0 {
|
||||
f["groupSenderAllowFrom"] = senders
|
||||
}
|
||||
}
|
||||
for _, section := range []string{"accounts", "groups"} {
|
||||
for id, value := range childMap(f, section) {
|
||||
entry, ok := value.(map[string]interface{})
|
||||
name := field + "." + section + "." + id
|
||||
if !ok {
|
||||
return feishuConfigError(name)
|
||||
}
|
||||
// Account/group overrides must be resolved explicitly, not flattened.
|
||||
for _, key := range []string{"threadSession", "replyMode"} {
|
||||
if _, ok := entry[key]; ok {
|
||||
return feishuConfigError(name + "." + key)
|
||||
}
|
||||
}
|
||||
if mention, ok := entry["requireMention"]; ok {
|
||||
if _, ok := mention.(bool); !ok {
|
||||
return feishuConfigError(name + ".requireMention")
|
||||
}
|
||||
}
|
||||
if raw, exists := entry["streaming"]; exists {
|
||||
streaming, ok := raw.(bool)
|
||||
if !ok || streaming {
|
||||
return feishuConfigError(name + ".streaming")
|
||||
}
|
||||
entry["streaming"] = map[string]interface{}{"mode": "off"}
|
||||
}
|
||||
if err := convertFeishuAccess(entry, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||
result := extractFeishuConfig(conf)
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true)
|
||||
result.GroupSessionScope = extractStringValue(f["groupSessionScope"])
|
||||
if result.GroupSessionScope == "" {
|
||||
result.GroupSessionScope = "group"
|
||||
}
|
||||
result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off"
|
||||
result.GroupPolicy = extractStringValue(f["groupPolicy"])
|
||||
if result.GroupPolicy == "" {
|
||||
result.GroupPolicy = "allowlist"
|
||||
}
|
||||
if len(f) == 0 {
|
||||
result.Streaming = false
|
||||
result.RequireMention = "true"
|
||||
} else if _, ok := f["requireMention"]; !ok {
|
||||
result.RequireMention = "open"
|
||||
}
|
||||
accounts := childMap(f, "accounts")
|
||||
ids := sortedChildKeys(accounts)
|
||||
if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") {
|
||||
ids = append([]string{"default"}, ids...)
|
||||
}
|
||||
result.Bots = []dto.AgentFeishuBot{}
|
||||
for _, id := range ids {
|
||||
effective := make(map[string]interface{}, len(f))
|
||||
for k, v := range f {
|
||||
effective[k] = v
|
||||
}
|
||||
for k, v := range childMap(accounts, id) {
|
||||
effective[k] = v
|
||||
}
|
||||
bot := defaultFeishuBot()
|
||||
bot.AccountID, bot.IsDefault = id, id == "default"
|
||||
bot.Name = extractDisplayName(childMap(accounts, id), "", id)
|
||||
bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true)
|
||||
bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"])
|
||||
bot.DmPolicy = extractStringValue(effective["dmPolicy"])
|
||||
if bot.DmPolicy == "" {
|
||||
bot.DmPolicy = "pairing"
|
||||
}
|
||||
bot.AllowFrom = extractStringList(effective["allowFrom"])
|
||||
result.Bots = append(result.Bots, bot)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error {
|
||||
if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) {
|
||||
return feishuConfigError("groupSessionScope")
|
||||
}
|
||||
f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||
accounts := childMap(f, "accounts")
|
||||
seen := map[string]bool{}
|
||||
appIDs := map[string]bool{}
|
||||
for _, bot := range req.Bots {
|
||||
if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) {
|
||||
return feishuConfigError("accounts")
|
||||
}
|
||||
seen[bot.AccountID] = true
|
||||
appIDs[bot.AppID] = true
|
||||
entry := childMap(accounts, bot.AccountID)
|
||||
if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" {
|
||||
entry["appSecret"] = f["appSecret"]
|
||||
}
|
||||
// SecretRef credentials cannot be represented by the form: keep them intact.
|
||||
if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" {
|
||||
return feishuConfigError("accounts." + bot.AccountID + ".appSecret")
|
||||
}
|
||||
entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy
|
||||
if bot.AppSecret != "" {
|
||||
entry["appSecret"] = bot.AppSecret
|
||||
}
|
||||
switch bot.DmPolicy {
|
||||
case "open":
|
||||
entry["allowFrom"] = []string{"*"}
|
||||
case "allowlist":
|
||||
entry["allowFrom"] = bot.AllowFrom
|
||||
default:
|
||||
if len(bot.AllowFrom) > 0 {
|
||||
entry["allowFrom"] = bot.AllowFrom
|
||||
} else {
|
||||
entry["allowFrom"] = []string{}
|
||||
}
|
||||
}
|
||||
accounts[bot.AccountID] = entry
|
||||
}
|
||||
for id := range accounts {
|
||||
if !seen[id] {
|
||||
delete(accounts, id)
|
||||
}
|
||||
}
|
||||
if !seen["default"] {
|
||||
delete(f, "appId")
|
||||
delete(f, "appSecret")
|
||||
}
|
||||
if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] {
|
||||
return feishuConfigError("defaultAccount")
|
||||
}
|
||||
if len(accounts) > 0 {
|
||||
f["accounts"] = accounts
|
||||
} else {
|
||||
delete(f, "accounts")
|
||||
}
|
||||
f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy
|
||||
if req.RequireMention == "open" {
|
||||
delete(f, "requireMention")
|
||||
} else {
|
||||
f["requireMention"] = req.RequireMention == "true"
|
||||
}
|
||||
if req.GroupPolicy == "allowlist" {
|
||||
f["groupAllowFrom"] = req.GroupAllowFrom
|
||||
} else {
|
||||
delete(f, "groupAllowFrom")
|
||||
}
|
||||
streaming := ensureChildMap(f, "streaming")
|
||||
streaming["mode"] = "off"
|
||||
if req.Streaming {
|
||||
streaming["mode"] = "partial"
|
||||
}
|
||||
selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write beside the original so validation and rename preserve the mounted filesystem and owner.
|
||||
func stageFeishuConfig(configPath string, payload []byte) (string, error) {
|
||||
info, err := os.Lstat(configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", feishuConfigError("openclaw.json")
|
||||
}
|
||||
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := f.Name()
|
||||
defer func() {
|
||||
f.Close()
|
||||
if err != nil {
|
||||
os.Remove(name)
|
||||
}
|
||||
}()
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
err = f.Chown(int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Chmod(info.Mode().Perm())
|
||||
}
|
||||
if err == nil {
|
||||
_, err = f.Write(payload)
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Close()
|
||||
}
|
||||
return name, err
|
||||
}
|
||||
|
||||
func validateFeishuConfig(container, configPath string) error {
|
||||
out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json")
|
||||
return feishuValidationResult(out, err)
|
||||
}
|
||||
|
||||
func replaceFeishuConfig(configPath string, expected, payload []byte) error {
|
||||
tmp, err := stageFeishuConfig(configPath, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp)
|
||||
current, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, expected) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
return os.Rename(tmp, configPath)
|
||||
}
|
||||
|
||||
func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error {
|
||||
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
started := false
|
||||
defer func() {
|
||||
if !started {
|
||||
feishuSwitches.Delete(agent.ConfigPath)
|
||||
}
|
||||
}()
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasBundledFeishu(install.ContainerName) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !legacy {
|
||||
return buserr.New("ErrFeishuAlreadySwitched")
|
||||
}
|
||||
if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||
return err
|
||||
}
|
||||
switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil)
|
||||
started = true
|
||||
go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) {
|
||||
original, conf, err := readFeishuConfigBytes(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !legacy {
|
||||
return buserr.New("ErrFeishuAlreadySwitched")
|
||||
}
|
||||
image, err := feishuContainerImage(install.ContainerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
image, err = bundledFeishuImage(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recovery, err := feishuRecoveryPayload(payload, configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate both candidates without invoking Doctor or mutating retained state.
|
||||
for _, candidate := range [][]byte{payload, recovery} {
|
||||
staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate)
|
||||
if stageErr != nil {
|
||||
return stageErr
|
||||
}
|
||||
validateErr := validateFeishuImageConfig(image, staged)
|
||||
os.Remove(staged)
|
||||
if validateErr != nil {
|
||||
return validateErr
|
||||
}
|
||||
}
|
||||
if _, err = compose.Stop(install.GetComposePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
replaced := false
|
||||
defer func() {
|
||||
if !replaced {
|
||||
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr))
|
||||
return
|
||||
}
|
||||
if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr))
|
||||
return
|
||||
}
|
||||
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||
return
|
||||
}
|
||||
if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr))
|
||||
return
|
||||
}
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||
}()
|
||||
current, err := os.ReadFile(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, original) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
backup, err := backupFeishuConfig(agent.ConfigPath, original)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup)))
|
||||
if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
replaced = true
|
||||
if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil {
|
||||
return err
|
||||
}
|
||||
enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
|
||||
if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
probeFeishuConnection(install.ContainerName, t)
|
||||
}
|
||||
if configured {
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages"))
|
||||
} else {
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error {
|
||||
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasBundledFeishu(install.ContainerName) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
original, err := os.ReadFile(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var conf map[string]interface{}
|
||||
if err = json.Unmarshal(original, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasLegacyFeishu(conf, install.GetPath()) {
|
||||
return buserr.New("ErrFeishuSwitchRequired")
|
||||
}
|
||||
if err = setBundledFeishuConfig(conf, req); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
staged, err := stageFeishuConfig(agent.ConfigPath, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(staged)
|
||||
if err = validateFeishuConfig(install.ContainerName, staged); err != nil {
|
||||
return err
|
||||
}
|
||||
return replaceFeishuConfig(agent.ConfigPath, original, payload)
|
||||
}
|
||||
|
||||
func feishuProbeOK(output string) bool {
|
||||
raw, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var status struct {
|
||||
ChannelAccounts map[string][]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Configured bool `json:"configured"`
|
||||
Running bool `json:"running"`
|
||||
Probe struct {
|
||||
OK bool `json:"ok"`
|
||||
} `json:"probe"`
|
||||
} `json:"channelAccounts"`
|
||||
}
|
||||
if json.Unmarshal(raw, &status) != nil {
|
||||
return false
|
||||
}
|
||||
accounts, ok := status.ChannelAccounts["feishu"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func feishuGatewayReady(output string, enabled bool) bool {
|
||||
raw, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var health struct {
|
||||
OK bool `json:"ok"`
|
||||
Plugins struct {
|
||||
Loaded []string `json:"loaded"`
|
||||
} `json:"plugins"`
|
||||
}
|
||||
return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu"))
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// Inspect a stopped disposable container; its entrypoint never touches retained state.
|
||||
func bundledFeishuImage(image string) (string, error) {
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
info, _, err := cli.ImageInspectWithRaw(ctx, image)
|
||||
if err != nil {
|
||||
return "", buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
cleanup, done := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer done()
|
||||
_ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true})
|
||||
}()
|
||||
if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil {
|
||||
return "", buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
func feishuContainerImage(name string) (string, error) {
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
info, err := cli.ContainerInspect(ctx, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.Image, nil
|
||||
}
|
||||
|
||||
// The original state is read-only and the normal Doctor entrypoint is bypassed.
|
||||
func validateFeishuImageConfig(image, configPath string) error {
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uid, gid := uint32(1000), uint32(1000)
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
uid, gid = stat.Uid, stat.Gid
|
||||
}
|
||||
user := fmt.Sprintf("%d:%d", uid, gid)
|
||||
tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid)
|
||||
name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json")
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
||||
defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }()
|
||||
out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user,
|
||||
"--tmpfs", tmpfs,
|
||||
"--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly",
|
||||
"--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state",
|
||||
"--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath),
|
||||
"--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json")
|
||||
return feishuValidationResult(out, runErr)
|
||||
}
|
||||
|
||||
func feishuValidationResult(out string, runErr error) error {
|
||||
raw, _ := extractEmbeddedJSON(out)
|
||||
var result struct {
|
||||
Valid bool `json:"valid"`
|
||||
Issues []struct {
|
||||
Path string `json:"path"`
|
||||
} `json:"issues"`
|
||||
}
|
||||
if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil {
|
||||
return nil
|
||||
}
|
||||
fields := []string{}
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path != "" {
|
||||
fields = append(fields, issue.Path)
|
||||
}
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
fields = append(fields, "openclaw.json")
|
||||
}
|
||||
return feishuConfigError(strings.Join(fields, ", "))
|
||||
}
|
||||
|
||||
func backupFeishuConfig(configPath string, original []byte) (string, error) {
|
||||
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = f.Write(original); err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
closeErr := f.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", closeErr
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// Doctor can spend 94 seconds waiting for the previous container's owner lease.
|
||||
func waitOpenclawReady(name string) error {
|
||||
script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')`
|
||||
deadline := time.Now().Add(4 * time.Minute)
|
||||
for time.Now().Before(deadline) {
|
||||
if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
|
||||
func verifyFeishuActivation(name string, enabled bool) error {
|
||||
if err := waitOpenclawReady(name); err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json")
|
||||
if err != nil {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
raw, _ := extractEmbeddedJSON(output)
|
||||
var result struct {
|
||||
Plugins []struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Origin string `json:"origin"`
|
||||
} `json:"plugins"`
|
||||
}
|
||||
if json.Unmarshal(raw, &result) != nil {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
found := false
|
||||
for _, p := range result.Plugins {
|
||||
if p.ID == "openclaw-lark" && p.Status != "disabled" {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json")
|
||||
if err != nil || !feishuGatewayReady(health, enabled) {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) {
|
||||
info, err := os.Lstat(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, nil, feishuConfigError("openclaw.json")
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
raw, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var conf map[string]interface{}
|
||||
if err = json.Unmarshal(raw, &conf); err != nil || conf == nil {
|
||||
return nil, nil, feishuConfigError("openclaw.json")
|
||||
}
|
||||
return raw, conf, nil
|
||||
}
|
||||
@@ -8,15 +8,11 @@ import (
|
||||
)
|
||||
|
||||
const hermesWeixinLoginScript = `import asyncio
|
||||
try:
|
||||
from plugins.platforms.weixin.adapter import qr_login
|
||||
except ModuleNotFoundError as exc:
|
||||
if exc.name not in ('plugins', 'plugins.platforms', 'plugins.platforms.weixin', 'plugins.platforms.weixin.adapter'):
|
||||
raise
|
||||
from gateway.platforms.weixin import qr_login
|
||||
from gateway.platforms.weixin import qr_login
|
||||
from hermes_cli.config import save_env_value
|
||||
|
||||
creds = asyncio.run(qr_login('/opt/data'))
|
||||
print('RESULT =', creds)
|
||||
|
||||
if not creds:
|
||||
raise SystemExit(1)
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/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) {
|
||||
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||
return nil, fmt.Errorf("invalid local model installation")
|
||||
}
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||
return nil, fmt.Errorf("model account belongs to another installation")
|
||||
}
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if !createIfMissing {
|
||||
return nil, nil
|
||||
}
|
||||
name := install.Name
|
||||
for suffix := 1; ; suffix++ {
|
||||
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
break
|
||||
}
|
||||
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||
}
|
||||
apiType, apiKey := "openai-completions", "vllm-local"
|
||||
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}
|
||||
}
|
||||
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||
account.AppInstallID = install.ID
|
||||
if updateBaseURL {
|
||||
account.BaseURL = baseURL
|
||||
}
|
||||
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||
verifyModelExists := account.VerifyModel == ""
|
||||
for _, item := range models {
|
||||
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||
if item.ID == account.VerifyModel {
|
||||
verifyModelExists = true
|
||||
}
|
||||
}
|
||||
if !verifyModelExists {
|
||||
account.VerifyModel = ""
|
||||
accountChanged = true
|
||||
}
|
||||
var previous []model.AgentAccountModel
|
||||
if account.ID != 0 {
|
||||
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
modelsChanged := len(previous) != len(rows)
|
||||
if !modelsChanged {
|
||||
for i := range rows {
|
||||
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||
modelsChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if modelsChanged {
|
||||
err = agentAccountRepo.SaveLocal(account, rows)
|
||||
} else if accountChanged {
|
||||
err = agentAccountRepo.Save(account)
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !install.SyncModelAccount {
|
||||
return nil
|
||||
}
|
||||
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
|
||||
}
|
||||
|
||||
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
|
||||
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||
for _, item := range models {
|
||||
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||
}
|
||||
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||
addressType := install.ModelAccountBaseURLType
|
||||
if addressType == "" {
|
||||
addressType = "container"
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return addressType, install.ModelAccountBaseURL, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if addressType != "custom" {
|
||||
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
|
||||
addressType = "custom"
|
||||
}
|
||||
}
|
||||
return addressType, account.BaseURL, nil
|
||||
}
|
||||
|
||||
func CleanupLocalModelAccount(installID uint) {
|
||||
if installID == 0 {
|
||||
return
|
||||
}
|
||||
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load local model accounts: %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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||
switch addressType {
|
||||
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 baseURL, nil
|
||||
case "localhost":
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||
case "systemIP":
|
||||
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
systemIP = strings.TrimSpace(systemIP)
|
||||
if systemIP == "" {
|
||||
systemIP = "127.0.0.1"
|
||||
}
|
||||
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||
default:
|
||||
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
|
||||
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
|
||||
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
|
||||
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
|
||||
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var actual modelEntry
|
||||
if err := json.Unmarshal(payload, &actual); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual.ID != id || actual.Name != "Saved name" {
|
||||
t.Fatalf("changed saved identity: %s", payload)
|
||||
}
|
||||
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
|
||||
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
|
||||
t.Fatalf("incorrect direct Flash metadata: %s", payload)
|
||||
}
|
||||
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
|
||||
t.Fatalf("changed existing/gateway limits: %s", payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
|
||||
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
|
||||
}}
|
||||
conf := map[string]interface{}{}
|
||||
if err := applyOpenclawModelsConfig(conf, current); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
|
||||
}}
|
||||
preserveOpenclawModelMetadata(conf, next)
|
||||
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
|
||||
t.Fatalf("saved metadata changed: %+v", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
|
||||
models, err := buildInitialAgentAccountModels(account, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
|
||||
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
|
||||
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
|
||||
}
|
||||
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
|
||||
t.Fatalf("default request: %s, %v", request.Body, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxHermesConfiguration(t *testing.T) {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
|
||||
dir := t.TempDir()
|
||||
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
|
||||
if err := writeHermesConfig(dir, account, id, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
|
||||
t.Fatalf("changed Hermes selection: %+v", conf.Model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxExplicitAccountModels(t *testing.T) {
|
||||
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
|
||||
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
|
||||
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
|
||||
actual, err := buildInitialAgentAccountModels(account, requested)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, requested) {
|
||||
t.Fatalf("changed explicit %s models: %+v", provider, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
|
||||
|
||||
func isDeepSeekAccessHost(value string) bool {
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
return ip.To4() != nil && !strings.Contains(value, ":")
|
||||
}
|
||||
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
|
||||
return false
|
||||
}
|
||||
for _, label := range strings.Split(value, ".") {
|
||||
if !deepSeekHostLabelPattern.MatchString(label) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
|
||||
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||
}
|
||||
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
if req.AgentType == constant.AppDeepSeekHarness {
|
||||
if !isDeepSeekAccessHost(req.AccessHost) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
|
||||
}
|
||||
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||
}
|
||||
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
hasLetter, hasDigit := false, false
|
||||
for _, char := range req.DashboardPassword {
|
||||
hasLetter = hasLetter || unicode.IsLetter(char)
|
||||
hasDigit = hasDigit || unicode.IsDigit(char)
|
||||
}
|
||||
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
switch strings.ToLower(req.DashboardPassword) {
|
||||
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
|
||||
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -107,9 +107,6 @@ func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOpenclawBundledFeishuVersion(install.Version) && (strings.TrimSpace(req.Package) == "@larksuite/openclaw-lark" || strings.TrimSpace(req.Package) == "@openclaw/feishu") {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,9 +132,6 @@ func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isOpenclawBundledFeishuVersion(install.Version) && (req.PluginID == "openclaw-lark" || req.PluginID == "feishu") {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||
resolvedAPIType := strings.TrimSpace(apiType)
|
||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
@@ -113,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
||||
return resolvedAgentAccountInput{}, err
|
||||
}
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" && !allowEmptyModel {
|
||||
if modelID == "" {
|
||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||
@@ -148,10 +148,6 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
|
||||
if _, busy := feishuSwitches.LoadOrStore(configPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(configPath)
|
||||
ensureGatewaySecurityDefaults(conf)
|
||||
ensureOpenclawUpdateDefaults(conf)
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
@@ -419,15 +415,6 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
} else {
|
||||
item.WebUIPort = appInstall.HttpPort
|
||||
}
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
item.WebUIPort = appInstall.HttpsPort
|
||||
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||
if isDeepSeekAccessHost(accessHost) {
|
||||
item.AccessHost = accessHost
|
||||
}
|
||||
}
|
||||
}
|
||||
item.Path = appInstall.GetPath()
|
||||
item.Status = appInstall.Status
|
||||
item.Message = appInstall.Message
|
||||
@@ -455,8 +442,7 @@ func localizedAgentProviderName(provider string) string {
|
||||
}
|
||||
|
||||
func isAgentAppKey(appKey string) bool {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
}
|
||||
|
||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||
@@ -1267,12 +1253,7 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
||||
if err != nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, err
|
||||
}
|
||||
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
|
||||
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
|
||||
entry.ContextWindow = 1000000
|
||||
entry.MaxTokens = 524288
|
||||
}
|
||||
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
|
||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
||||
ApiKey: providerPatch.APIKey,
|
||||
BaseUrl: providerPatch.BaseURL,
|
||||
Api: providerPatch.APIType,
|
||||
@@ -1660,10 +1641,6 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||
case constant.AppCopaw:
|
||||
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||
case constant.AppDeepSeekHarness:
|
||||
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||
case constant.AppOctop:
|
||||
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
|
||||
default:
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
+47
-107
@@ -8,7 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -29,15 +28,13 @@ import (
|
||||
|
||||
type AIToolService struct{}
|
||||
|
||||
var ollamaModelMu sync.Mutex
|
||||
|
||||
type IAIToolService interface {
|
||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||
Create(req dto.OllamaModelName) error
|
||||
Close(name string) error
|
||||
Recreate(req dto.OllamaModelName) error
|
||||
Delete(req dto.ForceDelete) error
|
||||
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||
Sync() ([]dto.OllamaModelDropList, error)
|
||||
LoadDetail(name string) (string, error)
|
||||
BindDomain(req dto.OllamaBindDomain) error
|
||||
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
||||
@@ -49,7 +46,6 @@ func NewIAIToolService() IAIToolService {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
||||
syncErr := syncOllamaModels()
|
||||
var options []repo.DBOption
|
||||
if len(req.Info) != 0 {
|
||||
options = append(options, repo.WithByLikeName(req.Info))
|
||||
@@ -64,11 +60,6 @@ func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModel
|
||||
if err := copier.Copy(&item, &itemModel); err != nil {
|
||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
if syncErr != nil && item.Status != constant.StatusWaiting {
|
||||
item.Status = constant.StatusUnknown
|
||||
item.Message = syncErr.Error()
|
||||
item.Size = ""
|
||||
}
|
||||
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
||||
if len(taskModel.ID) != 0 {
|
||||
item.LogFileExist = true
|
||||
@@ -94,8 +85,6 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -136,9 +125,6 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
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
|
||||
}
|
||||
@@ -158,8 +144,6 @@ func (u *AIToolService) Close(name string) error {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -195,16 +179,11 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
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
|
||||
}
|
||||
|
||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||
if len(ollamaList) == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
@@ -223,28 +202,55 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
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 {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
||||
containerName, err := LoadContainerName()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if install.App.Key != "ollama" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
|
||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||
return err
|
||||
var list []model.OllamaModel
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
continue
|
||||
}
|
||||
if parts[0] == "NAME" {
|
||||
continue
|
||||
}
|
||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
||||
}
|
||||
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||
"model_account_base_url": baseURL,
|
||||
}, repo.WithByID(install.ID))
|
||||
listInDB, _ := aiRepo.List()
|
||||
var dropList []dto.OllamaModelDropList
|
||||
for _, itemModel := range listInDB {
|
||||
isExit := false
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i].Name == itemModel.Name {
|
||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
|
||||
list = append(list[:i], list[(i+1):]...)
|
||||
isExit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isExit && itemModel.Status != constant.StatusWaiting {
|
||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
|
||||
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, item := range list {
|
||||
item.Status = constant.StatusSuccess
|
||||
item.From = "remote"
|
||||
_ = aiRepo.Create(&item)
|
||||
}
|
||||
|
||||
return dropList, nil
|
||||
}
|
||||
|
||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||
@@ -364,72 +370,6 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncOllamaModels() error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
containerName, err := LoadContainerName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
models := make(map[string]string)
|
||||
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||
if len(lines) == 0 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "NAME") {
|
||||
return fmt.Errorf("invalid ollama list output: %s", stdout)
|
||||
}
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
return fmt.Errorf("invalid ollama model entry: %s", line)
|
||||
}
|
||||
models[parts[0]] = parts[2] + " " + parts[3]
|
||||
}
|
||||
list, err := aiRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known := make(map[string]bool, len(list))
|
||||
for _, item := range list {
|
||||
name := item.Name
|
||||
if !strings.Contains(path.Base(name), ":") {
|
||||
name += ":latest"
|
||||
}
|
||||
size, exists := models[name]
|
||||
known[name] = true
|
||||
if item.Status == constant.StatusWaiting {
|
||||
continue
|
||||
}
|
||||
status, message := constant.StatusSuccess, ""
|
||||
if !exists {
|
||||
if item.Status == constant.StatusFailed || item.Status == constant.StatusCanceled {
|
||||
continue
|
||||
}
|
||||
status, message = constant.StatusDeleted, "not exist"
|
||||
}
|
||||
if item.Status != status || item.Size != size || item.Message != message {
|
||||
if err := aiRepo.Update(item.ID, map[string]interface{}{"status": status, "size": size, "message": message}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, size := range models {
|
||||
if known[name] {
|
||||
continue
|
||||
}
|
||||
item := model.OllamaModel{Name: name, Size: size, From: "remote", Status: constant.StatusSuccess}
|
||||
if err := aiRepo.Create(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadContainerName() (string, error) {
|
||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||
if err != nil {
|
||||
@@ -447,12 +387,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
lines := strings.Split(stdout, "\n")
|
||||
if !strings.Contains(path.Base(name), ":") {
|
||||
name += ":latest"
|
||||
}
|
||||
for _, line := range lines {
|
||||
if !strings.Contains(line, name) {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 || parts[0] != name {
|
||||
if len(parts) < 5 {
|
||||
continue
|
||||
}
|
||||
return parts[2] + " " + parts[3], nil
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
|
||||
if kind != "vllm" && kind != "ollama" {
|
||||
return nil, errors.New("unsupported benchmark source")
|
||||
}
|
||||
selectedID := strings.SplitN(selected, ":", 2)[0]
|
||||
type instance struct {
|
||||
id uint
|
||||
name, container string
|
||||
port int
|
||||
}
|
||||
instances := []instance{}
|
||||
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, app := range apps {
|
||||
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, install := range installs {
|
||||
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
|
||||
continue
|
||||
}
|
||||
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
|
||||
}
|
||||
}
|
||||
results := []dto.AIBenchmarkSource{}
|
||||
if len(instances) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
|
||||
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
for _, instance := range instances {
|
||||
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
|
||||
inspected, err := client.ContainerInspect(ctx, instance.container)
|
||||
if err != nil {
|
||||
base.ID = strconv.FormatUint(uint64(instance.id), 10)
|
||||
base.Message = "container inspection unavailable"
|
||||
results = append(results, base)
|
||||
continue
|
||||
}
|
||||
if inspected.State != nil {
|
||||
base.Status = inspected.State.Status
|
||||
}
|
||||
args := []string{}
|
||||
if inspected.Config != nil {
|
||||
args = append(args, inspected.Config.Entrypoint...)
|
||||
args = append(args, inspected.Config.Cmd...)
|
||||
}
|
||||
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
|
||||
parser := shellwords.NewParser()
|
||||
parser.ParseEnv = false
|
||||
parser.ParseBacktick = false
|
||||
if parsed, err := parser.Parse(args[2]); err == nil {
|
||||
args = parsed
|
||||
}
|
||||
}
|
||||
flags := map[string]string{}
|
||||
for i, arg := range args {
|
||||
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
|
||||
flags[key] = value
|
||||
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
|
||||
flags[arg] = args[i+1]
|
||||
}
|
||||
}
|
||||
if name := flags["--served-model-name"]; name != "" {
|
||||
base.Models = []string{name}
|
||||
}
|
||||
tokenizer := flags["--tokenizer"]
|
||||
if tokenizer == "" {
|
||||
tokenizer = flags["--model"]
|
||||
for i, arg := range args {
|
||||
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||
tokenizer = args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if kind != "ollama" && tokenizer != "" {
|
||||
if !filepath.IsAbs(tokenizer) {
|
||||
base.Tokenizer = tokenizer
|
||||
} else {
|
||||
longest := 0
|
||||
for _, mount := range inspected.Mounts {
|
||||
relative, err := filepath.Rel(mount.Destination, tokenizer)
|
||||
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
|
||||
candidate := filepath.Join(mount.Source, relative)
|
||||
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||
base.Tokenizer = candidate
|
||||
longest = len(mount.Destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ports := map[int]string{}
|
||||
if instance.port > 0 {
|
||||
ports[instance.port] = "127.0.0.1"
|
||||
}
|
||||
if inspected.NetworkSettings != nil {
|
||||
for _, bindings := range inspected.NetworkSettings.Ports {
|
||||
for _, binding := range bindings {
|
||||
port, _ := strconv.Atoi(binding.HostPort)
|
||||
if port > 0 {
|
||||
host := binding.HostIP
|
||||
if host == "" || host == "0.0.0.0" || host == "::" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
ports[port] = host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
|
||||
port, _ := strconv.Atoi(flags["--port"])
|
||||
if port > 0 {
|
||||
ports[port] = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
ordered := []int{}
|
||||
for port := range ports {
|
||||
ordered = append(ordered, port)
|
||||
}
|
||||
sort.Ints(ordered)
|
||||
if len(ordered) == 0 {
|
||||
ordered = append(ordered, 0)
|
||||
}
|
||||
for _, port := range ordered {
|
||||
source := base
|
||||
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
|
||||
if selected != "" && source.ID != selected {
|
||||
continue
|
||||
}
|
||||
if port > 0 {
|
||||
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
|
||||
}
|
||||
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
|
||||
endpoint := "/v1/models"
|
||||
if kind == "ollama" {
|
||||
endpoint = "/api/tags"
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
|
||||
if err != nil {
|
||||
source.Message = "model list unavailable; enter the served model name"
|
||||
} else {
|
||||
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
|
||||
response.Body.Close()
|
||||
if response.StatusCode == http.StatusOK && readErr == nil {
|
||||
var data struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
Models []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &data); err == nil {
|
||||
source.Models = []string{}
|
||||
for _, model := range data.Data {
|
||||
source.Models = append(source.Models, model.ID)
|
||||
}
|
||||
for _, model := range data.Models {
|
||||
source.Models = append(source.Models, model.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
results = append(results, source)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
+14
-26
@@ -74,8 +74,8 @@ type IAlertService interface {
|
||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
|
||||
DeleteAlertConfig(id uint) (string, error)
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||
DeleteAlertConfig(id uint) error
|
||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
|
||||
}
|
||||
@@ -623,26 +623,25 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
|
||||
})
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
|
||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
name := alertConfigLogName(config.Config, config.Title)
|
||||
if req.Status == constant.AlertEnable {
|
||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||
return name, err
|
||||
return err
|
||||
}
|
||||
if config.Type == constant.Custom {
|
||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||
return name, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||
"status": req.Status,
|
||||
"update_user": operator,
|
||||
}, repo.WithByID(req.ID))
|
||||
@@ -814,20 +813,19 @@ func alertConfigSMSPhone(configData string) string {
|
||||
return strings.TrimSpace(cfg.Phone)
|
||||
}
|
||||
|
||||
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
|
||||
config, err := alertRepo.GetConfigById(id)
|
||||
func (a AlertService) DeleteAlertConfig(id uint) error {
|
||||
_, err := alertRepo.GetConfigById(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
name := alertConfigLogName(config.Config, config.Title)
|
||||
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
||||
if err != nil {
|
||||
return name, err
|
||||
return err
|
||||
}
|
||||
if len(usedAlerts) > 0 {
|
||||
return name, buserr.New("ErrAlertConfigInUse")
|
||||
return buserr.New("ErrAlertConfigInUse")
|
||||
}
|
||||
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
||||
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
||||
}
|
||||
|
||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
@@ -1045,13 +1043,3 @@ func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method s
|
||||
_, err = a.validateAlertMethodReferences(method)
|
||||
return err
|
||||
}
|
||||
|
||||
func alertConfigLogName(config, title string) string {
|
||||
var value struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
|
||||
return strings.TrimSpace(value.DisplayName)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
+16
-57
@@ -258,35 +258,26 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
fileOp := files.NewFileOp()
|
||||
|
||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err == nil && app.Resource == constant.AppResourceRemote {
|
||||
detail.Update = false
|
||||
appDetailDTO.Update = false
|
||||
if !fileOp.Stat(versionPath) || detail.Update {
|
||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
}
|
||||
switch app.Type {
|
||||
case constant.RuntimePHP:
|
||||
paramMap := make(map[string]interface{})
|
||||
if app.Resource == constant.AppResourceRemote {
|
||||
if err = json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.Params = paramMap
|
||||
} else {
|
||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||
if !fileOp.Stat(paramsPath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||
}
|
||||
param, err := fileOp.GetContent(paramsPath)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||
if !fileOp.Stat(paramsPath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||
}
|
||||
param, err := fileOp.GetContent(paramsPath)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
paramMap := make(map[string]interface{})
|
||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||
if !fileOp.Stat(composePath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||
@@ -317,12 +308,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if appDetailDTO.DockerCompose == "" {
|
||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
||||
if app.Resource == constant.AppResourceRemote {
|
||||
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
}
|
||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||
@@ -449,12 +434,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
} else {
|
||||
if appDetail.DockerCompose == "" {
|
||||
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
||||
if app.Resource == constant.AppResourceRemote {
|
||||
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var statusCode int
|
||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if statusCode == http.StatusNotFound {
|
||||
@@ -499,22 +478,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
req.Params[constant.ContainerName] = containerName
|
||||
appInstall.ContainerName = containerName
|
||||
|
||||
modelAccountBaseURL := ""
|
||||
if app.Key == "ollama" {
|
||||
appInstall.SyncModelAccount = req.SyncModelAccount == nil || *req.SyncModelAccount
|
||||
appInstall.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||
if appInstall.ModelAccountBaseURLType == "" {
|
||||
appInstall.ModelAccountBaseURLType = "container"
|
||||
}
|
||||
appInstall.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||
}
|
||||
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
index := 0
|
||||
serviceName := ""
|
||||
for k := range servicesMap {
|
||||
@@ -625,10 +588,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
|
||||
}
|
||||
|
||||
|
||||
@@ -159,14 +159,6 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
||||
}
|
||||
|
||||
res.ContainerName = appInstall.ContainerName
|
||||
res.ModelAccountBaseURLType = appInstall.ModelAccountBaseURLType
|
||||
res.ModelAccountBaseURL = appInstall.ModelAccountBaseURL
|
||||
if app.Key == "ollama" {
|
||||
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(appInstall)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res.Name = appInstall.Name
|
||||
res.Version = appInstall.Version
|
||||
res.CreatedAt = appInstall.CreatedAt
|
||||
@@ -305,14 +297,13 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return syncAppInstallStatus(&install, true)
|
||||
case constant.Upgrade:
|
||||
upgradeReq := request.AppInstallUpgrade{
|
||||
FeishuTransition: req.FeishuTransition,
|
||||
InstallID: install.ID,
|
||||
DetailID: req.DetailId,
|
||||
Backup: req.Backup,
|
||||
PullImage: req.PullImage,
|
||||
DeleteImage: req.DeleteImage,
|
||||
DockerCompose: req.DockerCompose,
|
||||
TaskID: req.TaskID,
|
||||
InstallID: install.ID,
|
||||
DetailID: req.DetailId,
|
||||
Backup: req.Backup,
|
||||
PullImage: req.PullImage,
|
||||
DeleteImage: req.DeleteImage,
|
||||
DockerCompose: req.DockerCompose,
|
||||
TaskID: req.TaskID,
|
||||
}
|
||||
return upgradeInstall(upgradeReq)
|
||||
case constant.Reload:
|
||||
@@ -447,32 +438,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
}
|
||||
}
|
||||
|
||||
modelAccountBaseURL := ""
|
||||
if installed.App.Key == "ollama" {
|
||||
if req.SyncModelAccount != nil {
|
||||
installed.SyncModelAccount = *req.SyncModelAccount
|
||||
}
|
||||
if req.ModelAccountBaseURLType != "" {
|
||||
installed.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||
installed.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||
} else if installed.SyncModelAccount {
|
||||
installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(oldInstalled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if installed.SyncModelAccount {
|
||||
containerName := installed.ContainerName
|
||||
if req.Advanced && req.ContainerName != "" {
|
||||
containerName = req.ContainerName
|
||||
}
|
||||
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, containerName, "11434", installed.HttpPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backupDockerCompose := installed.DockerCompose
|
||||
if req.Advanced {
|
||||
composeMap := make(map[string]interface{})
|
||||
@@ -598,9 +563,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
|
||||
}
|
||||
|
||||
@@ -717,7 +679,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
var feishuLegacy, feishuConfigured, feishuChecked bool
|
||||
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
|
||||
if err != nil {
|
||||
return versions, err
|
||||
@@ -742,28 +703,10 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
||||
}
|
||||
if canUpgrade {
|
||||
feishuTarget := app.Key == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(detail.Version)
|
||||
if feishuTarget && !feishuChecked {
|
||||
conf, readErr := readOpenclawConfig(path.Join(install.GetPath(), "data/conf/openclaw.json"))
|
||||
if readErr != nil {
|
||||
return versions, readErr
|
||||
}
|
||||
feishuLegacy, feishuConfigured, err = classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
feishuChecked = true
|
||||
}
|
||||
var newCompose string
|
||||
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
||||
filename := filepath.Base(detail.DownloadUrl)
|
||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
||||
if app.Resource == constant.AppResourceRemote {
|
||||
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
}
|
||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||
@@ -788,11 +731,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
}
|
||||
}
|
||||
versions = append(versions, dto.AppVersion{
|
||||
FeishuLegacy: feishuTarget && feishuLegacy,
|
||||
FeishuConfigured: feishuTarget && feishuConfigured,
|
||||
Version: detail.Version,
|
||||
DetailId: detail.ID,
|
||||
DockerCompose: newCompose,
|
||||
Version: detail.Version,
|
||||
DetailId: detail.ID,
|
||||
DockerCompose: newCompose,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -987,18 +928,6 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
config := getAppCommonConfig(envs)
|
||||
config.DockerCompose = install.DockerCompose
|
||||
res.Params = params
|
||||
res.AppKey = install.App.Key
|
||||
res.Name = install.Name
|
||||
res.HttpPort = install.HttpPort
|
||||
res.SyncModelAccount = install.SyncModelAccount
|
||||
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
|
||||
res.ModelAccountBaseURL = install.ModelAccountBaseURL
|
||||
if install.App.Key == "ollama" {
|
||||
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if config.ContainerName == "" {
|
||||
config.ContainerName = install.ContainerName
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func prepareMCPWebsiteDirectory(req request.AppInstallCreate) error {
|
||||
changed := errors.New("MCP website directory changed; inspect the installation before retrying")
|
||||
directory, ok := req.Params["WEBSITE_DIR"].(string)
|
||||
if !ok || directory == "" || global.DB == nil {
|
||||
return changed
|
||||
}
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", "WEBSITE_DIR").First(&setting).Error
|
||||
missing := errors.Is(err, gorm.ErrRecordNotFound)
|
||||
if err != nil && !missing {
|
||||
return err
|
||||
}
|
||||
current := setting.Value
|
||||
if current == "" {
|
||||
current = path.Join(global.Dir.BaseDir, "1panel", "www")
|
||||
}
|
||||
if directory != current {
|
||||
return changed
|
||||
}
|
||||
if missing {
|
||||
if err := tx.Create(&model.Setting{Key: "WEBSITE_DIR", Value: directory}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if setting.Value == "" {
|
||||
result := tx.Model(&model.Setting{}).Where("id = ? AND value = ?", setting.ID, "").Update("value", directory)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return changed
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(directory, "conf.d"), constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -191,11 +191,7 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
|
||||
|
||||
if _, ok := InitTypes[app.Type]; ok {
|
||||
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
|
||||
dockerComposeUrl, err := appDownloadURL(dockerComposeUrl, strconv.Itoa(c.list.LastModified))
|
||||
var composeRes []byte
|
||||
if err == nil {
|
||||
_, composeRes, err = req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
}
|
||||
_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if err == nil {
|
||||
detail.DockerCompose = string(composeRes)
|
||||
} else {
|
||||
|
||||
@@ -60,16 +60,10 @@ type upgradeFileSnapshot struct {
|
||||
}
|
||||
|
||||
type appUpgradeContext struct {
|
||||
openclawOriginal []byte
|
||||
feishuOriginal []byte
|
||||
feishuImage string
|
||||
feishuOldImage string
|
||||
feishuEnabled bool
|
||||
feishuLockPath string
|
||||
req request.AppInstallUpgrade
|
||||
original model.AppInstall
|
||||
candidate model.AppInstall
|
||||
detail model.AppDetail
|
||||
req request.AppInstallUpgrade
|
||||
original model.AppInstall
|
||||
candidate model.AppInstall
|
||||
detail model.AppDetail
|
||||
|
||||
phase appUpgradePhase
|
||||
stopAttempted bool
|
||||
@@ -108,19 +102,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
}()
|
||||
|
||||
feishuLockPath := ""
|
||||
if install.App.Key == constant.AppOpenclaw {
|
||||
feishuLockPath = path.Join(install.GetPath(), "data/conf/openclaw.json")
|
||||
if _, busy := feishuSwitches.LoadOrStore(feishuLockPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer func() {
|
||||
if releaseLock {
|
||||
feishuSwitches.Delete(feishuLockPath)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -148,7 +129,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
return err
|
||||
}
|
||||
ctx := &appUpgradeContext{
|
||||
feishuLockPath: feishuLockPath,
|
||||
req: req,
|
||||
original: install,
|
||||
candidate: install,
|
||||
@@ -242,10 +222,6 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
u.openclawOriginal, err = os.ReadFile(path.Join(u.stageDir, "data/conf/openclaw.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if u.original.App.Key == constant.AppOpenresty {
|
||||
for _, relativePath := range []string{
|
||||
@@ -354,9 +330,6 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||
if u.original.App.Resource == constant.AppResourceRemote {
|
||||
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
|
||||
}
|
||||
if err = u.prepareFeishuUpgrade(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.phase = appUpgradePrepared
|
||||
return nil
|
||||
}
|
||||
@@ -414,9 +387,6 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
}
|
||||
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
|
||||
u.phase = appUpgradeStopped
|
||||
if err := u.checkFeishuUpgradeSource(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if u.original.App.Key == constant.AppOpenresty {
|
||||
@@ -484,9 +454,6 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
} else {
|
||||
t.Log("WARNING: no containers found for the upgraded application")
|
||||
}
|
||||
if err = u.verifyFeishuUpgrade(t); err != nil {
|
||||
return err
|
||||
}
|
||||
u.candidate.Status = constant.StatusRunning
|
||||
u.candidate.Message = ""
|
||||
|
||||
@@ -612,34 +579,8 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
if out != "" {
|
||||
err = fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
if u.feishuOriginal != nil {
|
||||
return err
|
||||
}
|
||||
rollbackErr = err
|
||||
}
|
||||
if u.feishuOriginal != nil {
|
||||
if u.backupFile == "" {
|
||||
return buserr.New("ErrFeishuUpgradeBackup")
|
||||
}
|
||||
if err := u.restoreManagedFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", "", false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pinFeishuRollbackImage(u.original.GetComposePath(), u.feishuOldImage); err != nil {
|
||||
return err
|
||||
}
|
||||
restoredCompose, err := os.ReadFile(u.original.GetComposePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.original.DockerCompose = string(restoredCompose)
|
||||
if _, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.finishRollback()
|
||||
}
|
||||
if u.backupFile != "" {
|
||||
_ = u.restoreManagedFiles()
|
||||
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
|
||||
@@ -661,11 +602,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) finishRollback() error {
|
||||
if u.feishuOriginal != nil {
|
||||
if err := waitOpenclawReady(u.original.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
restored := u.original
|
||||
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||
return err
|
||||
@@ -707,12 +643,6 @@ func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) cleanup() {
|
||||
if u.feishuLockPath != "" {
|
||||
feishuSwitches.Delete(u.feishuLockPath)
|
||||
}
|
||||
if u.feishuOriginal != nil && u.rollbackErr != nil {
|
||||
return
|
||||
}
|
||||
if u.snapshot != nil {
|
||||
u.snapshot.Cleanup()
|
||||
}
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func classifyLegacyFeishu(conf map[string]interface{}, installPath string) (legacy, configured bool, err error) {
|
||||
for _, field := range []string{"channels", "channels.feishu", "plugins", "plugins.entries", "plugins.entries.openclaw-lark", "plugins.entries.feishu"} {
|
||||
parts := strings.Split(field, ".")
|
||||
parent := conf
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
parent = childMap(parent, part)
|
||||
}
|
||||
if raw, exists := parent[parts[len(parts)-1]]; exists {
|
||||
if _, ok := raw.(map[string]interface{}); !ok {
|
||||
return false, false, feishuConfigError(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
plugins := childMap(conf, "plugins")
|
||||
for _, key := range []string{"allow", "deny"} {
|
||||
if raw, exists := plugins[key]; exists {
|
||||
if _, ok := raw.([]string); ok {
|
||||
continue
|
||||
}
|
||||
ids, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return false, false, feishuConfigError("plugins." + key)
|
||||
}
|
||||
for _, id := range ids {
|
||||
if _, ok := id.(string); !ok {
|
||||
return false, false, feishuConfigError("plugins." + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"openclaw-lark", "feishu"} {
|
||||
if raw, exists := childMap(childMap(plugins, "entries"), id)["enabled"]; exists {
|
||||
if _, ok := raw.(bool); !ok {
|
||||
return false, false, feishuConfigError("plugins.entries." + id + ".enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
if usesBundledFeishu(conf) {
|
||||
return false, false, nil
|
||||
}
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
configured = len(f) > 0
|
||||
if raw, exists := conf["bindings"]; exists {
|
||||
bindings, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return false, false, feishuConfigError("bindings")
|
||||
}
|
||||
for _, raw := range bindings {
|
||||
binding, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return false, false, feishuConfigError("bindings")
|
||||
}
|
||||
if match, exists := binding["match"]; exists {
|
||||
if _, ok := match.(map[string]interface{}); !ok {
|
||||
return false, false, feishuConfigError("bindings.match")
|
||||
}
|
||||
}
|
||||
if extractStringValue(childMap(binding, "match")["channel"]) == "feishu" {
|
||||
configured = true
|
||||
}
|
||||
}
|
||||
}
|
||||
entries := childMap(childMap(conf, "plugins"), "entries")
|
||||
entry, selected := entries["openclaw-lark"]
|
||||
if selected {
|
||||
old, ok := entry.(map[string]interface{})
|
||||
if !ok {
|
||||
return false, configured, feishuConfigError("plugins.entries.openclaw-lark")
|
||||
}
|
||||
// A completed disable-only repair must not repeat on every upgrade.
|
||||
if enabled, ok := old["enabled"].(bool); ok && !enabled && !configured {
|
||||
return false, false, nil
|
||||
}
|
||||
}
|
||||
installed, err := checkPluginInstalled(installPath, "feishu")
|
||||
if err != nil {
|
||||
return false, configured, err
|
||||
}
|
||||
_, oldThread := f["threadSession"]
|
||||
_, oldReply := f["replyMode"]
|
||||
_, oldStreaming := f["streaming"].(bool)
|
||||
return selected || installed || oldThread || oldReply || oldStreaming, configured, nil
|
||||
}
|
||||
|
||||
func prepareLegacyFeishu(conf map[string]interface{}, configured bool, transition *dto.AgentFeishuTransition) error {
|
||||
if !configured {
|
||||
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
|
||||
return nil
|
||||
}
|
||||
if transition == nil || !transition.Confirmed {
|
||||
return buserr.New("ErrFeishuUpgradeConfirm")
|
||||
}
|
||||
if !validFeishuTransition(transition.GroupSessionScope, transition.RequireMention) {
|
||||
return feishuConfigError("feishuTransition")
|
||||
}
|
||||
return convertLegacyFeishu(conf, dto.AgentFeishuSwitchReq{Streaming: transition.Streaming, GroupSessionScope: transition.GroupSessionScope, RequireMention: transition.RequireMention})
|
||||
}
|
||||
|
||||
func validFeishuTransition(scope, mention string) bool {
|
||||
return (scope == "group" || scope == "group_sender" || scope == "group_topic" || scope == "group_topic_sender") && (mention == "true" || mention == "false")
|
||||
}
|
||||
|
||||
func feishuRecoveryPayload(candidate []byte, configured bool) ([]byte, error) {
|
||||
var conf map[string]interface{}
|
||||
if err := json.Unmarshal(candidate, &conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configured {
|
||||
ensureChildMap(ensureChildMap(conf, "channels"), "feishu")["enabled"] = false
|
||||
selectBundledFeishu(conf, false)
|
||||
}
|
||||
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
|
||||
return json.MarshalIndent(conf, "", " ")
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) prepareFeishuUpgrade() error {
|
||||
if u.original.App.Key != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(u.candidate.Version) {
|
||||
return nil
|
||||
}
|
||||
source := filepath.Join(u.original.GetPath(), "data/conf/openclaw.json")
|
||||
staged := filepath.Join(u.stageDir, "data/conf/openclaw.json")
|
||||
// The staged copy predates protocol migration and is the exact cutover comparison source.
|
||||
original := u.openclawOriginal
|
||||
current, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(original, current) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
_, conf, err := readFeishuConfigBytes(staged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, u.original.GetPath())
|
||||
if err != nil || !legacy {
|
||||
return err
|
||||
}
|
||||
if !u.req.Backup {
|
||||
return buserr.New("ErrFeishuUpgradeBackup")
|
||||
}
|
||||
if err = prepareLegacyFeishu(conf, configured, u.req.FeishuTransition); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := docker.GetComposeProject(u.original.Name, u.original.GetPath(), []byte(u.candidate.DockerCompose), u.envContent, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(project.Services) != 1 {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
for _, service := range project.Services {
|
||||
if service.Build != nil {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
u.feishuImage, err = bundledFeishuImage(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configMounted := false
|
||||
for _, v := range service.Volumes {
|
||||
if v.Target == "/etc/localtime" {
|
||||
continue
|
||||
}
|
||||
if v.Type != "bind" {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
if err = feishuBackupPath(u.original.GetPath(), v.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
if v.Target == "/home/node/.openclaw" {
|
||||
configMounted = filepath.Clean(v.Source) == filepath.Dir(source)
|
||||
}
|
||||
}
|
||||
if !configMounted {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
info, err := cli.ContainerInspect(ctx, u.original.ContainerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.feishuOldImage = info.Image
|
||||
for _, m := range info.Mounts {
|
||||
if m.Destination == "/etc/localtime" {
|
||||
continue
|
||||
}
|
||||
if string(m.Type) != "bind" {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
if err = feishuBackupPath(u.original.GetPath(), m.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Tar preserves symlinks: an external target would not be in the application backup.
|
||||
if err = filepath.WalkDir(filepath.Join(u.original.GetPath(), "data"), func(file string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.Type()&os.ModeSymlink != 0 {
|
||||
return feishuBackupPath(u.original.GetPath(), file)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
u.feishuOriginal = original
|
||||
u.feishuEnabled = configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before, err := os.ReadFile(staged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = replaceFeishuConfig(staged, before, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
targetCompose := filepath.Join(u.stageDir, "docker-compose.yml")
|
||||
if err = pinFeishuRollbackImage(targetCompose, u.feishuImage); err != nil {
|
||||
return err
|
||||
}
|
||||
pinned, err := os.ReadFile(targetCompose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.candidate.DockerCompose = string(pinned)
|
||||
return nil
|
||||
}
|
||||
|
||||
func feishuBackupPath(root, source string) error {
|
||||
root, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(source)
|
||||
if err != nil {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
rel, err := filepath.Rel(root, resolved)
|
||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) checkFeishuUpgradeSource() error {
|
||||
if u.feishuOriginal == nil {
|
||||
return nil
|
||||
}
|
||||
current, err := os.ReadFile(filepath.Join(u.original.GetPath(), "data/conf/openclaw.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, u.feishuOriginal) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) verifyFeishuUpgrade(t *task.Task) error {
|
||||
if u.feishuOriginal == nil {
|
||||
return nil
|
||||
}
|
||||
if err := verifyFeishuActivation(u.candidate.ContainerName, u.feishuEnabled); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.feishuEnabled {
|
||||
probeFeishuConnection(u.candidate.ContainerName, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func probeFeishuConnection(name string, t *task.Task) {
|
||||
output, err := cmd.RunDockerExecWithStdout(30*time.Second, name, "openclaw", "channels", "status", "--channel", "feishu", "--probe", "--json")
|
||||
if err != nil || !feishuProbeOK(output) {
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuProbeWarning"))
|
||||
}
|
||||
}
|
||||
|
||||
// Pin the restored service to the actual old image, even if its registry tag moved.
|
||||
func pinFeishuRollbackImage(composeFile string, image string) error {
|
||||
raw, err := os.ReadFile(composeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var conf map[string]interface{}
|
||||
if err = yaml.Unmarshal(raw, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
services := childMap(conf, "services")
|
||||
if len(services) != 1 {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
for _, raw := range services {
|
||||
service, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return buserr.New("ErrFeishuUpgradeMounts")
|
||||
}
|
||||
service["image"] = image
|
||||
}
|
||||
payload, err := yaml.Marshal(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeUpgradeFile(composeFile, payload, constant.FilePerm)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -391,27 +390,12 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if install.App.Key == "ollama" {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
}
|
||||
tx, ctx := helper.GetTxAndContext()
|
||||
defer tx.Rollback()
|
||||
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
||||
return err
|
||||
}
|
||||
appKey := install.App.Key
|
||||
if appKey == "ollama" {
|
||||
remaining, listErr := appInstallRepo.ListBy(ctx, appInstallRepo.WithAppId(install.AppId))
|
||||
if listErr != nil {
|
||||
return listErr
|
||||
}
|
||||
if len(remaining) == 0 {
|
||||
if err = aiRepo.DeleteAll(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if isAgentAppKey(appKey) {
|
||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||
}
|
||||
@@ -484,12 +468,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if err == nil && len(entries) == 0 {
|
||||
_ = op.DeleteDir(parentDir)
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||
CleanupLocalModelAccount(install.ID)
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
||||
if len(existApps) == 0 {
|
||||
@@ -912,52 +891,16 @@ func handleMap(params map[string]interface{}, envParams map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
func appDownloadURL(rawURL, version string) (string, error) {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
query := parsedURL.Query()
|
||||
query.Set("v", version)
|
||||
parsedURL.RawQuery = query.Encode()
|
||||
return parsedURL.String(), nil
|
||||
}
|
||||
|
||||
func currentAppDownloadURL(rawURL string) (string, error) {
|
||||
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return appDownloadURL(rawURL, storeVersion)
|
||||
}
|
||||
|
||||
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
|
||||
if app.IsLocalApp() || app.IsCustomApp() {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && appInstall != nil {
|
||||
appInstall.Status = constant.StatusDownloadErr
|
||||
appInstall.Message = err.Error()
|
||||
}
|
||||
}()
|
||||
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
|
||||
appDownloadDir := app.GetAppResourcePath()
|
||||
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
||||
fileOp := files.NewFileOp()
|
||||
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheVersionPath := path.Join(appVersionDir, ".1panel-appstore-version")
|
||||
if !appDetail.Update && fileOp.Stat(appVersionDir) {
|
||||
if cachedVersion, readErr := os.ReadFile(cacheVersionPath); readErr == nil && string(cachedVersion) == storeVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
downloadURL, err := appDownloadURL(appDetail.DownloadUrl, storeVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
if !fileOp.Stat(appDownloadDir) {
|
||||
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
|
||||
@@ -966,14 +909,23 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
||||
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
|
||||
}
|
||||
if logger == nil {
|
||||
global.LOG.Infof("download app [%s] from %s", app.Name, downloadURL)
|
||||
global.LOG.Infof("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
||||
} else {
|
||||
logger.Printf("download app [%s] from %s", app.Name, downloadURL)
|
||||
logger.Printf("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
||||
}
|
||||
|
||||
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
|
||||
|
||||
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if appInstall != nil {
|
||||
appInstall.Status = constant.StatusDownloadErr
|
||||
appInstall.Message = err.Error()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
|
||||
if logger == nil {
|
||||
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
||||
} else {
|
||||
@@ -990,9 +942,6 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
||||
return
|
||||
}
|
||||
_ = fileOp.DeleteFile(filePath)
|
||||
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
appDetail.Update = false
|
||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
||||
return
|
||||
@@ -2035,9 +1984,6 @@ func getAppTags(appID uint, lang string) ([]response.TagDTO, error) {
|
||||
|
||||
func handleSiteDir(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task) error {
|
||||
if app.Key == "openresty" && (app.Resource == "remote" || app.Resource == "custom") && common.CompareVersion(appDetail.Version, "1.27") {
|
||||
if req.MCPPreserveWebsiteDir {
|
||||
return prepareMCPWebsiteDirectory(req)
|
||||
}
|
||||
if dir, ok := req.Params["WEBSITE_DIR"]; ok {
|
||||
siteDir := dir.(string)
|
||||
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
||||
|
||||
@@ -197,8 +197,7 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string, startAfterRecover ...bool) error {
|
||||
startAfter := len(startAfterRecover) == 0 || startAfterRecover[0]
|
||||
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
||||
// Validate AI Gateway backups before stopping the app, creating rollback
|
||||
// backups, or touching its files/database. Keep the validated extraction for
|
||||
// the restore so uploaded/encrypted archives are not unpacked twice.
|
||||
@@ -247,15 +246,12 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
defer os.RemoveAll(tmpPath)
|
||||
if !startAfter {
|
||||
return
|
||||
}
|
||||
if isRollback {
|
||||
_, _ = compose.UpWithoutBuild(install.GetComposePath())
|
||||
} else {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
}
|
||||
_ = os.RemoveAll(tmpPath)
|
||||
}()
|
||||
|
||||
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
||||
@@ -356,12 +352,8 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
|
||||
appDir := install.GetPath()
|
||||
backPath := fmt.Sprintf("%s_bak", appDir)
|
||||
if err := fileOp.Rename(appDir, backPath); err != nil && !startAfter {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CreateDir(appDir, constant.DirPerm); err != nil && !startAfter {
|
||||
return err
|
||||
}
|
||||
_ = fileOp.Rename(appDir, backPath)
|
||||
_ = fileOp.CreateDir(appDir, constant.DirPerm)
|
||||
|
||||
deCompressName := i18n.GetWithName("DeCompressFile", "app.tar.gz")
|
||||
t.LogStart(deCompressName)
|
||||
@@ -384,10 +376,6 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
_, _ = file.WriteString(mergedEnvContent)
|
||||
}
|
||||
|
||||
// The upgrade transaction verifies readiness before saving its restored install.
|
||||
if !startAfter {
|
||||
return nil
|
||||
}
|
||||
backupInstall.ID = install.ID
|
||||
backupInstall.Status = constant.StatusRunning
|
||||
backupInstall.AppId = install.AppId
|
||||
|
||||
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||
|
||||
type IContainerService interface {
|
||||
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []dto.ContainerOptions
|
||||
ListByImage(imageName string) []dto.ContainerOptions
|
||||
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
|
||||
LoadStatus() (dto.ContainerStatus, error)
|
||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
||||
ContainerUpdate(req dto.ContainerOperate) error
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
|
||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
@@ -112,7 +112,7 @@ func NewIContainerService() IContainerService {
|
||||
return &ContainerService{}
|
||||
}
|
||||
|
||||
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
|
||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
@@ -123,7 +123,7 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", req.Filters)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -140,29 +140,6 @@ 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 {
|
||||
@@ -229,30 +206,27 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
||||
return options
|
||||
}
|
||||
|
||||
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
|
||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
var data dto.ContainerStatus
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
if !containersOnly {
|
||||
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c := context.Background()
|
||||
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
||||
data.ContainerCount = len(containers)
|
||||
for _, item := range containers {
|
||||
switch item.State {
|
||||
@@ -318,48 +292,25 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
|
||||
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
options := container.ListOptions{All: true}
|
||||
if ids != nil {
|
||||
if len(ids) == 0 {
|
||||
return []dto.ContainerListStats{}, nil
|
||||
}
|
||||
options.Filters = filters.NewArgs()
|
||||
for _, id := range ids {
|
||||
options.Filters.Add("id", id)
|
||||
}
|
||||
}
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
datas := make([]dto.ContainerListStats, len(list))
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
datas[index].ContainerID = list[index].ID
|
||||
if ctx.Err() == nil && list[index].State == "running" {
|
||||
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
|
||||
}
|
||||
if len(list[index].Names) > 0 {
|
||||
datas[index].ContainerName = strings.TrimPrefix(list[index].Names[0], "/")
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Add(len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
go func(index int, item container.Summary) {
|
||||
datas[index] = loadCpuAndMem(client, item.ID)
|
||||
wg.Done()
|
||||
}(i, list[i])
|
||||
}
|
||||
for index := range list {
|
||||
jobs <- index
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return datas, nil
|
||||
}
|
||||
@@ -1771,13 +1722,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
||||
return selected
|
||||
}
|
||||
|
||||
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
data := dto.ContainerListStats{
|
||||
ContainerID: containerItem,
|
||||
}
|
||||
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"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/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/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 {
|
||||
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
|
||||
repositories map[string]*imageCheckUpdateRepository
|
||||
targets map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult
|
||||
}
|
||||
|
||||
func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error {
|
||||
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{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.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
|
||||
record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID
|
||||
return record, nil
|
||||
}
|
||||
return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture)
|
||||
}
|
||||
|
||||
// Revalidate immediately before each small write batch. Never hold a database
|
||||
// transaction open while waiting for Docker or Docker Hub.
|
||||
func saveContainerImageCheckUpdates(ctx context.Context, cli imageCheckUpdateDockerClient, pending []model.ContainerImageCheckUpdate, names map[string]string, result *ImageCheckUpdateStatus, log func(string, map[string]interface{})) error {
|
||||
records := make([]model.ContainerImageCheckUpdate, 0, len(pending))
|
||||
for _, record := range pending {
|
||||
current, err := cli.ContainerInspect(ctx, record.ContainerID)
|
||||
if err != nil && !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
if err != nil || current.Image != record.ImageID {
|
||||
result.Skipped++
|
||||
log("ContainerImageCheckChanged", map[string]interface{}{"name": names[record.ContainerID]})
|
||||
continue
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := global.DB.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&records, 100).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Count and announce only results that were successfully persisted.
|
||||
for _, record := range records {
|
||||
values := map[string]interface{}{"name": names[record.ContainerID], "image": record.ImageName}
|
||||
switch record.CheckStatus {
|
||||
case "failed":
|
||||
result.Failed++
|
||||
values["error"] = record.CheckError
|
||||
log("ContainerImageCheckFailed", values)
|
||||
case "skipped":
|
||||
result.Skipped++
|
||||
log("ContainerImageCheckSkipped", values)
|
||||
default:
|
||||
result.Checked++
|
||||
if record.HasUpdate {
|
||||
result.Updates++
|
||||
values["target"] = record.TargetImage
|
||||
log("ContainerImageCheckAvailable", values)
|
||||
} else {
|
||||
log("ContainerImageCheckCurrent", values)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameContainerImage guards cached results after external image changes.
|
||||
func sameContainerImage(stored, displayed string) bool {
|
||||
return strings.TrimPrefix(stored, "sha256:") == strings.TrimPrefix(displayed, "sha256:")
|
||||
}
|
||||
|
||||
// Reconcile only after Docker confirms the replacement container uses the target.
|
||||
func reconcileContainerImageCheckUpdate(oldID, newID, imageID, imageName string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var previous model.ContainerImageCheckUpdate
|
||||
if err := tx.Where("container_id = ?", oldID).Find(&previous).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if previous.ContainerID == "" {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Where("container_id = ?", oldID).Delete(&model.ContainerImageCheckUpdate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if previous.TargetImageID != imageID {
|
||||
return nil
|
||||
}
|
||||
previous.ContainerID = newID
|
||||
previous.ImageID = imageID
|
||||
previous.ImageName = imageName
|
||||
previous.HasUpdate = false
|
||||
previous.CheckStatus = "success"
|
||||
previous.CheckError = ""
|
||||
previous.CheckedAt = time.Now()
|
||||
return tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(&previous).Error
|
||||
})
|
||||
}
|
||||
|
||||
type imageCheckUpdateDockerClient interface {
|
||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||
ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error)
|
||||
Close() error
|
||||
}
|
||||
type imageCheckUpdateHubClient interface {
|
||||
Tags(context.Context) ([]string, error)
|
||||
Resolve(context.Context, string, dockerhub.Platform) (dockerhub.Target, bool, error)
|
||||
}
|
||||
|
||||
var newImageCheckUpdateDockerClient = func() (imageCheckUpdateDockerClient, error) { return docker.NewDockerClient() }
|
||||
var newImageCheckUpdateHubClient = func(ctx context.Context, repository, username, password string) (imageCheckUpdateHubClient, error) {
|
||||
return dockerhub.New(ctx, repository, username, password)
|
||||
}
|
||||
@@ -152,11 +152,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -43,10 +43,6 @@ type IDashboardService interface {
|
||||
|
||||
LoadQuickOptions() []dto.QuickJump
|
||||
ChangeQuick(req dto.ChangeQuicks) error
|
||||
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
|
||||
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
|
||||
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
|
||||
|
||||
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
||||
ChangeShow(req dto.SettingUpdate) error
|
||||
@@ -401,15 +397,16 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
||||
}
|
||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
showCount := 0
|
||||
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||
var quicks []model.QuickJump
|
||||
for _, item := range req.Quicks {
|
||||
var quick model.QuickJump
|
||||
if item.IsShow {
|
||||
showCount++
|
||||
}
|
||||
quicks = append(quicks, model.QuickJump{
|
||||
BaseModel: model.BaseModel{ID: item.ID},
|
||||
IsShow: item.IsShow,
|
||||
})
|
||||
if err := copier.Copy(&quick, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
quicks = append(quicks, quick)
|
||||
}
|
||||
if showCount == 0 {
|
||||
return buserr.New("ErrMinQuickJump")
|
||||
@@ -709,13 +706,6 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
switch quicks[i].Name {
|
||||
case "File":
|
||||
count, err := launcherRepo.CountQuickDirectories()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load quick directory count: %v", err)
|
||||
}
|
||||
quicks[i].Detail = fmt.Sprintf("%d", count)
|
||||
quicks[i].Alias = ""
|
||||
case "Agent":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||
case "Website":
|
||||
|
||||
@@ -5,14 +5,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mysql"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mysql/client"
|
||||
@@ -128,9 +126,6 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
}
|
||||
|
||||
func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
|
||||
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
|
||||
return false
|
||||
}
|
||||
if req.Timeout == 0 {
|
||||
req.Timeout = 30
|
||||
}
|
||||
@@ -185,9 +180,6 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
|
||||
}
|
||||
|
||||
func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
|
||||
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if req.Timeout == 0 {
|
||||
req.Timeout = 30
|
||||
}
|
||||
@@ -311,9 +303,6 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
|
||||
}
|
||||
|
||||
func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
|
||||
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
switch req.Type {
|
||||
case constant.AppPostgresql:
|
||||
if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{
|
||||
|
||||
@@ -64,7 +64,7 @@ func (u *MongodbService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
}
|
||||
|
||||
func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
@@ -146,7 +146,7 @@ func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) error {
|
||||
}
|
||||
|
||||
func (u *MongodbService) BindUser(req dto.MongodbBind) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ func (u *MongodbService) BindUser(req dto.MongodbBind) error {
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangeRootPassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if req.From != constant.AppResourceLocal {
|
||||
|
||||
@@ -433,11 +433,7 @@ func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
|
||||
}
|
||||
|
||||
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||
return u.create(ctx, req, false)
|
||||
}
|
||||
|
||||
func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBounded bool) (*model.DatabaseMysql, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if len(req.Username) != 0 && len(req.Password) == 0 {
|
||||
@@ -474,13 +470,13 @@ func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBou
|
||||
return nil, errors.New("cannot set root as user name")
|
||||
}
|
||||
|
||||
cli, version, err := loadMysqlClientByFrom(req.Database, mcpBounded)
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createItem.MysqlName = req.Database
|
||||
defer cli.Close()
|
||||
createInfo := client.CreateInfo{
|
||||
if err := cli.Create(client.CreateInfo{
|
||||
Name: req.Name,
|
||||
Format: req.Format,
|
||||
Collation: req.Collation,
|
||||
@@ -489,31 +485,9 @@ func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBou
|
||||
Permission: req.Permission,
|
||||
Version: version,
|
||||
Timeout: 300,
|
||||
}
|
||||
if mcpBounded {
|
||||
local, ok := cli.(*client.Local)
|
||||
if !ok {
|
||||
return nil, errors.New("an installed local MySQL instance is required")
|
||||
}
|
||||
deadline := time.Now().Add(300 * time.Second)
|
||||
if writeDeadline, ok := ctx.Deadline(); ok && writeDeadline.Add(-10*time.Second).Before(deadline) {
|
||||
deadline = writeDeadline.Add(-10 * time.Second)
|
||||
}
|
||||
sqlContext, cancel := context.WithDeadline(ctx, deadline)
|
||||
err = local.CreateWithContext(sqlContext, createInfo)
|
||||
cancel()
|
||||
} else {
|
||||
err = cli.Create(createInfo)
|
||||
}
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mcpBounded {
|
||||
if err := persistMCPMysqlMetadata(ctx, req, permissionHosts, &createItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &createItem, nil
|
||||
}
|
||||
if len(req.Username) != 0 {
|
||||
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
||||
return nil, err
|
||||
@@ -532,45 +506,6 @@ func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBou
|
||||
return &createItem, nil
|
||||
}
|
||||
|
||||
func persistMCPMysqlMetadata(ctx context.Context, req dto.MysqlDBCreate, hosts []string, item *model.DatabaseMysql) error {
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if req.Username != "" {
|
||||
key := global.CONF.Base.EncryptKey
|
||||
if key == "" {
|
||||
var setting model.Setting
|
||||
if err := tx.Where("key = ?", "EncryptKey").First(&setting).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
key = setting.Value
|
||||
}
|
||||
password, err := encrypt.StringEncryptWithKey(req.Password, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, host := range hosts {
|
||||
var user model.DatabaseUser
|
||||
err := tx.Where("type = ? AND database = ? AND username = ? AND host = ?", "mysql", req.Database, req.Username, host).First(&user).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
user.Type, user.Database, user.Username, user.Host = "mysql", req.Database, req.Username, host
|
||||
user.Password, user.IsDelete = password, false
|
||||
if req.Description != "" {
|
||||
user.Description = req.Description
|
||||
}
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
grant := model.DatabaseUserGrant{Type: "mysql", Database: req.Database, DBName: req.Name, Username: req.Username, Host: host}
|
||||
if err := tx.Where("type = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", grant.Type, grant.Database, grant.DBName, grant.Username, grant.Host).FirstOrCreate(&grant).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Create(item).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
@@ -680,7 +615,7 @@ func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[st
|
||||
}
|
||||
|
||||
func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Password, req.Host) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Username, req.Password, req.Host) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
dbs := make([]string, 0, len(req.DBs))
|
||||
@@ -883,7 +818,7 @@ func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
|
||||
}
|
||||
|
||||
func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Host, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if err := checkMysqlNormalUser(req.Username); err != nil {
|
||||
@@ -936,7 +871,7 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
|
||||
}
|
||||
|
||||
func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Host, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if err := checkMysqlNormalUser(req.Username); err != nil {
|
||||
@@ -1302,7 +1237,7 @@ func isMysqlDatabaseResourceInUseError(err error) bool {
|
||||
}
|
||||
|
||||
func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
@@ -1463,14 +1398,7 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loadMysqlStatus(req, app, executeSqlForMaps, executeSqlForRows)
|
||||
}
|
||||
|
||||
func loadMysqlStatus(req dto.OperationWithNameAndType, app *repo.RootInfo,
|
||||
maps func(string, string, string, string) (map[string]string, error),
|
||||
rowsQuery func(string, string, string, string) ([]string, error),
|
||||
) (*dto.MysqlStatus, error) {
|
||||
statusMap, err := maps(app.ContainerName, app.Key, app.Password, "show global status;")
|
||||
statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1498,7 +1426,7 @@ func loadMysqlStatus(req dto.OperationWithNameAndType, app *repo.RootInfo,
|
||||
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
||||
masterStatus = "show binary log status;"
|
||||
}
|
||||
rows, err := rowsQuery(app.ContainerName, app.Key, app.Password, masterStatus)
|
||||
rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1603,10 +1531,6 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
|
||||
}
|
||||
|
||||
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
||||
return loadMysqlClientByFrom(database, false)
|
||||
}
|
||||
|
||||
func loadMysqlClientByFrom(database string, mcpLocalOnly bool) (mysql.MysqlClient, string, error) {
|
||||
var (
|
||||
dbInfo client.DBInfo
|
||||
version string
|
||||
@@ -1618,9 +1542,6 @@ func loadMysqlClientByFrom(database string, mcpLocalOnly bool) (mysql.MysqlClien
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if mcpLocalOnly && (databaseItem.ID == 0 || databaseItem.From != "local" || databaseItem.Type != "mysql" || databaseItem.AppInstallID == 0) {
|
||||
return nil, "", errors.New("an installed local MySQL instance is required")
|
||||
}
|
||||
dbInfo.Type = databaseItem.Type
|
||||
dbInfo.From = databaseItem.From
|
||||
dbInfo.Database = database
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
)
|
||||
|
||||
func (u *MysqlService) CreateForMCP(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||
if req.From != "local" || ctx.Err() != nil {
|
||||
return nil, errors.New("invalid MCP database target or request")
|
||||
}
|
||||
writeContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), 310*time.Second)
|
||||
defer cancel()
|
||||
created, err := u.create(writeContext, req, true)
|
||||
if err != nil {
|
||||
return nil, mcpapi.SanitizeBusinessError(err, "database creation failed; inspect the invocation before retrying")
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (u *PostgresqlService) ListDBOption() ([]dto.PostgresqlOption, error) {
|
||||
}
|
||||
|
||||
func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), repo.WithByName(req.Name))
|
||||
@@ -123,7 +123,7 @@ func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error {
|
||||
}
|
||||
|
||||
func (u *PostgresqlService) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) || strings.Contains(req.Password, " ") {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ func (u *PostgresqlService) ChangePrivileges(req dto.PostgresqlPrivileges) error
|
||||
}
|
||||
|
||||
func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
cli, err := LoadPostgresqlClientByFrom(req.Database)
|
||||
|
||||
@@ -882,16 +882,6 @@ func (f *FileService) ChangeName(req request.FileRename) error {
|
||||
if hasInvalidFileName(req.NewName) {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
if req.OldName == req.NewName {
|
||||
return nil
|
||||
}
|
||||
// os.Rename silently replaces an existing target on Linux, so reject the
|
||||
// rename here to avoid losing the target file without confirmation.
|
||||
if _, err := os.Lstat(req.NewName); err == nil {
|
||||
return buserr.New("ErrFileIsExist")
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
||||
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
func TestChangeNameRejectsExistingTarget(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
oldPath := filepath.Join(dir, "old.txt")
|
||||
targetPath := filepath.Join(dir, "target.txt")
|
||||
// Binary source content keeps this test independent of the file-history DB.
|
||||
if err := os.WriteFile(oldPath, []byte("old\x00"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte("target"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: targetPath})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when the target name already exists")
|
||||
}
|
||||
var bizErr buserr.BusinessError
|
||||
if !errors.As(err, &bizErr) || bizErr.Msg != "ErrFileIsExist" {
|
||||
t.Fatalf("expected ErrFileIsExist, got %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(targetPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(content) != "target" {
|
||||
t.Fatalf("target file was overwritten, content = %q", content)
|
||||
}
|
||||
if _, err := os.Stat(oldPath); err != nil {
|
||||
t.Fatalf("source file should still exist after a rejected rename: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeNameRenamesToNewName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
oldPath := filepath.Join(dir, "old")
|
||||
newPath := filepath.Join(dir, "new")
|
||||
if err := os.Mkdir(oldPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: newPath}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(newPath); err != nil {
|
||||
t.Fatalf("renamed path does not exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("old path should be gone, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -131,9 +131,6 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem
|
||||
|
||||
func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) {
|
||||
response := dto.FirewallLifecycleOperationResponse{}
|
||||
if request.BackupFile != "" && request.Operation != string(lifecycle.OperationStart) {
|
||||
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
|
||||
}
|
||||
if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" {
|
||||
return response, s.OperateFirewall(request)
|
||||
}
|
||||
@@ -158,19 +155,6 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
var restoreRequest dto.FirewallRuleCreate
|
||||
if request.BackupFile != "" {
|
||||
if client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW {
|
||||
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
|
||||
}
|
||||
restoreRequest, err = prepareFirewallRuleCreate(dto.FirewallRuleCreate{BackupFile: request.BackupFile, Initialize: true})
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
if err := validateFirewallCreateBatch(restoreRequest, filter.Provider(client.Name())); err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) ||
|
||||
(request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) &&
|
||||
request.Operation != string(lifecycle.OperationRestart)) {
|
||||
@@ -189,26 +173,7 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
|
||||
return response, err
|
||||
}
|
||||
taskItem.AddSubTaskWithOps(name, func(t *task.Task) error {
|
||||
if request.BackupFile != "" {
|
||||
if err := s.checkSelectedProvider(t.TaskCtx, filter.Provider(client.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
prepareStart := func(lifecycle.Client) error {
|
||||
if request.BackupFile != "" {
|
||||
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallCreateRulesStep"), func() error {
|
||||
return s.runFirewallCreateRulesTask(t, restoreRequest, filter.Provider(client.Name()))
|
||||
})
|
||||
}
|
||||
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
|
||||
err := s.SyncPortWhitelist(t.TaskCtx)
|
||||
if err == nil && client.Name() == lifecycle.ProviderFirewalld {
|
||||
err = lifecycleproviders.RemoveFirewalldSSHService()
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
return s.runFirewallLifecycleTask(t, client, request, prepareStart)
|
||||
return s.runFirewallLifecycleTask(t, client, request, true)
|
||||
}, nil, 0, 0)
|
||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||
closeUnstartedFirewallTask(taskItem)
|
||||
@@ -589,9 +554,29 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request
|
||||
}
|
||||
|
||||
func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) {
|
||||
request, err := prepareFirewallRuleCreate(request)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleCreateResponse{}, err
|
||||
if request.BackupFile != "" {
|
||||
if len(request.Items) != 0 {
|
||||
return dto.FirewallRuleCreateResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
items, err := readFirewallBackup(request.BackupFile)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleCreateResponse{}, err
|
||||
}
|
||||
for _, item := range items {
|
||||
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
|
||||
}
|
||||
}
|
||||
|
||||
for index, item := range request.Items {
|
||||
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
|
||||
continue
|
||||
}
|
||||
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
|
||||
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
|
||||
observed[0].Rule.Description = item.Rule.Description
|
||||
request.Items[index].Rule = observed[0].Rule
|
||||
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := s.selectedProvider(ctx)
|
||||
@@ -624,11 +609,30 @@ func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCr
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, nil)
|
||||
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, false)
|
||||
}, nil)
|
||||
}
|
||||
ruleState := make(map[string]filter.RuleSet)
|
||||
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error {
|
||||
return s.runFirewallCreateRulesTask(t, request, provider)
|
||||
firewallRuleMutationMu.Lock()
|
||||
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
return err
|
||||
}
|
||||
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
ruleState = nil
|
||||
}
|
||||
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
|
||||
if whitelistErr == nil && provider == filter.ProviderFirewalld {
|
||||
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
|
||||
}
|
||||
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
|
||||
return errors.Join(err, whitelistErr)
|
||||
}, nil, 0, 0)
|
||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||
@@ -773,11 +777,7 @@ func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
whitelistErr := s.SyncPortWhitelist(ctx)
|
||||
if whitelistErr == nil && provider == filter.ProviderFirewalld {
|
||||
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
|
||||
}
|
||||
recordFailure("restore whitelist allowances", whitelistErr)
|
||||
recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx))
|
||||
return errors.Join(recoveryErrors...)
|
||||
}
|
||||
|
||||
@@ -805,9 +805,17 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context,
|
||||
return restoreErr
|
||||
}
|
||||
|
||||
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, prepareStart func(lifecycle.Client) error) error {
|
||||
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, syncWhitelist bool) error {
|
||||
ctx := t.TaskCtx
|
||||
provider := filter.Provider(client.Name())
|
||||
var prepareStart func(lifecycle.Client) error
|
||||
if syncWhitelist {
|
||||
prepareStart = func(lifecycle.Client) error {
|
||||
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
|
||||
return s.SyncPortWhitelist(ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t)
|
||||
if request.Operation == string(lifecycle.OperationStop) {
|
||||
return operationErr
|
||||
|
||||
@@ -554,58 +554,6 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareFirewallRuleCreate(request dto.FirewallRuleCreate) (dto.FirewallRuleCreate, error) {
|
||||
if request.BackupFile != "" {
|
||||
if len(request.Items) != 0 {
|
||||
return request, filter.ErrInvalidRule
|
||||
}
|
||||
items, err := readFirewallBackup(request.BackupFile)
|
||||
if err != nil {
|
||||
return request, err
|
||||
}
|
||||
for _, item := range items {
|
||||
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
|
||||
}
|
||||
}
|
||||
|
||||
for index, item := range request.Items {
|
||||
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
|
||||
continue
|
||||
}
|
||||
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
|
||||
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
|
||||
observed[0].Rule.Description = item.Rule.Description
|
||||
request.Items[index].Rule = observed[0].Rule
|
||||
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
|
||||
}
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (s *FirewallService) runFirewallCreateRulesTask(t *task.Task, request dto.FirewallRuleCreate, provider filter.Provider) error {
|
||||
ruleState := make(map[string]filter.RuleSet)
|
||||
firewallRuleMutationMu.Lock()
|
||||
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
return err
|
||||
}
|
||||
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
ruleState = nil
|
||||
}
|
||||
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
|
||||
if whitelistErr == nil && provider == filter.ProviderFirewalld {
|
||||
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
|
||||
}
|
||||
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
|
||||
return errors.Join(err, whitelistErr)
|
||||
}
|
||||
|
||||
func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) {
|
||||
var firstFailure error
|
||||
defer func() {
|
||||
@@ -1786,6 +1734,9 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
|
||||
var recoveryErrors []error
|
||||
if prepareStart != nil {
|
||||
err := prepareStart(client)
|
||||
if err == nil && client.Name() == constant.FirewallProviderFirewalld {
|
||||
err = lifecycleproviders.RemoveFirewalldSSHService()
|
||||
}
|
||||
if err != nil {
|
||||
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err))
|
||||
}
|
||||
@@ -1807,82 +1758,17 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
|
||||
}
|
||||
|
||||
func (c firewallLifecycleClient) Start() error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
|
||||
global.LOG.Warnf("prepare %s access whitelist before start failed: %v", c.Name(), err)
|
||||
}
|
||||
return c.Client.Start()
|
||||
}
|
||||
|
||||
func (c firewallLifecycleClient) Restart() error {
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
|
||||
global.LOG.Warnf("prepare %s access whitelist before restart failed: %v", c.Name(), err)
|
||||
}
|
||||
return c.Client.Restart()
|
||||
}
|
||||
|
||||
func (c firewallLifecycleClient) prepareAccessWhitelistBeforeStart() error {
|
||||
provider := filter.Provider(c.Name())
|
||||
if provider != filter.ProviderFirewalld && provider != filter.ProviderUFW {
|
||||
return nil
|
||||
}
|
||||
ports, err := LoadRequiredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var adapter filter.Adapter
|
||||
var active bool
|
||||
if provider == filter.ProviderFirewalld {
|
||||
active, err = c.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adapter = filterfirewalld.NewAdapter()
|
||||
} else {
|
||||
adapter = filterufw.NewAdapter()
|
||||
}
|
||||
var commands []filter.NativeCommand
|
||||
for _, port := range firewall.ExpandPortWhitelist(ports) {
|
||||
rule := firewall.RuleForSystemPort(provider, port)
|
||||
rule.Scope = rule.Scope.Normalize()
|
||||
rule.UUID = uuid.NewString()
|
||||
if provider == filter.ProviderUFW {
|
||||
position := int64(1)
|
||||
rule.OrderIndex = &position
|
||||
}
|
||||
plan, err := adapter.BuildCommands(filter.RuleSet{Scope: rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &rule}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range plan.Rules {
|
||||
for _, command := range item.Commands {
|
||||
if provider == filter.ProviderFirewalld {
|
||||
if active {
|
||||
command.Args = append(command.Args, "--permanent")
|
||||
} else {
|
||||
command.Executable = "firewall-offline-cmd"
|
||||
}
|
||||
}
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en"))
|
||||
for _, command := range commands {
|
||||
if err := manager.RunWithOptionalSudo(command.Executable, command.Args...); err != nil {
|
||||
return fmt.Errorf("prepare %s access whitelist before start: %w", provider, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
|
||||
t.Log(i18n.GetWithName("TaskStart", name))
|
||||
started := time.Now()
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
)
|
||||
|
||||
const mcpMysqlStoppedMessage = "The selected MySQL instance is not running. Start the database service and try the query again."
|
||||
const mcpMysqlConnectionFailedMessage = "Cannot connect to the selected MySQL instance. Check the database service, connection settings and panel logs."
|
||||
|
||||
var errMCPMysqlStopped = mcpapi.NewBusinessError(mcpapi.CodeMySQLStopped, mcpMysqlStoppedMessage)
|
||||
var errMCPMysqlConnectionFailed = mcpapi.NewBusinessError(mcpapi.CodeMySQLConnectionFailed, mcpMysqlConnectionFailedMessage)
|
||||
|
||||
var mcpMysqlStoppedPattern = regexp.MustCompile(`^Error response from daemon: [Cc]ontainer [A-Za-z0-9][A-Za-z0-9_.-]{0,127} is not running$`)
|
||||
var mcpMysqlConnectionPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`^ERROR 2002 \(HY000\): Can't connect to local MySQL server through socket '[^'\r\n]{1,1000}' \([0-9]{1,10}\)$`),
|
||||
regexp.MustCompile(`^ERROR 2003 \(HY000\): Can't connect to MySQL server on '[^'\r\n]{1,1000}' \([0-9]{1,10}\)$`),
|
||||
regexp.MustCompile(`^ERROR 1045 \(28000\): Access denied for user '[^'\r\n]{1,128}'@'[^'\r\n]{1,255}' \(using password: (YES|NO)\)$`),
|
||||
}
|
||||
|
||||
func classifyMCPMysqlReadFailure(stderr string) error {
|
||||
generic := errors.New("local MySQL status query failed")
|
||||
if len(stderr) == 0 || len(stderr) > 2048 || !utf8.ValidString(stderr) {
|
||||
return generic
|
||||
}
|
||||
warning := "mysql: [Warning] Using a password on the command line interface can be insecure."
|
||||
stderr = strings.TrimPrefix(stderr, warning+"\n")
|
||||
if strings.HasSuffix(stderr, "\r\n") {
|
||||
stderr = strings.TrimSuffix(stderr, "\r\n")
|
||||
} else {
|
||||
stderr = strings.TrimSuffix(stderr, "\n")
|
||||
}
|
||||
if strings.IndexFunc(stderr, unicode.IsControl) >= 0 {
|
||||
return generic
|
||||
}
|
||||
if mcpMysqlStoppedPattern.MatchString(stderr) {
|
||||
return errMCPMysqlStopped
|
||||
}
|
||||
for _, pattern := range mcpMysqlConnectionPatterns {
|
||||
if pattern.MatchString(stderr) {
|
||||
return errMCPMysqlConnectionFailed
|
||||
}
|
||||
}
|
||||
return generic
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"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/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const mcpMysqlQueryTimeout = 30 * time.Second
|
||||
const mcpMysqlQueryOutputLimit = 1 << 20
|
||||
|
||||
func mcpLocalMysqlQuery(ctx context.Context) *gorm.DB {
|
||||
return global.DB.WithContext(ctx).Table("databases AS d").
|
||||
Joins("JOIN app_installs AS ai ON ai.id = d.app_install_id AND ai.name = d.name").
|
||||
Joins("JOIN apps AS a ON a.id = ai.app_id").
|
||||
Where("d.type = ? AND d.\"from\" = ? AND a.key = ?", constant.AppMysql, "local", constant.AppMysql).
|
||||
Where("ai.status <> ? AND ai.container_name <> ?", constant.StatusDeleted, "")
|
||||
}
|
||||
|
||||
func (u *DatabaseService) ListForMCP(ctx context.Context, dbType string) ([]dto.DatabaseOption, error) {
|
||||
if dbType != constant.AppMysql {
|
||||
return nil, errors.New("only installed local MySQL instances are supported")
|
||||
}
|
||||
result := make([]dto.DatabaseOption, 0)
|
||||
err := mcpLocalMysqlQuery(ctx).Select("d.id, d.type, d.\"from\", d.name AS database, ai.version").Order("d.id").Scan(&result).Error
|
||||
if err != nil {
|
||||
return nil, errors.New("cannot read local MySQL instances")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type mcpMysqlReadRunner func(context.Context, string, string, string) (string, error)
|
||||
|
||||
func (u *MysqlService) LoadStatusForMCP(ctx context.Context, req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) {
|
||||
return u.loadStatusForMCP(ctx, req, runMCPMysqlRead)
|
||||
}
|
||||
func (u *MysqlService) loadStatusForMCP(ctx context.Context, req dto.OperationWithNameAndType, run mcpMysqlReadRunner) (*dto.MysqlStatus, error) {
|
||||
if req.Type != constant.AppMysql || req.Name == "" || len(req.Name) > 128 || strings.TrimSpace(req.Name) != req.Name || strings.ContainsAny(req.Name, "\x00\r\n/\\") || ctx.Err() != nil {
|
||||
return nil, errors.New("an explicitly named installed local MySQL instance is required")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, mcpMysqlQueryTimeout)
|
||||
defer cancel()
|
||||
var target struct {
|
||||
ID uint
|
||||
ContainerName, Version, Env string
|
||||
}
|
||||
if err := mcpLocalMysqlQuery(ctx).Where("d.name = ?", req.Name).Select("d.id, ai.container_name, ai.version, ai.env").Take(&target).Error; err != nil || target.ID == 0 {
|
||||
return nil, errors.New("the installed local MySQL instance is unavailable")
|
||||
}
|
||||
var env map[string]any
|
||||
if json.Unmarshal([]byte(target.Env), &env) != nil {
|
||||
return nil, errors.New("local MySQL configuration is unavailable")
|
||||
}
|
||||
password, ok := env["PANEL_DB_ROOT_PASSWORD"].(string)
|
||||
if !ok || password == "" {
|
||||
return nil, errors.New("local MySQL configuration is unavailable")
|
||||
}
|
||||
app := &repo.RootInfo{ContainerName: target.ContainerName, Version: target.Version, Key: constant.AppMysql, Password: password}
|
||||
rows := func(container, _, password, sql string) ([]string, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, errors.New("local MySQL status query failed")
|
||||
}
|
||||
output, err := run(ctx, container, password, sql)
|
||||
if err != nil {
|
||||
if err == errMCPMysqlStopped || err == errMCPMysqlConnectionFailed {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("local MySQL status query failed")
|
||||
}
|
||||
return strings.Split(output, "\n"), nil
|
||||
}
|
||||
maps := func(container, dbType, password, sql string) (map[string]string, error) {
|
||||
lines, err := rows(container, dbType, password, sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values := map[string]string{}
|
||||
for _, line := range lines {
|
||||
if pair := strings.Split(line, "\t"); len(pair) == 2 {
|
||||
values[pair[0]] = pair[1]
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
return loadMysqlStatus(req, app, maps, rows)
|
||||
}
|
||||
|
||||
type mcpQueryOutput struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
exceeded bool
|
||||
}
|
||||
|
||||
func (b *mcpQueryOutput) Write(data []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if len(data) > mcpMysqlQueryOutputLimit-b.buffer.Len() {
|
||||
b.exceeded = true
|
||||
return 0, errors.New("query output limit exceeded")
|
||||
}
|
||||
return b.buffer.Write(data)
|
||||
}
|
||||
func (b *mcpQueryOutput) Len() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buffer.Len()
|
||||
}
|
||||
func (b *mcpQueryOutput) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buffer.String()
|
||||
}
|
||||
func runMCPMysqlRead(ctx context.Context, container, password, sql string) (string, error) {
|
||||
switch sql {
|
||||
case "show global status;", "show master status;", "show binary log status;":
|
||||
default:
|
||||
return "", errors.New("unsupported MySQL monitoring query")
|
||||
}
|
||||
command := exec.CommandContext(ctx, "docker", "exec", container, "mysql", "-uroot", "-p"+password, "-e", sql)
|
||||
output := &mcpQueryOutput{}
|
||||
var stderr bytes.Buffer
|
||||
command.Stdout, command.Stderr = output, io.MultiWriter(output, &stderr)
|
||||
runErr := command.Run()
|
||||
if ctx.Err() != nil || output.exceeded {
|
||||
return "", errors.New("local MySQL status query failed")
|
||||
}
|
||||
if runErr != nil {
|
||||
return "", classifyMCPMysqlReadFailure(stderr.String())
|
||||
}
|
||||
value := strings.ReplaceAll(output.String(), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
if strings.HasPrefix(value, "ERROR ") {
|
||||
return "", errors.New("local MySQL status query failed")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (a *AppInstallService) GetAppInstallInfoForMCP(ctx context.Context, id uint) (*response.AppInstallInfo, error) {
|
||||
var installed model.AppInstall
|
||||
if id == 0 {
|
||||
return nil, errors.New("invalid installed application")
|
||||
}
|
||||
err := global.DB.WithContext(ctx).Preload("App").Where("id = ?", id).First(&installed).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &response.AppInstallInfo{Status: constant.StatusDeleted}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.New("installed application metadata is unavailable")
|
||||
}
|
||||
var env map[string]any
|
||||
if json.Unmarshal([]byte(installed.Env), &env) != nil {
|
||||
return nil, errors.New("installed application metadata is unavailable")
|
||||
}
|
||||
result := &response.AppInstallInfo{ID: installed.ID, Name: installed.Name, Version: installed.Version, Status: installed.Status, HttpPort: installed.HttpPort, AppKey: installed.App.Key, AppPorts: []int{}}
|
||||
for key, value := range env {
|
||||
if !strings.Contains(strings.ToUpper(key), "PANEL_APP_PORT") {
|
||||
continue
|
||||
}
|
||||
var port int
|
||||
switch item := value.(type) {
|
||||
case string:
|
||||
port, _ = strconv.Atoi(item)
|
||||
case float64:
|
||||
if item == float64(int(item)) {
|
||||
port = int(item)
|
||||
}
|
||||
}
|
||||
if port > 0 && port <= 65535 {
|
||||
result.AppPorts = append(result.AppPorts, port)
|
||||
}
|
||||
}
|
||||
sort.Ints(result.AppPorts)
|
||||
return result, nil
|
||||
}
|
||||
@@ -55,7 +55,7 @@ type IMonitorService interface {
|
||||
LoadIOOptions() []string
|
||||
LoadNetworkOptions() []string
|
||||
LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
|
||||
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error)
|
||||
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
|
||||
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
|
||||
LoadGPUOptions() dto.MonitorGPUOptions
|
||||
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
|
||||
|
||||
@@ -60,9 +60,6 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
for i := range points {
|
||||
points[i].MonitorVLLM.CompletedRequests = points[i].CompletedRequests
|
||||
}
|
||||
if data.BucketSeconds > 0 {
|
||||
aggregates := map[int64]*vllm.HistoryAccumulator{}
|
||||
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
|
||||
@@ -94,6 +91,15 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
|
||||
data.Points = append(data.Points, point)
|
||||
}
|
||||
} else {
|
||||
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
intervalSeconds, err := strconv.Atoi(setting)
|
||||
if err != nil || intervalSeconds <= 0 {
|
||||
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
|
||||
}
|
||||
interval := time.Duration(intervalSeconds) * time.Second
|
||||
for i, point := range points {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return data, err
|
||||
@@ -103,20 +109,8 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
|
||||
return data, err
|
||||
}
|
||||
aggregate.Apply(&point.MonitorVLLM, "max")
|
||||
if i > 0 {
|
||||
previous := points[i-1]
|
||||
interval := time.Duration(max(previous.SampleIntervalSeconds, point.SampleIntervalSeconds)) * time.Second
|
||||
if interval == 0 {
|
||||
if i > 1 {
|
||||
interval = previous.CreatedAt.Sub(points[i-2].CreatedAt)
|
||||
}
|
||||
if i+1 < len(points) {
|
||||
interval = max(interval, points[i+1].CreatedAt.Sub(point.CreatedAt))
|
||||
}
|
||||
}
|
||||
if interval > 0 && point.CreatedAt.Sub(previous.CreatedAt) > 2*interval {
|
||||
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: previous.CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
||||
}
|
||||
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
||||
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
||||
}
|
||||
data.Points = append(data.Points, point.MonitorVLLM)
|
||||
}
|
||||
@@ -124,30 +118,31 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error) {
|
||||
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||
if err != nil {
|
||||
return dto.MonitorVLLMCurrentData{}, err
|
||||
return model.MonitorVLLM{}, err
|
||||
}
|
||||
if install.App.Key != "vllm" {
|
||||
return dto.MonitorVLLMCurrentData{}, fmt.Errorf("not a vLLM installation")
|
||||
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
|
||||
}
|
||||
point, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
|
||||
data := dto.MonitorVLLMCurrentData{MonitorVLLM: point}
|
||||
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
|
||||
return data, nil
|
||||
return previous, nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(point.RawMetrics), &data.Metrics); err != nil {
|
||||
return data, err
|
||||
timer := time.NewTimer(time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return model.MonitorVLLM{}, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
for name := range data.Metrics {
|
||||
if name == "request_prompt_tokens_sum" || name == "request_prompt_tokens_count" ||
|
||||
(!strings.HasSuffix(name, "_total") && !strings.HasSuffix(name, "_sum") && !strings.HasSuffix(name, "_count")) {
|
||||
delete(data.Metrics, name)
|
||||
}
|
||||
point, err := collectVLLMMetrics(ctx, install, previous)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
|
||||
}
|
||||
return data, nil
|
||||
return point, nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
|
||||
@@ -169,7 +164,7 @@ func StartVLLMMonitor(interval string) error {
|
||||
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
|
||||
}
|
||||
service := &MonitorService{}
|
||||
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(func() { service.saveVLLMData(seconds) }))
|
||||
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
|
||||
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -181,7 +176,7 @@ func StartVLLMMonitor(interval string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) saveVLLMData(intervalSeconds int) {
|
||||
func (m *MonitorService) saveVLLMData() {
|
||||
if !vllmMonitorMutex.TryLock() {
|
||||
return
|
||||
}
|
||||
@@ -233,7 +228,6 @@ func (m *MonitorService) saveVLLMData(intervalSeconds int) {
|
||||
return err
|
||||
}
|
||||
point, err := collectVLLMMetrics(context.Background(), install, previous)
|
||||
point.SampleIntervalSeconds = intervalSeconds
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
|
||||
}
|
||||
|
||||
@@ -78,17 +78,9 @@ func getNginxParamsByKeys(scope string, keys []string, website *model.Website) (
|
||||
var res []response.NginxParam
|
||||
var block components.IBlock
|
||||
if scope == constant.NginxScopeHttp {
|
||||
httpBlock := nginxFull.RootConfig.Config.FindHttp()
|
||||
if httpBlock == nil {
|
||||
return nil, fmt.Errorf("nginx HTTP configuration block is missing")
|
||||
}
|
||||
block = httpBlock
|
||||
block = nginxFull.RootConfig.Config.FindHttp()
|
||||
} else {
|
||||
servers := nginxFull.SiteConfig.Config.FindServers()
|
||||
if len(servers) == 0 {
|
||||
return nil, fmt.Errorf("website nginx server block is missing")
|
||||
}
|
||||
block = servers[0]
|
||||
block = nginxFull.SiteConfig.Config.FindServers()[0]
|
||||
}
|
||||
for _, key := range keys {
|
||||
dirs := block.FindDirectives(key)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||
return launcherRepo.ListQuickDirectories()
|
||||
}
|
||||
|
||||
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
|
||||
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
|
||||
if strings.TrimSpace(item.Path) == "" {
|
||||
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
|
||||
item, err := quickDirectoryValues(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
|
||||
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.ID = req.ID
|
||||
item.Legacy = req.Legacy
|
||||
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
|
||||
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -47,7 +46,7 @@ type RuntimeService struct {
|
||||
type IRuntimeService interface {
|
||||
Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error)
|
||||
Create(create request.RuntimeCreate) (*model.Runtime, error)
|
||||
Delete(delete request.RuntimeDelete) (string, error)
|
||||
Delete(delete request.RuntimeDelete) error
|
||||
Update(req request.RuntimeUpdate) error
|
||||
Get(id uint) (res *response.RuntimeDTO, err error)
|
||||
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
|
||||
@@ -377,16 +376,16 @@ func (r *RuntimeService) DeleteCheck(runTimeId uint) ([]dto.AppResource, error)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) (string, error) {
|
||||
func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
|
||||
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(runtimeDelete.ID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID))
|
||||
if website.ID > 0 {
|
||||
return runtime.Name, buserr.New("ErrDelWithWebsite")
|
||||
return buserr.New("ErrDelWithWebsite")
|
||||
}
|
||||
return runtime.Name, startRuntimeDeleteTask(runtimeDelete, runtime)
|
||||
return startRuntimeDeleteTask(runtimeDelete, runtime)
|
||||
}
|
||||
|
||||
func startRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *model.Runtime) error {
|
||||
@@ -585,21 +584,6 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
||||
|
||||
for _, form := range appForm.FormFields {
|
||||
if v, ok := envs[form.EnvKey]; ok {
|
||||
if form.EnvKey == "PHP_VERSION" {
|
||||
// Refresh only version choices; keep the saved form if synchronized metadata is unavailable.
|
||||
var latestForm dto.AppForm
|
||||
if json.Unmarshal([]byte(appDetail.Params), &latestForm) == nil {
|
||||
for _, latest := range latestForm.FormFields {
|
||||
if latest.EnvKey == form.EnvKey && len(latest.Values) > 0 {
|
||||
form.Values = latest.Values
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if v != "" && !slices.ContainsFunc(form.Values, func(value dto.AppFormValue) bool { return value.Value == v }) {
|
||||
form.Values = append(form.Values, dto.AppFormValue{Label: v, Value: v})
|
||||
}
|
||||
}
|
||||
appParam := response.AppParam{
|
||||
Edit: false,
|
||||
Key: form.EnvKey,
|
||||
@@ -647,7 +631,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) {
|
||||
func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -656,7 +640,6 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) {
|
||||
runtime.Version = req.Version
|
||||
return runtimeRepo.Save(runtime)
|
||||
}
|
||||
oldRuntime := *runtime
|
||||
appDetail, err := getRuntimeUpdateAppDetail(runtime, req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -701,20 +684,6 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) {
|
||||
if err = downloadApp(app, appDetail, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
var snapshot *runtimeUpdateSnapshot
|
||||
if runtime.Type != constant.RuntimePHP {
|
||||
snapshot, err = snapshotRuntimeUpdate(&oldRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if restoreErr := snapshot.restoreFiles(); restoreErr != nil {
|
||||
err = fmt.Errorf("%w; restore runtime configuration: %v", err, restoreErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
if versionChanged {
|
||||
if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil {
|
||||
return err
|
||||
@@ -788,13 +757,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) {
|
||||
if err = runtimeRepo.Save(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
if err := prepareRuntimeUpdate(runtime, snapshot); err != nil {
|
||||
global.LOG.Errorf("prepare runtime [%s] update failed: %v", runtime.Name, err)
|
||||
return
|
||||
}
|
||||
reCreateRuntime(runtime)
|
||||
}()
|
||||
go reCreateRuntime(runtime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
@@ -15,89 +12,6 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Keep the on-disk configuration as well as the model: files may contain edits
|
||||
// that have not been reflected in the database.
|
||||
type runtimeUpdateSnapshot struct {
|
||||
runtime model.Runtime
|
||||
files []runtimeUpdateFile
|
||||
}
|
||||
|
||||
type runtimeUpdateFile struct {
|
||||
path string
|
||||
content []byte
|
||||
mode os.FileMode
|
||||
exists bool
|
||||
}
|
||||
|
||||
func snapshotRuntimeUpdate(runtime *model.Runtime) (*runtimeUpdateSnapshot, error) {
|
||||
snapshot := &runtimeUpdateSnapshot{runtime: *runtime}
|
||||
for _, name := range []string{"docker-compose.yml", ".env", "run.sh"} {
|
||||
file := runtimeUpdateFile{path: filepath.Join(runtime.GetPath(), name)}
|
||||
info, err := os.Stat(file.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
snapshot.files = append(snapshot.files, file)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.content, err = os.ReadFile(file.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.exists, file.mode = true, info.Mode().Perm()
|
||||
snapshot.files = append(snapshot.files, file)
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (s *runtimeUpdateSnapshot) restoreFiles() error {
|
||||
var result error
|
||||
for _, file := range s.files {
|
||||
var err error
|
||||
if file.exists {
|
||||
err = os.WriteFile(file.path, file.content, file.mode)
|
||||
if err == nil {
|
||||
err = os.Chmod(file.path, file.mode)
|
||||
}
|
||||
} else {
|
||||
err = os.Remove(file.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
result = errors.Join(result, fmt.Errorf("restore %s: %w", file.path, err))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// This must finish before reCreateRuntime stops the existing container. A
|
||||
// failed preparation restores the previous version without touching Docker.
|
||||
func prepareRuntimeUpdate(runtime *model.Runtime, snapshot *runtimeUpdateSnapshot) error {
|
||||
if err := pullRuntimeComposeImages(runtime.GetComposePath(), false, nil); err != nil {
|
||||
result := fmt.Errorf("prepare runtime images: %w", err)
|
||||
restoreErr := snapshot.restoreFiles()
|
||||
*runtime = snapshot.runtime
|
||||
if restoreErr != nil {
|
||||
result = errors.Join(result, restoreErr)
|
||||
runtime.Status = constant.StatusError
|
||||
}
|
||||
runtime.Message = result.Error()
|
||||
if saveErr := runtimeRepo.Save(runtime); saveErr != nil {
|
||||
result = errors.Join(result, fmt.Errorf("restore runtime record: %w", saveErr))
|
||||
}
|
||||
logFile, logErr := os.OpenFile(runtime.GetLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
|
||||
if logErr == nil {
|
||||
_, logErr = fmt.Fprintln(logFile, result.Error())
|
||||
logErr = errors.Join(logErr, logFile.Close())
|
||||
}
|
||||
return errors.Join(result, logErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) {
|
||||
current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
|
||||
if err != nil {
|
||||
|
||||
+38
-117
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -974,16 +973,11 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS,
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
applyWebsiteHTTPSParams(&res, params)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func applyWebsiteHTTPSParams(res *response.WebsiteHTTPS, params []response.NginxParam) {
|
||||
for _, p := range params {
|
||||
if p.Name == "ssl_protocols" {
|
||||
res.SSLProtocol = p.Params
|
||||
}
|
||||
if p.Name == "ssl_ciphers" && len(p.Params) > 0 {
|
||||
if p.Name == "ssl_ciphers" {
|
||||
res.Algorithm = p.Params[0]
|
||||
}
|
||||
if p.Name == "add_header" && len(p.Params) > 0 {
|
||||
@@ -1001,6 +995,7 @@ func applyWebsiteHTTPSParams(res *response.WebsiteHTTPS, params []response.Nginx
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) {
|
||||
@@ -1330,12 +1325,11 @@ func (w WebsiteService) ChangeDefaultServer(id uint) error {
|
||||
return websiteRepo.Save(context.Background(), &website)
|
||||
}
|
||||
|
||||
func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) (err error) {
|
||||
func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error {
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wasPHP := website.Type == constant.Runtime
|
||||
if website.Type == constant.Runtime {
|
||||
oldRuntime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(website.RuntimeID))
|
||||
if err != nil {
|
||||
@@ -1353,8 +1347,26 @@ func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) (err
|
||||
return buserr.WithName("ErrImageNotExist", oldRuntime.Name)
|
||||
}
|
||||
}
|
||||
|
||||
configPath := GetSitePath(website, SiteConf)
|
||||
nginxContent, err := files.NewFileOp().GetContent(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := parser.NewStringParser(string(nginxContent)).Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
servers := config.FindServers()
|
||||
if len(servers) == 0 {
|
||||
return errors.New("nginx config is not valid")
|
||||
}
|
||||
server := servers[0]
|
||||
fileOp := files.NewFileOp()
|
||||
indexPHPPath := path.Join(GetSitePath(website, SiteIndexDir), "index.php")
|
||||
indexHtmlPath := path.Join(GetSitePath(website, SiteIndexDir), "index.html")
|
||||
if req.RuntimeID > 0 {
|
||||
server.UpdateDirective("index", []string{"index.php index.html index.htm default.php default.htm default.html"})
|
||||
server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"})
|
||||
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(req.RuntimeID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1362,129 +1374,38 @@ func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) (err
|
||||
if runtime.Resource == constant.ResourceLocal {
|
||||
return buserr.New("ErrPHPResource")
|
||||
}
|
||||
website.RuntimeID = runtime.ID
|
||||
website.RuntimeID = req.RuntimeID
|
||||
website.AppInstallID = 0
|
||||
website.Proxy = fmt.Sprintf("127.0.0.1:%s", runtime.Port)
|
||||
phpProxy := fmt.Sprintf("127.0.0.1:%s", runtime.Port)
|
||||
website.Proxy = phpProxy
|
||||
server.UpdatePHPProxy([]string{website.Proxy}, "")
|
||||
website.Type = constant.Runtime
|
||||
if !fileOp.Stat(indexPHPPath) {
|
||||
_ = fileOp.WriteFile(indexPHPPath, strings.NewReader(string(nginx_conf.IndexPHP)), constant.FilePerm)
|
||||
}
|
||||
} else {
|
||||
server.UpdateDirective("index", []string{"index.html index.php index.htm default.php default.htm default.html"})
|
||||
website.RuntimeID = 0
|
||||
website.Type = constant.Static
|
||||
website.Proxy = ""
|
||||
server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"})
|
||||
if !fileOp.Stat(indexHtmlPath) {
|
||||
_ = fileOp.WriteFile(indexHtmlPath, strings.NewReader(string(nginx_conf.Index)), constant.FilePerm)
|
||||
}
|
||||
}
|
||||
|
||||
subsites, err := websiteRepo.List(websiteRepo.WithParentID(website.ID), websiteRepo.WithType(constant.Subsite))
|
||||
if err != nil {
|
||||
config.FilePath = configPath
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
websites := append([]model.Website{website}, subsites...)
|
||||
configs := make([]*components.Config, 0, len(websites))
|
||||
oldContents := make(map[string][]byte, len(websites))
|
||||
fileOp := files.NewFileOp()
|
||||
for i := range websites {
|
||||
site := &websites[i]
|
||||
if i > 0 {
|
||||
site.RuntimeID = website.RuntimeID
|
||||
site.Proxy = website.Proxy
|
||||
}
|
||||
configPath := GetSitePath(*site, SiteConf)
|
||||
content, err := fileOp.GetContent(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := parser.NewStringParser(string(content)).Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
servers := config.FindServers()
|
||||
if len(servers) == 0 {
|
||||
return errors.New("nginx config is not valid")
|
||||
}
|
||||
server := servers[0]
|
||||
if i == 0 || wasPHP != (req.RuntimeID > 0) {
|
||||
if req.RuntimeID > 0 {
|
||||
server.UpdateDirective("index", []string{"index.php index.html index.htm default.php default.htm default.html"})
|
||||
} else {
|
||||
server.UpdateDirective("index", []string{"index.html index.php index.htm default.php default.htm default.html"})
|
||||
}
|
||||
}
|
||||
if req.RuntimeID > 0 && site.Status != constant.WebStopped {
|
||||
updated := false
|
||||
for _, location := range server.FindDirectives("location") {
|
||||
if strings.Join(location.GetParameters(), " ") == "~ [^/]\\.php(/|$)" && location.GetBlock() != nil {
|
||||
location.GetBlock().UpdateDirective("fastcgi_pass", []string{site.Proxy})
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
server.UpdatePHPProxy([]string{site.Proxy}, "")
|
||||
}
|
||||
} else {
|
||||
server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"})
|
||||
}
|
||||
config.FilePath = configPath
|
||||
configs = append(configs, config)
|
||||
oldContents[configPath] = content
|
||||
}
|
||||
|
||||
indexPath := path.Join(GetSitePath(website, SiteIndexDir), "index.html")
|
||||
indexContent := nginx_conf.Index
|
||||
if req.RuntimeID > 0 {
|
||||
indexPath = path.Join(GetSitePath(website, SiteIndexDir), "index.php")
|
||||
indexContent = nginx_conf.IndexPHP
|
||||
}
|
||||
if !fileOp.Stat(indexPath) {
|
||||
_ = fileOp.WriteFile(indexPath, strings.NewReader(string(indexContent)), constant.FilePerm)
|
||||
}
|
||||
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx, ctx := helper.GetTxAndContext()
|
||||
if err = tx.Error; err != nil {
|
||||
if err = nginxCheckAndReload(string(nginxContent), configPath, nginxInstall.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
written := 0
|
||||
reloadAttempted := false
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if rollbackErr := tx.Rollback().Error; rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
|
||||
err = errors.Join(err, rollbackErr)
|
||||
}
|
||||
for _, config := range configs[:written] {
|
||||
if restoreErr := fileOp.WriteFile(config.FilePath, strings.NewReader(string(oldContents[config.FilePath])), constant.FilePerm); restoreErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("restore %s: %w", config.FilePath, restoreErr))
|
||||
}
|
||||
}
|
||||
if reloadAttempted {
|
||||
if checkErr := opNginx(nginxInstall.ContainerName, constant.NginxCheck); checkErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("check restored nginx configuration: %w", checkErr))
|
||||
} else if reloadErr := opNginx(nginxInstall.ContainerName, constant.NginxReload); reloadErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("reload restored nginx configuration: %w", reloadErr))
|
||||
}
|
||||
}
|
||||
}()
|
||||
for i := range websites {
|
||||
if err = websiteRepo.Save(ctx, &websites[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, config := range configs {
|
||||
written++
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
reloadAttempted = true
|
||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit().Error
|
||||
return websiteRepo.Save(context.Background(), &website)
|
||||
}
|
||||
|
||||
func (w WebsiteService) UpdateSiteDir(req request.WebsiteUpdateDir) error {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -22,27 +18,25 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
|
||||
"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/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WebsiteCAService struct {
|
||||
}
|
||||
|
||||
type IWebsiteCAService interface {
|
||||
Import(req request.WebsiteCAImport) error
|
||||
Upload(name string, archive []byte) error
|
||||
Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error)
|
||||
Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
|
||||
GetCA(id uint) (*response.WebsiteCADTO, error)
|
||||
@@ -218,17 +212,10 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
websiteSSL.Shell = req.Shell
|
||||
}
|
||||
if req.PushDir {
|
||||
dirs := certificateDirectories(req.Dir)
|
||||
if len(dirs) == 0 {
|
||||
if !files.NewFileOp().Stat(req.Dir) {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
websiteSSL.Dir = req.Dir
|
||||
}
|
||||
if req.Domains != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
@@ -340,7 +327,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
websiteSSL.ExpireDate = cert.NotAfter
|
||||
websiteSSL.StartDate = cert.NotBefore
|
||||
websiteSSL.Type = cert.Issuer.CommonName
|
||||
websiteSSL.Organization = strings.Join(rootCsr.Subject.Organization, ",")
|
||||
websiteSSL.Organization = rootCsr.Subject.Organization[0]
|
||||
websiteSSL.Status = constant.SSLReady
|
||||
|
||||
if req.Renew {
|
||||
@@ -361,9 +348,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
@@ -448,150 +433,3 @@ func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
|
||||
}
|
||||
return os.Open(path.Join(dir, fileName))
|
||||
}
|
||||
|
||||
func (w WebsiteCAService) Upload(name string, archive []byte) error {
|
||||
ca, err := parseCAArchive(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return saveImportedCA(name, ca)
|
||||
}
|
||||
|
||||
func saveImportedCA(name string, ca *model.WebsiteCA) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || len(name) > 128 || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") {
|
||||
return buserr.WithDetail("ErrInvalidParams", "name", nil)
|
||||
}
|
||||
exist, err := websiteCARepo.GetFirst(repo.WithByName(name))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if exist.ID > 0 {
|
||||
return buserr.New("ErrNameIsExist")
|
||||
}
|
||||
ca.Name = name
|
||||
return websiteCARepo.Create(context.Background(), ca)
|
||||
}
|
||||
|
||||
// Read only the certificate and key in memory; never extract user-supplied paths.
|
||||
func parseCAArchive(archive []byte) (*model.WebsiteCA, error) {
|
||||
invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAUpload") }
|
||||
if len(archive) > 10<<20 {
|
||||
return invalid()
|
||||
}
|
||||
reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
|
||||
if err != nil || len(reader.File) > 16 {
|
||||
return invalid()
|
||||
}
|
||||
contents := make(map[string][]byte)
|
||||
for _, file := range reader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
name := path.Base(file.Name)
|
||||
if (name != "ca.crt" && name != "ca.key") || contents[name] != nil || file.UncompressedSize64 > 1<<20 {
|
||||
return invalid()
|
||||
}
|
||||
stream, err := file.Open()
|
||||
if err != nil {
|
||||
return invalid()
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(stream, (1<<20)+1))
|
||||
stream.Close()
|
||||
if err != nil || len(data) > 1<<20 {
|
||||
return invalid()
|
||||
}
|
||||
contents[name] = data
|
||||
}
|
||||
return parseAndValidateRootCA(contents["ca.crt"], contents["ca.key"])
|
||||
}
|
||||
|
||||
func parseAndValidateRootCA(certificate, privateKey []byte) (*model.WebsiteCA, error) {
|
||||
invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAImport") }
|
||||
if len(certificate) > 1<<20 || len(privateKey) > 1<<20 {
|
||||
return invalid()
|
||||
}
|
||||
pair, err := tls.X509KeyPair(certificate, privateKey)
|
||||
if err != nil || len(pair.Certificate) != 1 {
|
||||
return invalid()
|
||||
}
|
||||
cert := pair.Leaf
|
||||
now := time.Now()
|
||||
if cert == nil || !cert.IsCA || !cert.BasicConstraintsValid || cert.KeyUsage&x509.KeyUsageCertSign == 0 ||
|
||||
!bytes.Equal(cert.RawIssuer, cert.RawSubject) || cert.CheckSignatureFrom(cert) != nil ||
|
||||
now.Before(cert.NotBefore) || !now.Before(cert.NotAfter) {
|
||||
return invalid()
|
||||
}
|
||||
ca := &model.WebsiteCA{CSR: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))}
|
||||
// Normalize keys to the formats used by ObtainSSL, including PKCS#8 uploads.
|
||||
switch key := pair.PrivateKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
bits := key.Curve.Params().BitSize
|
||||
if bits != 256 && bits != 384 {
|
||||
return invalid()
|
||||
}
|
||||
der, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return invalid()
|
||||
}
|
||||
ca.KeyType = fmt.Sprintf("EC%d", bits)
|
||||
ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}))
|
||||
case *rsa.PrivateKey:
|
||||
bits := key.N.BitLen()
|
||||
if bits != 2048 && bits != 3072 && bits != 4096 && bits != 8192 {
|
||||
return invalid()
|
||||
}
|
||||
ca.KeyType = fmt.Sprintf("RSA%d", bits)
|
||||
ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
|
||||
default:
|
||||
return invalid()
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
func (w WebsiteCAService) Import(req request.WebsiteCAImport) error {
|
||||
certificate, privateKey := []byte(req.Certificate), []byte(req.PrivateKey)
|
||||
switch req.Type {
|
||||
case "local":
|
||||
var err error
|
||||
certificate, err = readCAImportFile(req.CertificatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, err = readCAImportFile(req.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "paste":
|
||||
default:
|
||||
return buserr.New("ErrCAImport")
|
||||
}
|
||||
ca, err := parseAndValidateRootCA(certificate, privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return saveImportedCA(req.Name, ca)
|
||||
}
|
||||
|
||||
func readCAImportFile(name string) ([]byte, error) {
|
||||
info, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Size() > 1<<20 {
|
||||
return nil, buserr.New("ErrCAImport")
|
||||
}
|
||||
file, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(io.LimitReader(file, (1<<20)+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(content) > 1<<20 {
|
||||
return nil, buserr.New("ErrCAImport")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
@@ -209,17 +209,11 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
websiteSSL.Shell = create.Shell
|
||||
}
|
||||
if create.PushDir {
|
||||
dirs := certificateDirectories(create.Dir)
|
||||
if len(dirs) == 0 {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(create.Dir) {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
websiteSSL.Dir = create.Dir
|
||||
}
|
||||
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
|
||||
|
||||
@@ -579,9 +573,7 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
printSSLLog(logger, "ExecShellStart", nil)
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
@@ -801,16 +793,11 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
|
||||
}
|
||||
|
||||
if update.PushDir {
|
||||
dirs := certificateDirectories(update.Dir)
|
||||
if len(dirs) == 0 {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(update.Dir) {
|
||||
_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
updateParams["dir"] = strings.Join(dirs, "\n")
|
||||
updateParams["dir"] = update.Dir
|
||||
}
|
||||
var domains []string
|
||||
if update.OtherDomains != "" {
|
||||
|
||||
@@ -1454,10 +1454,10 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultHTTPPort, default
|
||||
return
|
||||
}
|
||||
domainModel.Domain = strings.ToLower(domainModel.Domain)
|
||||
domainModel.Port = domain.Port
|
||||
if domain.Port == 0 {
|
||||
domain.Port = defaultHTTPPort
|
||||
}
|
||||
domainModel.Port = domain.Port
|
||||
domainModel.SSL = domain.SSL
|
||||
domainModel.WebsiteID = websiteID
|
||||
domainModels = append(domainModels, domainModel)
|
||||
@@ -1529,41 +1529,26 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// certificateDirectories accepts both legacy single paths and newline-separated paths.
|
||||
func certificateDirectories(value string) []string {
|
||||
var dirs []string
|
||||
seen := make(map[string]bool)
|
||||
for _, dir := range strings.Split(value, "\n") {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = filepath.Clean(dir)
|
||||
if !seen[dir] {
|
||||
seen[dir] = true
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
if !websiteSSL.PushDir {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
for _, dir := range certificateDirectories(websiteSSL.Dir) {
|
||||
status := i18n.GetMsgByKey("Success")
|
||||
for _, file := range []struct{ name, content string }{
|
||||
{"privkey.pem", websiteSSL.PrivateKey},
|
||||
{"fullchain.pem", websiteSSL.Pem},
|
||||
} {
|
||||
if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
|
||||
status = i18n.GetMsgByKey("Failed")
|
||||
logger.Printf("Push dir %s failed: %s", dir, err)
|
||||
}
|
||||
if websiteSSL.PushDir {
|
||||
fileOp := files.NewFileOp()
|
||||
var (
|
||||
pushErr error
|
||||
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
|
||||
)
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr == nil {
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,13 @@
|
||||
"formatZH": "添加 Ollama 模型重试 [name]",
|
||||
"formatEN": "re-add Ollama model [name]"
|
||||
},
|
||||
"/ai/ollama/model/sync": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 Ollama 模型列表",
|
||||
"formatEN": "sync Ollama model list"
|
||||
},
|
||||
"/ai/tensorrt/create": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
@@ -101,29 +108,33 @@
|
||||
"formatEN": "create alert [title]"
|
||||
},
|
||||
"/alert/config/del": {
|
||||
"bodyKeys": [],
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"formatZH": "删除告警配置 [name]",
|
||||
"formatEN": "delete alert config [name]",
|
||||
"beforeFunctions": []
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除告警配置 [id]",
|
||||
"formatEN": "delete alert config [id]"
|
||||
},
|
||||
"/alert/config/status": {
|
||||
"bodyKeys": [
|
||||
"id",
|
||||
"status"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"formatZH": "更新告警配置状态 [name][status]",
|
||||
"formatEN": "update alert config status [name][status]",
|
||||
"beforeFunctions": []
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新告警配置状态 [id][status]",
|
||||
"formatEN": "update alert config status [id][status]"
|
||||
},
|
||||
"/alert/config/update": {
|
||||
"bodyKeys": [
|
||||
"id",
|
||||
"displayName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"formatZH": "更新告警配置 [displayName]",
|
||||
"formatEN": "update alert config [displayName]",
|
||||
"beforeFunctions": []
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新告警配置 [id][displayName]",
|
||||
"formatEN": "update alert config [id][displayName]"
|
||||
},
|
||||
"/alert/del": {
|
||||
"bodyKeys": [
|
||||
@@ -3733,11 +3744,13 @@
|
||||
"formatEN": "Create runtime [name]"
|
||||
},
|
||||
"/runtimes/del": {
|
||||
"bodyKeys": [],
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除运行环境 [name]",
|
||||
"formatEN": "Delete runtime [name]"
|
||||
"formatZH": "删除运行环境 [id]",
|
||||
"formatEN": "Delete runtime [id]"
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -4573,13 +4586,13 @@
|
||||
},
|
||||
"/websites/php/version": {
|
||||
"bodyKeys": [
|
||||
"websiteID"
|
||||
"websiteId"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "websiteID",
|
||||
"input_value": "websiteId",
|
||||
"isList": false,
|
||||
"db": "websites",
|
||||
"output_column": "primary_domain",
|
||||
|
||||
@@ -10,8 +10,6 @@ const (
|
||||
AppOpenclaw = "openclaw"
|
||||
AppCopaw = "copaw"
|
||||
AppHermesAgent = "hermes-agent"
|
||||
AppDeepSeekHarness = "deepseek-harness"
|
||||
AppOctop = "octop"
|
||||
AppMysql = "mysql"
|
||||
AppMariaDB = "mariadb"
|
||||
AppPostgresql = "postgresql"
|
||||
|
||||
+1
-5
@@ -77,11 +77,7 @@ func Run() {
|
||||
global.LOG.Errorf("generate random hour failed: %v", err)
|
||||
hourRand = big.NewInt(0)
|
||||
}
|
||||
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 {
|
||||
if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), 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 {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/1Panel-dev/1Panel/agent
|
||||
|
||||
go 1.27.3
|
||||
go 1.26.6
|
||||
|
||||
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
|
||||
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [
|
||||
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Resolve incompatible configuration before continuing: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "Configuration changed during switching. Retry after reloading."
|
||||
ErrFeishuImageRequired: "Use an image containing bundled Feishu."
|
||||
ErrFeishuAlreadySwitched: "Feishu has already switched to the built-in plugin."
|
||||
ErrFeishuSwitchRequired: "Switch to the built-in plugin from the Feishu channel page first."
|
||||
ErrFeishuManaged: "Manage Feishu from its channel page; the plugin is updated with the image."
|
||||
ErrFeishuRestore: "Failed to restore Feishu configuration: {{ .err }}"
|
||||
ErrFeishuActivation: "The built-in Feishu plugin or gateway failed to start."
|
||||
AgentFeishuSwitch: "Switch Feishu plugin"
|
||||
AgentFeishuBackup: "Configuration backup: {{ .name }}"
|
||||
AgentFeishuRestored: "Previous configuration restored."
|
||||
AgentFeishuProbeWarning: "Feishu connection check failed. Check the network and credentials; the switch has been retained."
|
||||
AgentFeishuVerifyMessages: "Local activation completed. Verify pairing, DMs, groups, bot routing and streaming in Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Confirm the Feishu transition options on the upgrade page before retrying."
|
||||
|
||||
ErrFeishuUpgradeBackup: "A full application backup is required for the Feishu compatibility upgrade."
|
||||
|
||||
ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use standard data mounts inside the application directory."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained."
|
||||
|
||||
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"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquin
|
||||
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Corrija la configuración incompatible antes de continuar: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "La configuración cambió durante la transición. Recargue y reintente."
|
||||
ErrFeishuImageRequired: "Use una imagen que incluya Feishu."
|
||||
ErrFeishuAlreadySwitched: "Feishu ya utiliza el complemento integrado."
|
||||
ErrFeishuSwitchRequired: "Cambie primero al complemento integrado desde la página del canal Feishu."
|
||||
ErrFeishuManaged: "Gestione Feishu desde su canal; el complemento se actualiza con la imagen."
|
||||
ErrFeishuRestore: "Error al restaurar la configuración de Feishu: {{ .err }}"
|
||||
ErrFeishuActivation: "No se pudo iniciar el complemento Feishu integrado o la puerta de enlace."
|
||||
AgentFeishuSwitch: "Cambiar el complemento Feishu"
|
||||
AgentFeishuBackup: "Copia de configuración: {{ .name }}"
|
||||
AgentFeishuRestored: "Se restauró la configuración anterior."
|
||||
AgentFeishuProbeWarning: "Falló la comprobación de conexión de Feishu. Revise la red y las credenciales; se conservó el cambio."
|
||||
AgentFeishuVerifyMessages: "Activación local completada. Verifique el emparejamiento, chats, grupos, rutas de bots y transmisión en Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Confirme las opciones de transición de Feishu en la página de actualización."
|
||||
|
||||
ErrFeishuUpgradeBackup: "La actualización de compatibilidad de Feishu requiere una copia completa de la aplicación."
|
||||
|
||||
ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración completas. Use los montajes de datos estándar dentro del directorio de la aplicación."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original."
|
||||
|
||||
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"
|
||||
|
||||
@@ -774,41 +774,3 @@ VMSnapshotRecoverTask: "بازیابی اسنپشات [{{ .snapshot }}] بر
|
||||
VMSnapshotRecoverStep: "بازیابی اسنپشات ماشین مجازی [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "حذف اسنپشات [{{ .snapshot }}] برای ماشین مجازی [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "حذف اسنپشات ماشین مجازی [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "پیش از ادامه تنظیمات ناسازگار را اصلاح کنید: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "تنظیمات هنگام تغییر عوض شد. صفحه را تازه و دوباره تلاش کنید."
|
||||
ErrFeishuImageRequired: "از تصویری با Feishu داخلی استفاده کنید."
|
||||
ErrFeishuAlreadySwitched: "Feishu قبلاً به افزونه داخلی تغییر کرده است."
|
||||
ErrFeishuSwitchRequired: "ابتدا در صفحه کانال Feishu به افزونه داخلی تغییر دهید."
|
||||
ErrFeishuManaged: "Feishu را از صفحه کانال مدیریت کنید؛ افزونه همراه تصویر بهروز میشود."
|
||||
ErrFeishuRestore: "بازیابی تنظیمات Feishu ناموفق بود: {{ .err }}"
|
||||
ErrFeishuActivation: "راهاندازی افزونه داخلی Feishu یا دروازه ناموفق بود."
|
||||
AgentFeishuSwitch: "تغییر افزونه Feishu"
|
||||
AgentFeishuBackup: "پشتیبان تنظیمات: {{ .name }}"
|
||||
AgentFeishuRestored: "تنظیمات قبلی بازیابی شد."
|
||||
AgentFeishuProbeWarning: "بررسی اتصال Feishu ناموفق بود. شبکه و اطلاعات ورود را بررسی کنید؛ تغییر حفظ شد."
|
||||
AgentFeishuVerifyMessages: "فعالسازی محلی انجام شد. جفتسازی، پیام خصوصی و گروهی، مسیریابی ربات و پاسخ جریانی را در Feishu بررسی کنید."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "گزینههای انتقال Feishu را در صفحه ارتقا تأیید کنید."
|
||||
|
||||
ErrFeishuUpgradeBackup: "ارتقای سازگاری Feishu به پشتیبان کامل برنامه نیاز دارد."
|
||||
|
||||
ErrFeishuUpgradeMounts: "این اتصالهای ذخیرهسازی کاملاً قابل پشتیبانگیری و بازیابی نیستند. از مسیرهای استاندارد داده در پوشه برنامه استفاده کنید."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
|
||||
|
||||
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 }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "仮想マシン [{{ .vm }}] のスナップショット
|
||||
VMSnapshotRecoverStep: "仮想マシンのスナップショット [{{ .snapshot }}] を復元"
|
||||
VMSnapshotDeleteTask: "仮想マシン [{{ .vm }}] のスナップショット [{{ .snapshot }}] を削除"
|
||||
VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot }}] を削除"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "続行前に互換性のない設定を修正してください:{{ .detail }}"
|
||||
ErrFeishuConfigChanged: "切り替え中に設定が変更されました。再読み込みして再試行してください。"
|
||||
ErrFeishuImageRequired: "Feishu を内蔵したイメージを使用してください。"
|
||||
ErrFeishuAlreadySwitched: "Feishu は組み込みプラグインに切り替え済みです。"
|
||||
ErrFeishuSwitchRequired: "先に Feishu チャネル画面で組み込みプラグインに切り替えてください。"
|
||||
ErrFeishuManaged: "Feishu はチャネル画面で管理してください。プラグインはイメージとともに更新されます。"
|
||||
ErrFeishuRestore: "Feishu 設定の復元に失敗しました:{{ .err }}"
|
||||
ErrFeishuActivation: "組み込み Feishu プラグインまたはゲートウェイを起動できませんでした。"
|
||||
AgentFeishuSwitch: "Feishu プラグインの切り替え"
|
||||
AgentFeishuBackup: "設定のバックアップ:{{ .name }}"
|
||||
AgentFeishuRestored: "以前の設定を復元しました。"
|
||||
AgentFeishuProbeWarning: "Feishu の接続確認に失敗しました。ネットワークと認証情報を確認してください。切り替え結果は保持しています。"
|
||||
AgentFeishuVerifyMessages: "ローカルの有効化が完了しました。Feishu でペアリング、個別・グループチャット、ボットルーティング、ストリーミングを確認してください。"
|
||||
|
||||
ErrFeishuUpgradeConfirm: "更新画面で Feishu の切り替え設定を確認してください。"
|
||||
|
||||
ErrFeishuUpgradeBackup: "Feishu 互換更新にはアプリ全体のバックアップが必要です。"
|
||||
|
||||
ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・復元に対応しません。アプリディレクトリ内の標準データマウントを使用してください。"
|
||||
|
||||
AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。"
|
||||
|
||||
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 }} コンテナの確認に失敗しました。タスクログを確認してください"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}]
|
||||
VMSnapshotRecoverStep: "가상 머신 스냅샷 [{{ .snapshot }}] 복구"
|
||||
VMSnapshotDeleteTask: "가상 머신 [{{ .vm }}] 스냅샷 [{{ .snapshot }}] 삭제"
|
||||
VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "계속하기 전에 호환되지 않는 설정을 수정하세요: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "전환 중 설정이 변경되었습니다. 새로고침 후 다시 시도하세요."
|
||||
ErrFeishuImageRequired: "Feishu가 내장된 이미지를 사용하세요."
|
||||
ErrFeishuAlreadySwitched: "이미 내장 Feishu 플러그인으로 전환했습니다."
|
||||
ErrFeishuSwitchRequired: "먼저 Feishu 채널 페이지에서 내장 플러그인으로 전환하세요."
|
||||
ErrFeishuManaged: "Feishu 채널 페이지에서 관리하세요. 플러그인은 이미지와 함께 업데이트됩니다."
|
||||
ErrFeishuRestore: "Feishu 설정 복구 실패: {{ .err }}"
|
||||
ErrFeishuActivation: "내장 Feishu 플러그인 또는 게이트웨이 시작에 실패했습니다."
|
||||
AgentFeishuSwitch: "Feishu 플러그인 전환"
|
||||
AgentFeishuBackup: "설정 백업: {{ .name }}"
|
||||
AgentFeishuRestored: "이전 설정을 복구했습니다."
|
||||
AgentFeishuProbeWarning: "Feishu 연결 확인에 실패했습니다. 네트워크와 자격 증명을 확인하세요. 전환 결과는 유지됩니다."
|
||||
AgentFeishuVerifyMessages: "로컬 활성화가 완료되었습니다. Feishu에서 페어링, 개인 및 그룹 채팅, 봇 라우팅과 스트리밍을 확인하세요."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "업그레이드 화면에서 Feishu 전환 옵션을 확인하세요."
|
||||
|
||||
ErrFeishuUpgradeBackup: "Feishu 호환 업그레이드에는 전체 앱 백업이 필요합니다."
|
||||
|
||||
ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원하지 않습니다. 앱 디렉터리 내부의 표준 데이터 마운트를 사용하세요."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다."
|
||||
|
||||
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 }}개 확인에 실패했습니다. 작업 로그를 확인하세요"
|
||||
|
||||
@@ -764,41 +764,3 @@ VMSnapshotRecoverTask: "ກູ້ຄືນສະແນັບຊັອດ [{{ .sn
|
||||
VMSnapshotRecoverStep: "ກູ້ຄືນສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "ລຶບສະແນັບຊັອດ [{{ .snapshot }}] ຂອງເຄື່ອງສະເໝືອນ [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງສະເໝືອນ [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "ກະລຸນາແກ້ໄຂການຕັ້ງຄ່າທີ່ບໍ່ເຂົ້າກັນກ່ອນ: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "ການຕັ້ງຄ່າປ່ຽນໃນລະຫວ່າງການສະຫຼັບ. ໂຫຼດໃໝ່ແລ້ວລອງອີກ."
|
||||
ErrFeishuImageRequired: "ໃຊ້ອິມເມຈທີ່ມີ Feishu ໃນຕົວ."
|
||||
ErrFeishuAlreadySwitched: "Feishu ໄດ້ສະຫຼັບເປັນປລັກອິນໃນຕົວແລ້ວ."
|
||||
ErrFeishuSwitchRequired: "ກະລຸນາສະຫຼັບໃນໜ້າຊ່ອງ Feishu ກ່ອນ."
|
||||
ErrFeishuManaged: "ຈັດການ Feishu ໃນໜ້າຊ່ອງ; ປລັກອິນອັບເດດພ້ອມອິມເມຈ."
|
||||
ErrFeishuRestore: "ກູ້ຄືນການຕັ້ງຄ່າ Feishu ບໍ່ສຳເລັດ: {{ .err }}"
|
||||
ErrFeishuActivation: "ປລັກອິນ Feishu ໃນຕົວ ຫຼື ເກດເວເລີ່ມບໍ່ສຳເລັດ."
|
||||
AgentFeishuSwitch: "ສະຫຼັບປລັກອິນ Feishu"
|
||||
AgentFeishuBackup: "ສຳຮອງການຕັ້ງຄ່າ: {{ .name }}"
|
||||
AgentFeishuRestored: "ກູ້ຄືນການຕັ້ງຄ່າເດີມແລ້ວ."
|
||||
AgentFeishuProbeWarning: "ກວດການເຊື່ອມຕໍ່ Feishu ບໍ່ຜ່ານ. ກວດເຄືອຂ່າຍ ແລະ ຂໍ້ມູນເຂົ້າໃຊ້; ຮັກສາຜົນການສະຫຼັບໄວ້."
|
||||
AgentFeishuVerifyMessages: "ເປີດໃຊ້ພາຍໃນສຳເລັດ. ກວດການຈັບຄູ່, ແຊັດສ່ວນຕົວ, ກຸ່ມ, ເສັ້ນທາງບອດ ແລະ ສະຕຣີມໃນ Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "ຢືນຢັນຕົວເລືອກການປ່ຽນ Feishu ໃນໜ້າອັບເກຣດ."
|
||||
|
||||
ErrFeishuUpgradeBackup: "ການອັບເກຣດ Feishu ຕ້ອງສຳຮອງແອັບທັງໝົດ."
|
||||
|
||||
ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້ບໍ່ສາມາດສຳຮອງແລະກູ້ຄືນທັງໝົດໄດ້. ໃຊ້ຈຸດເຊື່ອມມາດຕະຖານພາຍໃນໂຟນເດີແອັບ."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
|
||||
|
||||
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 }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Pulihkan syot kilat [{{ .snapshot }}] untuk mesin maya [
|
||||
VMSnapshotRecoverStep: "Pulihkan syot kilat mesin maya [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Padam syot kilat [{{ .snapshot }}] untuk mesin maya [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Padam syot kilat mesin maya [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Betulkan konfigurasi tidak serasi sebelum meneruskan: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "Konfigurasi berubah semasa pertukaran. Muat semula dan cuba lagi."
|
||||
ErrFeishuImageRequired: "Gunakan imej yang mengandungi Feishu terbina dalam."
|
||||
ErrFeishuAlreadySwitched: "Feishu sudah menggunakan pemalam terbina dalam."
|
||||
ErrFeishuSwitchRequired: "Tukar dahulu kepada pemalam terbina dalam pada halaman saluran Feishu."
|
||||
ErrFeishuManaged: "Urus Feishu pada halaman salurannya; pemalam dikemas kini bersama imej."
|
||||
ErrFeishuRestore: "Gagal memulihkan konfigurasi Feishu: {{ .err }}"
|
||||
ErrFeishuActivation: "Pemalam Feishu terbina dalam atau gerbang gagal dimulakan."
|
||||
AgentFeishuSwitch: "Tukar pemalam Feishu"
|
||||
AgentFeishuBackup: "Sandaran konfigurasi: {{ .name }}"
|
||||
AgentFeishuRestored: "Konfigurasi sebelumnya dipulihkan."
|
||||
AgentFeishuProbeWarning: "Semakan sambungan Feishu gagal. Semak rangkaian dan kelayakan; pertukaran dikekalkan."
|
||||
AgentFeishuVerifyMessages: "Pengaktifan setempat selesai. Sahkan pasangan, mesej peribadi, kumpulan, penghalaan bot dan penstriman dalam Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Sahkan pilihan peralihan Feishu pada halaman naik taraf."
|
||||
|
||||
ErrFeishuUpgradeBackup: "Naik taraf keserasian Feishu memerlukan sandaran aplikasi penuh."
|
||||
|
||||
ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuhnya. Gunakan lekapan data standard dalam direktori aplikasi."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan."
|
||||
|
||||
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"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Restaurar snapshot [{{ .snapshot }}] da máquina virtual
|
||||
VMSnapshotRecoverStep: "Restaurar snapshot da máquina virtual [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Excluir snapshot [{{ .snapshot }}] da máquina virtual [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Excluir snapshot da máquina virtual [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Corrija a configuração incompatível antes de continuar: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "A configuração mudou durante a troca. Recarregue e tente novamente."
|
||||
ErrFeishuImageRequired: "Use uma imagem com Feishu integrado."
|
||||
ErrFeishuAlreadySwitched: "O Feishu já usa o plugin integrado."
|
||||
ErrFeishuSwitchRequired: "Primeiro troque para o plugin integrado na página do canal Feishu."
|
||||
ErrFeishuManaged: "Gerencie o Feishu pela página do canal; o plugin é atualizado com a imagem."
|
||||
ErrFeishuRestore: "Falha ao restaurar a configuração do Feishu: {{ .err }}"
|
||||
ErrFeishuActivation: "Falha ao iniciar o plugin Feishu integrado ou o gateway."
|
||||
AgentFeishuSwitch: "Trocar plugin Feishu"
|
||||
AgentFeishuBackup: "Backup da configuração: {{ .name }}"
|
||||
AgentFeishuRestored: "Configuração anterior restaurada."
|
||||
AgentFeishuProbeWarning: "Falha na verificação de conexão do Feishu. Verifique a rede e as credenciais; a troca foi mantida."
|
||||
AgentFeishuVerifyMessages: "Ativação local concluída. Verifique pareamento, conversas, grupos, rotas de bots e streaming no Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Confirme as opções de transição do Feishu na página de atualização."
|
||||
|
||||
ErrFeishuUpgradeBackup: "A atualização de compatibilidade do Feishu exige backup completo do aplicativo."
|
||||
|
||||
ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração completos. Use montagens de dados padrão dentro do diretório do aplicativo."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido."
|
||||
|
||||
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"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Восстановить снимок [{{ .snapshot }}]
|
||||
VMSnapshotRecoverStep: "Восстановить снимок виртуальной машины [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Удалить снимок [{{ .snapshot }}] виртуальной машины [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Удалить снимок виртуальной машины [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Исправьте несовместимые настройки перед продолжением: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "Настройки изменились во время переключения. Обновите страницу и повторите."
|
||||
ErrFeishuImageRequired: "Используйте образ со встроенным Feishu."
|
||||
ErrFeishuAlreadySwitched: "Feishu уже переключён на встроенный плагин."
|
||||
ErrFeishuSwitchRequired: "Сначала перейдите на встроенный плагин на странице канала Feishu."
|
||||
ErrFeishuManaged: "Управляйте Feishu на странице канала; плагин обновляется с образом."
|
||||
ErrFeishuRestore: "Не удалось восстановить настройки Feishu: {{ .err }}"
|
||||
ErrFeishuActivation: "Не удалось запустить встроенный плагин Feishu или шлюз."
|
||||
AgentFeishuSwitch: "Переключить плагин Feishu"
|
||||
AgentFeishuBackup: "Резервная копия настроек: {{ .name }}"
|
||||
AgentFeishuRestored: "Прежние настройки восстановлены."
|
||||
AgentFeishuProbeWarning: "Проверка подключения Feishu не пройдена. Проверьте сеть и учётные данные; переключение сохранено."
|
||||
AgentFeishuVerifyMessages: "Локальная активация завершена. Проверьте сопряжение, личные и групповые сообщения, маршрутизацию ботов и потоковые ответы в Feishu."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Подтвердите параметры перехода Feishu на странице обновления."
|
||||
|
||||
ErrFeishuUpgradeBackup: "Для обновления совместимости Feishu нужна полная резервная копия приложения."
|
||||
|
||||
ErrFeishuUpgradeMounts: "Эти монтирования не поддерживают полное резервное копирование и восстановление. Используйте стандартные данные внутри каталога приложения."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена."
|
||||
|
||||
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 }}. Подробности в журнале задачи"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snaps
|
||||
VMSnapshotRecoverStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] geri yükle"
|
||||
VMSnapshotDeleteTask: "Sanal makine [{{ .vm }}] anlık görüntüsü [{{ .snapshot }}] sil"
|
||||
VMSnapshotDeleteStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] sil"
|
||||
|
||||
ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm."
|
||||
|
||||
ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm."
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "Devam etmeden önce uyumsuz ayarları düzeltin: {{ .detail }}"
|
||||
ErrFeishuConfigChanged: "Geçiş sırasında ayarlar değişti. Yenileyip tekrar deneyin."
|
||||
ErrFeishuImageRequired: "Yerleşik Feishu içeren bir imaj kullanın."
|
||||
ErrFeishuAlreadySwitched: "Feishu zaten yerleşik eklentiye geçti."
|
||||
ErrFeishuSwitchRequired: "Önce Feishu kanal sayfasından yerleşik eklentiye geçin."
|
||||
ErrFeishuManaged: "Feishu kanal sayfasından yönetin; eklenti imajla güncellenir."
|
||||
ErrFeishuRestore: "Feishu ayarları geri yüklenemedi: {{ .err }}"
|
||||
ErrFeishuActivation: "Yerleşik Feishu eklentisi veya ağ geçidi başlatılamadı."
|
||||
AgentFeishuSwitch: "Feishu eklentisini değiştir"
|
||||
AgentFeishuBackup: "Ayar yedeği: {{ .name }}"
|
||||
AgentFeishuRestored: "Önceki ayarlar geri yüklendi."
|
||||
AgentFeishuProbeWarning: "Feishu bağlantı kontrolü başarısız. Ağı ve kimlik bilgilerini kontrol edin; geçiş korundu."
|
||||
AgentFeishuVerifyMessages: "Yerel etkinleştirme tamamlandı. Feishu üzerinden eşleştirmeyi, özel ve grup mesajlarını, bot yönlendirmesini ve akışı doğrulayın."
|
||||
|
||||
ErrFeishuUpgradeConfirm: "Yükseltme sayfasında Feishu geçiş seçeneklerini onaylayın."
|
||||
|
||||
ErrFeishuUpgradeBackup: "Feishu uyumluluk yükseltmesi tam uygulama yedeği gerektirir."
|
||||
|
||||
ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygulama dizinindeki standart veri bağlamalarını kullanın."
|
||||
|
||||
AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu."
|
||||
|
||||
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"
|
||||
|
||||
@@ -773,41 +773,3 @@ VMSnapshotRecoverTask: "復原虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
|
||||
VMSnapshotRecoverStep: "復原虛擬機快照 [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "刪除虛擬機 [{{ .vm }}] 快照 [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]"
|
||||
|
||||
ErrCAUpload: "請上傳包含有效自簽根憑證 ca.crt 及匹配私鑰 ca.key 的 ZIP 檔案(最大 10 MB),金鑰演算法須為支援的 RSA 或 EC。"
|
||||
|
||||
ErrCAImport: "請提供有效的自簽根憑證及匹配私鑰(每個最大 1 MB),金鑰演算法須為支援的 RSA 或 EC。"
|
||||
|
||||
# Built-in Feishu
|
||||
ErrFeishuConfig: "請先處理不相容的設定:{{ .detail }}"
|
||||
ErrFeishuConfigChanged: "切換期間設定已變更,請重新整理後重試。"
|
||||
ErrFeishuImageRequired: "請使用包含內建飛書外掛的映像。"
|
||||
ErrFeishuAlreadySwitched: "飛書已切換至內建外掛。"
|
||||
ErrFeishuSwitchRequired: "請先在飛書管道頁面切換至內建外掛。"
|
||||
ErrFeishuManaged: "請在飛書管道頁面管理,外掛隨映像更新。"
|
||||
ErrFeishuRestore: "還原飛書設定失敗:{{ .err }}"
|
||||
ErrFeishuActivation: "內建飛書外掛或閘道啟動失敗。"
|
||||
AgentFeishuSwitch: "切換飛書外掛"
|
||||
AgentFeishuBackup: "設定備份:{{ .name }}"
|
||||
AgentFeishuRestored: "已還原切換前的設定。"
|
||||
AgentFeishuProbeWarning: "飛書連線檢查未通過,請檢查網路和憑證;已保留切換結果。"
|
||||
AgentFeishuVerifyMessages: "本機啟用完成,請在飛書驗證配對、私聊、群聊、機器人路由和串流回覆。"
|
||||
|
||||
ErrFeishuUpgradeConfirm: "請在升級頁面確認飛書切換選項後重試。"
|
||||
|
||||
ErrFeishuUpgradeBackup: "飛書相容升級必須開啟完整應用程式備份。"
|
||||
|
||||
ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應用程式目錄內的標準資料掛載。"
|
||||
|
||||
AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。"
|
||||
|
||||
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 }} 個容器檢查失敗,請查看任務日誌"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user