Compare commits

..
Author SHA1 Message Date
wanghe-fit2cloud 3a888d289f fix app image pull check 2026-06-12 14:10:20 +08:00
533 changed files with 69932 additions and 72260 deletions
+25
View File
@@ -0,0 +1,25 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2.5-coder-3b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
+21
View File
@@ -0,0 +1,21 @@
name: SonarCloud Scan
on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
if: github.repository == '1Panel-dev/1Panel'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Mirror the Github organization repos to Gitee. - name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master uses: Yikun/hub-mirror-action@master
with: with:
src: 'github/1Panel-dev' src: 'github/1Panel-dev'
dst: 'gitee/fit2cloud-feizhiyun' dst: 'gitee/fit2cloud-feizhiyun'
+11
View File
@@ -0,0 +1,11 @@
name: Typos Check
on: pull_request
jobs:
run:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
- name: Check spelling
uses: crate-ci/typos@master
+1 -3
View File
@@ -25,7 +25,7 @@
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a> <a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a> <a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a> <a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
<a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a> <a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a> <a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a> <a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a> <a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
@@ -33,8 +33,6 @@
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a> <a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a> <a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a> <a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
<a href="/docs/README.lo.md"><img alt="ພາສາລາວ" src="https://img.shields.io/badge/%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BA%A5%E0%BA%B2%E0%BA%A7-d9d9d9"></a>
</p> </p>
--- ---
-103
View File
@@ -500,27 +500,6 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags AI
// @Summary Discover custom provider models
// @Accept json
// @Param request body dto.AgentAccountModelDiscoverReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/accounts/models/discover [post]
func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelDiscoverReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.DiscoverAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI // @Tags AI
// @Summary Create model account model // @Summary Create model account model
// @Accept json // @Accept json
@@ -1378,88 +1357,6 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags AI
// @Summary List OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginsReq true "request"
// @Success 200 {array} dto.AgentPluginItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/list [post]
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
var req dto.AgentPluginsReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.ListPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Search OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginSearchReq true "request"
// @Success 200 {array} dto.AgentPluginSearchItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/search [post]
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
var req dto.AgentPluginSearchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.SearchPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Install an OpenClaw marketplace plugin
// @Accept json
// @Param request body dto.AgentPluginMarketInstallReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/install [post]
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
var req dto.AgentPluginMarketInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.InstallMarketPlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Operate an OpenClaw plugin
// @Accept json
// @Param request body dto.AgentPluginOperateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/operate [post]
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
var req dto.AgentPluginOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.OperatePlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI // @Tags AI
// @Summary Login Agent Weixin channel // @Summary Login Agent Weixin channel
// @Accept json // @Accept json
+1 -6
View File
@@ -413,15 +413,10 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type { switch req.Type {
case "app": case "app":
record, err := backupService.AppBackup(req) if _, err := backupService.AppBackup(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster: case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil { if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
-20
View File
@@ -881,26 +881,6 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Container Compose
// @Summary Pin compose
// @Accept json
// @Param request body dto.ComposePin true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/compose/pin [post]
func (b *BaseApi) ComposePin(c *gin.Context) {
var req dto.ComposePin
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ComposePin(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Container Compose // @Tags Container Compose
// @Summary Load compose environment variables // @Summary Load compose environment variables
// @Accept json // @Accept json
+12 -215
View File
@@ -41,40 +41,20 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary List mysql users // @Summary Bind user of mysql database
// @Accept json // @Accept json
// @Param request body dto.MysqlUserSearch true "request" // @Param request body dto.BindUser true "request"
// @Success 200 {array} dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/search [post]
func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListUsers(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Create mysql user
// @Accept json
// @Param request body dto.MysqlUserCreate true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/users [post] // @Router /databases/bind [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"create mysql database [database] user [username]@[host]"} // @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
func (b *BaseApi) CreateMysqlUser(c *gin.Context) { func (b *BaseApi) BindUser(c *gin.Context) {
var req dto.MysqlUserCreate var req dto.BindUser
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if len(req.Password) != 0 { if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password) password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil { if err != nil {
@@ -83,191 +63,8 @@ func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
} }
req.Password = string(password) req.Password = string(password)
} }
if err := mysqlService.CreateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql if err := mysqlService.BindUser(req); err != nil {
// @Summary Delete mysql user
// @Accept json
// @Param request body dto.MysqlUserDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/del [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"delete mysql database [database] user [username]@[host]"}
func (b *BaseApi) DeleteMysqlUser(c *gin.Context) {
var req dto.MysqlUserDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.DeleteUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Update mysql user
// @Accept json
// @Param request body dto.MysqlUserUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/update [post]
// @x-panel-log {"bodyKeys":["database","username","host","newHost","description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]","formatEN":"update mysql database [database] user [username] access [host] => [newHost] description [description]"}
func (b *BaseApi) UpdateMysqlUser(c *gin.Context) {
var req dto.MysqlUserUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.UpdateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Change mysql user password
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"update mysql database [database] user [username]@[host] password"}
func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.ChangeUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Save mysql user password locally
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password/save [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.SaveUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary List mysql grants
// @Accept json
// @Param request body dto.MysqlUserSearch true "request"
// @Success 200 {array} dto.MysqlGrant
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/search [post]
func (b *BaseApi) ListMysqlGrants(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrants(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary List mysql grant summary
// @Accept json
// @Param request body dto.MysqlGrantSummarySearch true "request"
// @Success 200 {object} map[string][]dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/summary [post]
func (b *BaseApi) ListMysqlGrantSummary(c *gin.Context) {
var req dto.MysqlGrantSummarySearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrantSummary(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Grant mysql user
// @Accept json
// @Param request body dto.MysqlGrantCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]","formatEN":"grant mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) GrantMysqlUser(c *gin.Context) {
var req dto.MysqlGrantCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.GrantUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Revoke mysql grant
// @Accept json
// @Param request body dto.MysqlGrantDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/del [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权","formatEN":"revoke mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) RevokeMysqlGrant(c *gin.Context) {
var req dto.MysqlGrantDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.RevokeGrant(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -297,14 +94,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql root password // @Summary Change mysql password
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/password [post] // @Router /databases/change/password [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"}
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) { func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -328,14 +125,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql root access // @Summary Change mysql access
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/access [post] // @Router /databases/change/access [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"}
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) { func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+8 -11
View File
@@ -37,16 +37,14 @@ var (
cronjobService = service.NewICronjobService() cronjobService = service.NewICronjobService()
fileService = service.NewIFileService() fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService() fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService() fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService() sshService = service.NewISSHService()
firewallService = service.NewIFirewallService() firewallService = service.NewIFirewallService()
forwardingService = service.NewIForwardingService() iptablesService = service.NewIIptablesService()
iptablesService = service.NewIIptablesService() monitorService = service.NewIMonitorService()
monitorService = service.NewIMonitorService() systemService = service.NewISystemService()
systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
deviceService = service.NewIDeviceService() deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService() fail2banService = service.NewIFail2BanService()
@@ -61,7 +59,6 @@ var (
websiteDnsAccountService = service.NewIWebsiteDnsAccountService() websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
websiteSSLService = service.NewIWebsiteSSLService() websiteSSLService = service.NewIWebsiteSSLService()
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService() websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
websiteTemplateService = service.NewIWebsiteTemplateService()
nginxService = service.NewINginxService() nginxService = service.NewINginxService()
+2 -69
View File
@@ -12,9 +12,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,11 +28,6 @@ import (
qrcode "github.com/skip2/go-qrcode" qrcode "github.com/skip2/go-qrcode"
) )
var cancelledChunkUploads = struct {
sync.RWMutex
ids map[string]struct{}
}{ids: make(map[string]struct{})}
// @Tags File // @Tags File
// @Summary List files // @Summary List files
// @Accept json // @Accept json
@@ -880,24 +873,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
} }
} }
filename := c.PostForm("filename") filename := c.PostForm("filename")
if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) { fileDir := filepath.Join(tmpDir, filename)
helper.BadRequest(c, errors.New("invalid filename"))
return
}
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
cancellable := uploadID != ""
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
if !cancellable {
uploadID = filename
}
fileDir := filepath.Join(tmpDir, uploadID)
if cancellable && chunkUploadCancelled(uploadID) {
helper.BadRequest(c, errors.New("upload cancelled"))
return
}
if chunkIndex == 0 { if chunkIndex == 0 {
if fileOp.Stat(fileDir) { if fileOp.Stat(fileDir) {
_ = fileOp.DeleteDir(fileDir) _ = fileOp.DeleteDir(fileDir)
@@ -908,7 +884,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
defer func() { defer func() {
if err != nil { if err != nil {
_ = os.RemoveAll(fileDir) _ = os.Remove(fileDir)
} }
}() }()
var ( var (
@@ -928,11 +904,6 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if cancellable && chunkUploadCancelled(uploadID) {
err = errors.New("upload cancelled")
helper.BadRequest(c, err)
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm) err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
@@ -951,50 +922,12 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if cancellable {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
}
helper.SuccessWithData(c, true) helper.SuccessWithData(c, true)
} else { } else {
return return
} }
} }
// StopChunkUpload removes temporary chunks left by a cancelled upload.
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
uploadID := strings.TrimSpace(req.Key)
if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
cancelledChunkUploads.Lock()
cancelledChunkUploads.ids[uploadID] = struct{}{}
cancelledChunkUploads.Unlock()
time.AfterFunc(10*time.Minute, func() {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
})
if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func chunkUploadCancelled(uploadID string) bool {
cancelledChunkUploads.RLock()
_, ok := cancelledChunkUploads.ids[uploadID]
cancelledChunkUploads.RUnlock()
return ok
}
var wsUpgrade = websocket.Upgrader{ var wsUpgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
+4 -32
View File
@@ -20,15 +20,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
return return
} }
var ( data, err := firewallService.LoadBaseInfo(req.Name)
data dto.FirewallBaseInfo
err error
)
if req.Name == "forward" {
data, err = forwardingService.LoadBaseInfo()
} else {
data, err = firewallService.LoadBaseInfo(req.Name)
}
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -51,21 +43,7 @@ func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
return return
} }
var ( total, list, err := firewallService.SearchWithPage(req)
total int64
list interface{}
err error
)
if req.Type == "forward" {
total, list, err = forwardingService.SearchWithPage(dto.ForwardRuleSearch{
PageInfo: req.PageInfo,
Info: req.Info,
Status: req.Status,
Strategy: req.Strategy,
})
} else {
total, list, err = firewallService.SearchWithPage(req)
}
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -138,7 +116,7 @@ func (b *BaseApi) OperateForwardRule(c *gin.Context) {
return return
} }
if err := forwardingService.Operate(req); err != nil { if err := firewallService.OperateForwardRule(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -335,13 +313,7 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
var err error if err := iptablesService.Operate(req); err != nil {
if req.Operate == "init-forward" {
err = forwardingService.Enable()
} else {
err = iptablesService.Operate(req)
}
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
-55
View File
@@ -2,7 +2,6 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -21,57 +20,3 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Logs
// @Summary Get host system log status
// @Produce json
// @Success 200 {object} dto.SystemLogStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/status [get]
func (b *BaseApi) GetSystemLogStatus(c *gin.Context) {
data, err := logService.GetSystemLogStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Read host logs
// @Accept json
// @Param request body dto.SystemLogReq true "request"
// @Produce json
// @Success 200 {object} dto.SystemLogRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/read [post]
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
var req dto.SystemLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := logService.ReadSystemLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary List running host services
// @Produce json
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/services [get]
func (b *BaseApi) ListRunningServices(c *gin.Context) {
data, err := logService.ListRunningServices()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
-63
View File
@@ -23,27 +23,6 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags McpServer
// @Summary Load mcp server detail
// @Accept json
// @Param request body request.McpServerDetail true "request"
// @Success 200 {object} response.McpServerDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/detail [post]
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
var req request.McpServerDetail
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.Detail(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Create mcp server // @Summary Create mcp server
// @Accept json // @Accept json
@@ -128,48 +107,6 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags McpServer
// @Summary Sync mcp server status
// @Accept json
// @Param request body request.McpServerStatusSync true "request"
// @Success 200 {array} response.McpServerStatusDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/status/sync [post]
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
var req request.McpServerStatusSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.SyncStatus(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Test mcp server connection
// @Accept json
// @Param request body request.McpServerConnectionTest true "request"
// @Success 200 {object} response.McpServerConnectionTestRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/connection/test [post]
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
var req request.McpServerConnectionTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.TestConnection(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Bind Domain for mcp server // @Summary Bind Domain for mcp server
// @Accept json // @Accept json
-63
View File
@@ -1,63 +0,0 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithDataGzipped(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
-15
View File
@@ -224,21 +224,6 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
helper.SuccessWithData(c, tmpFile) helper.SuccessWithData(c, tmpFile)
} }
// @Tags SSH
// @Summary Clean host SSH logs
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/log/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
if err := sshService.CleanLog(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags SSH // @Tags SSH
// @Summary Load host SSH conf // @Summary Load host SSH conf
// @Accept json // @Accept json
-25
View File
@@ -210,27 +210,6 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL // @Tags Website SSL
// @Summary Upload ssl // @Summary Upload ssl
// @Accept json // @Accept json
@@ -269,8 +248,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description") req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID") sslID := c.PostForm("sslID")
if sslID != "" { if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64) req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -306,8 +283,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent), Certificate: string(certificateContent),
Description: req.Description, Description: req.Description,
SSLID: uint(req.SSLID), SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
} }
if err := websiteSSLService.Upload(uploadReq); err != nil { if err := websiteSSLService.Upload(uploadReq); err != nil {
-259
View File
@@ -1,259 +0,0 @@
package v2
import (
"io"
"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/gin-gonic/gin"
)
// @Tags Website Template
// @Summary Page website templates
// @Accept json
// @Param request body request.WebsiteTemplateSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/search [post]
func (b *BaseApi) PageWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, templates, err := websiteTemplateService.PageTemplate(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: templates,
})
}
// @Tags Website Template
// @Summary Create website template
// @Accept json
// @Param request body request.WebsiteTemplateCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站模板 [name]","formatEN":"Create website template [name]"}
func (b *BaseApi) CreateWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.CreateTemplate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Update website template
// @Accept json
// @Param request body request.WebsiteTemplateUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/update [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站模板 [name]","formatEN":"Update website template [name]"}
func (b *BaseApi) UpdateWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.UpdateTemplate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Delete website template
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_templates","output_column":"name","output_value":"name"}],"formatZH":"删除网站模板 [name]","formatEN":"Delete website template [name]"}
func (b *BaseApi) DeleteWebsiteTemplate(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.DeleteTemplate(req.ID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Get website template
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} response.WebsiteTemplateDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/get [post]
func (b *BaseApi) GetWebsiteTemplate(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
template, err := websiteTemplateService.GetTemplate(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, template)
}
// @Tags Website Template
// @Summary Upload website template zip
// @Accept multipart/form-data
// @Param file formData file true "file"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/upload [post]
func (b *BaseApi) UploadTemplateZip(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
helper.BadRequest(c, err)
return
}
file, err := fileHeader.Open()
if err != nil {
helper.InternalServer(c, err)
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
helper.InternalServer(c, err)
return
}
filePath, variables, err := websiteTemplateService.SaveUploadZip(fileHeader.Filename, content)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, gin.H{"filePath": filePath, "variables": variables})
}
// @Tags Website Template
// @Summary Preview website template
// @Accept json
// @Param request body request.WebsitePreviewReq true "request"
// @Success 200 {object} response.WebsitePreviewDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/preview [post]
func (b *BaseApi) PreviewWebsiteTemplate(c *gin.Context) {
var req request.WebsitePreviewReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
preview, err := websiteTemplateService.Preview(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, preview)
}
// @Tags Website Template
// @Summary Page website template outputs
// @Accept json
// @Param request body request.WebsiteTemplateOutputSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/search [post]
func (b *BaseApi) PageWebsiteTemplateOutput(c *gin.Context) {
var req request.WebsiteTemplateOutputSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, outputs, err := websiteTemplateService.PageOutput(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: outputs,
})
}
// @Tags Website Template
// @Summary Create website template output
// @Accept json
// @Param request body request.WebsiteTemplateOutputCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成模板产物 [name]","formatEN":"Generate template output [name]"}
func (b *BaseApi) CreateWebsiteTemplateOutput(c *gin.Context) {
var req request.WebsiteTemplateOutputCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.CreateOutput(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Delete website template output
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_template_outputs","output_column":"name","output_value":"name"}],"formatZH":"删除模板产物 [name]","formatEN":"Delete template output [name]"}
func (b *BaseApi) DeleteWebsiteTemplateOutput(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.DeleteOutput(req.ID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Get website template output
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} response.WebsiteTemplateOutputDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/get [post]
func (b *BaseApi) GetWebsiteTemplateOutput(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
output, err := websiteTemplateService.GetOutput(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, output)
}
+70 -143
View File
@@ -3,30 +3,28 @@ package dto
import "time" import "time"
type AgentCreateReq struct { type AgentCreateReq struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Remark string `json:"remark"` Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"` AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"` BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"` AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"` TaskID string `json:"taskID"`
DashboardPassword string `json:"dashboardPassword"` Advanced bool `json:"advanced"`
TaskID string `json:"taskID"` ContainerName string `json:"containerName"`
Advanced bool `json:"advanced"` AllowPort bool `json:"allowPort"`
ContainerName string `json:"containerName"` SpecifyIP string `json:"specifyIP"`
AllowPort bool `json:"allowPort"` RestartPolicy string `json:"restartPolicy"`
SpecifyIP string `json:"specifyIP"` CpuQuota float64 `json:"cpuQuota"`
RestartPolicy string `json:"restartPolicy"` MemoryLimit float64 `json:"memoryLimit"`
CpuQuota float64 `json:"cpuQuota"` MemoryUnit string `json:"memoryUnit"`
MemoryLimit float64 `json:"memoryLimit"` PullImage bool `json:"pullImage"`
MemoryUnit string `json:"memoryUnit"` EditCompose bool `json:"editCompose"`
PullImage bool `json:"pullImage"` DockerCompose string `json:"dockerCompose"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
} }
type AgentBatchInstallReq struct { type AgentBatchInstallReq struct {
@@ -40,8 +38,6 @@ type AgentBatchInstallReq struct {
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Advanced bool `json:"advanced"` Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"` ContainerName string `json:"containerName"`
@@ -118,11 +114,11 @@ type AgentItem struct {
ProviderName string `json:"providerName"` ProviderName string `json:"providerName"`
Model string `json:"model"` Model string `json:"model"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
AppInstallID uint `json:"appInstallId"` AppInstallID uint `json:"appInstallId"`
@@ -290,22 +286,19 @@ type AgentOverviewSnapshot struct {
} }
type AgentAccountModel struct { type AgentAccountModel struct {
RecordID uint `json:"recordId"` RecordID uint `json:"recordId"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
} }
type AgentAccountModelReq struct { type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
} }
type AgentAccountModelDiscoverReq struct {
Provider string `json:"provider" validate:"required"`
BaseURL string `json:"baseURL" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
APIType string `json:"apiType" validate:"required"`
}
type AgentAccountModelCreateReq struct { type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"` Model AgentAccountModel `json:"model" validate:"required"`
@@ -322,40 +315,31 @@ type AgentAccountModelDeleteReq struct {
} }
type AgentAccountCreateReq struct { type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
Models []AgentAccountModel `json:"models"` Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType" validate:"required"` APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"` Remark string `json:"remark"`
VerifyModel string `json:"verifyModel"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
} }
type AgentAccountUpdateReq struct { type AgentAccountUpdateReq struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"` APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"` Remark string `json:"remark"`
VerifyModel string `json:"verifyModel"` SyncAgents bool `json:"syncAgents"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
} }
type AgentAccountVerifyReq struct { type AgentAccountVerifyReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
Model string `json:"model"`
} }
type AgentAccountDeleteReq struct { type AgentAccountDeleteReq struct {
@@ -365,8 +349,6 @@ type AgentAccountDeleteReq struct {
type AgentAccountSearch struct { type AgentAccountSearch struct {
PageInfo PageInfo
Provider string `json:"provider"` Provider string `json:"provider"`
APIType string `json:"apiType"`
TextOnly bool `json:"textOnly"`
Name string `json:"name"` Name string `json:"name"`
} }
@@ -385,36 +367,26 @@ type AgentAccountInfo struct {
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"` Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
} }
type ProviderModelInfo struct { type ProviderModelInfo struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
type ProviderAPIInfo struct { Reasoning bool `json:"reasoning"`
APIType string `json:"apiType"` Input []string `json:"input"`
BaseURL string `json:"baseUrl"`
EditableBaseURL bool `json:"editableBaseUrl"`
SupportsModelDiscovery bool `json:"supportsModelDiscovery"`
DefaultAuthMode string `json:"defaultAuthMode"`
AuthModes []string `json:"authModes"`
Models []ProviderModelInfo `json:"models"`
} }
type ProviderInfo struct { type ProviderInfo struct {
Sort uint `json:"-"` Sort uint `json:"-"`
Provider string `json:"provider"` Provider string `json:"provider"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
DefaultAPIType string `json:"defaultApiType"` Models []ProviderModelInfo `json:"models"`
APITypes []ProviderAPIInfo `json:"apiTypes"`
Models []ProviderModelInfo `json:"models"`
} }
type AgentFeishuConfigReq struct { type AgentFeishuConfigReq struct {
@@ -435,6 +407,11 @@ type AgentFeishuConfigUpdateReq struct {
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
} }
type AgentFeishuPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentFeishuConfig struct { type AgentFeishuConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"` ThreadSession bool `json:"threadSession"`
@@ -604,52 +581,6 @@ type AgentPluginStatus struct {
Upgradable bool `json:"upgradable"` Upgradable bool `json:"upgradable"`
} }
type AgentPluginsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentPluginSearchReq struct {
AgentID uint `json:"agentId" validate:"required"`
Keyword string `json:"keyword" validate:"required,max=100"`
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
}
type AgentPluginMarketInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Package string `json:"package" validate:"required,max=200"`
Version string `json:"version" validate:"required,max=100"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginOperateReq struct {
AgentID uint `json:"agentId" validate:"required"`
PluginID string `json:"pluginId" validate:"required,max=200"`
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginItem struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type AgentPluginSearchItem struct {
Package string `json:"package"`
PluginID string `json:"pluginId"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Channel string `json:"channel"`
VerificationTier string `json:"verificationTier"`
Categories []string `json:"categories"`
Official bool `json:"official"`
Downloads int64 `json:"downloads"`
Score float64 `json:"score"`
}
type AgentDiscordConfigUpdateReq struct { type AgentDiscordConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@@ -729,20 +660,16 @@ type AgentSecurityConfig struct {
} }
type AgentOtherConfigUpdateReq struct { type AgentOtherConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry" validate:"required"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentOtherConfig struct { type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentConfigFileReq struct { type AgentConfigFileReq struct {
+2 -4
View File
@@ -113,10 +113,8 @@ type DiskDTO struct {
type AlertLogSearch struct { type AlertLogSearch struct {
PageInfo PageInfo
Count uint `json:"count"` Count uint `json:"count"`
Status string `json:"status"` Status string `json:"status"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
} }
type AlertLogDTO struct { type AlertLogDTO struct {
-3
View File
@@ -2,7 +2,6 @@ package dto
import ( import (
"context" "context"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/app/task"
) )
@@ -133,8 +132,6 @@ type Locale struct {
Ko string `json:"ko"` Ko string `json:"ko"`
Tr string `json:"tr"` Tr string `json:"tr"`
Es string `json:"es-es" yaml:"es-es"` Es string `json:"es-es" yaml:"es-es"`
Fa string `json:"fa"`
Lo string `json:"lo"`
} }
type AppForm struct { type AppForm struct {
+8 -9
View File
@@ -65,15 +65,14 @@ type UploadForRecover struct {
} }
type CommonBackup struct { type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"` Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"` Name string `json:"name"`
DetailName string `json:"detailName"` DetailName string `json:"detailName"`
Secret string `json:"secret"` Secret string `json:"secret"`
IsImmediate bool `json:"isImmediate"` StopBefore bool `json:"stopBefore"`
StopBefore bool `json:"stopBefore"` TaskID string `json:"taskID"`
TaskID string `json:"taskID"` FileName string `json:"fileName"`
FileName string `json:"fileName"` Args []string `json:"args"`
Args []string `json:"args"`
Description string `json:"description"` Description string `json:"description"`
} }
+1 -2
View File
@@ -2,8 +2,7 @@ package dto
type SearchWithPage struct { type SearchWithPage struct {
PageInfo PageInfo
Info string `json:"info"` Info string `json:"info"`
ExcludeAppStore bool `json:"excludeAppStore"`
} }
type SearchPageWithType struct { type SearchPageWithType struct {
+5 -16
View File
@@ -134,15 +134,10 @@ type ExtraHost struct {
IP string `json:"ip"` IP string `json:"ip"`
} }
type ContainerNetwork struct { type ContainerNetwork struct {
Network string `json:"network"` Network string `json:"network"`
Ipv4 string `json:"ipv4"` Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"` Ipv6 string `json:"ipv6"`
MacAddr string `json:"macAddr"` MacAddr string `json:"macAddr"`
Links []string `json:"links"`
Aliases []string `json:"aliases"`
DriverOpts map[string]string `json:"driverOpts"`
GwPriority int `json:"gwPriority"`
LinkLocalIPs []string `json:"linkLocalIPs"`
} }
type ContainerCreateByCommand struct { type ContainerCreateByCommand struct {
@@ -300,7 +295,6 @@ type ComposeInfo struct {
ConfigFile string `json:"configFile"` ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"` Workdir string `json:"workdir"`
ComposeFileExists bool `json:"composeFileExists"` ComposeFileExists bool `json:"composeFileExists"`
IsPinned bool `json:"isPinned"`
Path string `json:"path"` Path string `json:"path"`
Containers []ComposeContainer `json:"containers"` Containers []ComposeContainer `json:"containers"`
Env string `json:"env"` Env string `json:"env"`
@@ -315,7 +309,6 @@ type ComposeContainer struct {
type ComposeCreate struct { type ComposeCreate struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Name string `json:"name"` Name string `json:"name"`
DirName string `json:"dirName"`
From string `json:"from" validate:"required,oneof=edit path template"` From string `json:"from" validate:"required,oneof=edit path template"`
File string `json:"file"` File string `json:"file"`
Path string `json:"path"` Path string `json:"path"`
@@ -326,7 +319,7 @@ type ComposeCreate struct {
type ComposeOperation struct { type ComposeOperation struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path"` Path string `json:"path"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"`
WithFile bool `json:"withFile"` WithFile bool `json:"withFile"`
Force bool `json:"force"` Force bool `json:"force"`
} }
@@ -339,10 +332,6 @@ type ComposeUpdate struct {
Env string `json:"env"` Env string `json:"env"`
ForcePull bool `json:"forcePull"` ForcePull bool `json:"forcePull"`
} }
type ComposePin struct {
Name string `json:"name" validate:"required"`
IsPinned bool `json:"isPinned"`
}
type ComposeLogClean struct { type ComposeLogClean struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
-1
View File
@@ -197,7 +197,6 @@ type SearchRecord struct {
type Record struct { type Record struct {
ID uint `json:"id"` ID uint `json:"id"`
CronjobID uint `json:"cronjobID"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
StartTime string `json:"startTime"` StartTime string `json:"startTime"`
Records string `json:"records"` Records string `json:"records"`
+2 -10
View File
@@ -79,9 +79,8 @@ type NodeCurrent struct {
} }
type DashboardCurrent struct { type DashboardCurrent struct {
Uptime uint64 `json:"uptime"` Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"` TimeSinceUptime string `json:"timeSinceUptime"`
RunningTime RunningTime `json:"runningTime"`
Procs uint64 `json:"procs"` Procs uint64 `json:"procs"`
@@ -129,13 +128,6 @@ type DashboardCurrent struct {
ShotTime time.Time `json:"shotTime"` ShotTime time.Time `json:"shotTime"`
} }
type RunningTime struct {
Days uint64 `json:"days"`
Hours uint64 `json:"hours"`
Minutes uint64 `json:"minutes"`
Seconds uint64 `json:"seconds"`
}
type AppLauncherSync struct { type AppLauncherSync struct {
Keys []string `json:"keys"` Keys []string `json:"keys"`
} }
+10 -69
View File
@@ -60,84 +60,25 @@ type MysqlDBCreate struct {
Database string `json:"database" validate:"required"` Database string `json:"database" validate:"required"`
Format string `json:"format" validate:"required"` Format string `json:"format" validate:"required"`
Collation string `json:"collation"` Collation string `json:"collation"`
Username string `json:"username"` Username string `json:"username" validate:"required"`
Password string `json:"password"` Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"` Permission string `json:"permission" validate:"required"`
Description string `json:"description"` Description string `json:"description"`
} }
type MysqlUser struct {
Username string `json:"username"`
Host string `json:"host"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type MysqlGrant struct {
Database string `json:"database"`
Username string `json:"username"`
Host string `json:"host"`
}
type MysqlGrantSummarySearch struct {
Database string `json:"database" validate:"required"`
DBs []string `json:"dbs" validate:"required"`
}
type MysqlUserSearch struct {
Database string `json:"database" validate:"required"`
}
type MysqlUserCreate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
DBs []string `json:"dbs"`
}
type MysqlUserDelete struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlUserUpdate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
NewHost string `json:"newHost" validate:"required"`
Description string `json:"description"`
}
type MysqlUserPassword struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
Password string `json:"password" validate:"required"`
}
type MysqlGrantCreate struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlGrantDelete struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlFormatCollationOption struct { type MysqlFormatCollationOption struct {
Format string `json:"format"` Format string `json:"format"`
Collations []string `json:"collations"` Collations []string `json:"collations"`
} }
type BindUser struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"`
}
type MysqlLoadDB struct { type MysqlLoadDB struct {
From string `json:"from" validate:"required,oneof=local remote"` From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"` Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
+14 -1
View File
@@ -35,6 +35,19 @@ type PortRuleOperate struct {
Description string `json:"description"` Description string `json:"description"`
} }
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
} `json:"rules"`
}
type UpdateFirewallDescription struct { type UpdateFirewallDescription struct {
Type string `json:"type"` Type string `json:"type"`
Chain string `json:"chain"` Chain string `json:"chain"`
@@ -73,7 +86,7 @@ type BatchRuleOperate struct {
} }
type IptablesOp struct { type IptablesOp struct {
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC 1PANEL_FORWARD"` Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"` Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
} }
-43
View File
@@ -1,43 +0,0 @@
package dto
type ForwardRuleSearch struct {
PageInfo
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
}
// ForwardRule preserves the existing firewall search response shape while
// keeping forwarding data separate from the filter client model.
type ForwardRule struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Family string `json:"family"`
Address string `json:"address"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Strategy string `json:"strategy"`
Num string `json:"num"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
Interface string `json:"interface"`
UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
}
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules"`
}
type ForwardRuleOperation struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
}
-36
View File
@@ -1,8 +1,6 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
) )
@@ -16,37 +14,3 @@ type SearchTaskLogReq struct {
type TaskDTO struct { type TaskDTO struct {
model.Task model.Task
} }
type SystemLogReq struct {
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
Cursor string `json:"cursor"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Keyword string `json:"keyword"`
Priority string `json:"priority"`
Service string `json:"service"`
}
type SystemLogRes struct {
Source string `json:"source"`
Items []SystemLogItem `json:"items"`
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
}
type SystemLogStatus struct {
Source string `json:"source"`
Version string `json:"version"`
KeywordFilterSupported bool `json:"keywordFilterSupported"`
Message string `json:"message"`
}
type SystemLogItem struct {
Timestamp int64 `json:"-"`
Cursor string `json:"-"`
Time string `json:"time"`
Priority string `json:"priority"`
Service string `json:"service"`
Message string `json:"message"`
Raw string `json:"raw"`
}
+5 -39
View File
@@ -1,8 +1,6 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components" "github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
) )
@@ -89,41 +87,9 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}} var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Custom bool `json:"custom,omitempty"` Script string `json:"script"`
Script string `json:"script"` Packages []string `json:"packages"`
Packages []string `json:"packages"` Params string `json:"params"`
Params string `json:"params"` Enable bool `json:"enable"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode,omitempty"`
Provider string `json:"provider,omitempty"`
LoadOrder int `json:"loadOrder,omitempty"`
Builds []NginxModuleBuild `json:"builds,omitempty"`
LastError string `json:"lastError,omitempty"`
}
type NginxModuleBuild struct {
Provider string `json:"provider"`
BuildMode string `json:"buildMode"`
Status string `json:"status"`
Hash string `json:"hash"`
Target NginxModuleTarget `json:"target"`
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
Error string `json:"error,omitempty"`
BuiltAt time.Time `json:"builtAt,omitempty"`
}
type NginxModuleTarget struct {
Key string `json:"key"`
OpenRestyVersion string `json:"openrestyVersion"`
Architecture string `json:"architecture"`
Image string `json:"image,omitempty"`
ImageDigest string `json:"imageDigest,omitempty"`
BuilderDigest string `json:"builderDigest,omitempty"`
}
type NginxModuleArtifact struct {
Name string `json:"name"`
Path string `json:"path"`
Checksum string `json:"checksum"`
} }
-1
View File
@@ -99,7 +99,6 @@ type AppInstallUpgrade struct {
DetailID uint `json:"detailId"` DetailID uint `json:"detailId"`
Backup bool `json:"backup"` Backup bool `json:"backup"`
PullImage bool `json:"pullImage"` PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
} }
-16
View File
@@ -21,10 +21,6 @@ type McpServerCreate struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"` OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
TaskID string `json:"taskID"`
} }
type McpServerUpdate struct { type McpServerUpdate struct {
@@ -36,23 +32,11 @@ type McpServerDelete struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type McpServerDetail struct {
ID uint `json:"id" validate:"required"`
}
type McpServerStatusSync struct {
IDs []uint `json:"ids"`
}
type McpServerOperate struct { type McpServerOperate struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required"`
} }
type McpServerConnectionTest struct {
ID uint `json:"id" validate:"required"`
}
type McpBindDomain struct { type McpBindDomain struct {
Domain string `json:"domain" validate:"required"` Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
+8 -13
View File
@@ -114,22 +114,17 @@ type NginxRedirectUpdate struct {
} }
type NginxBuildReq struct { type NginxBuildReq struct {
TaskID string `json:"taskID" validate:"required"` TaskID string `json:"taskID" validate:"required"`
Mirror string `json:"mirror" validate:"required"` Mirror string `json:"mirror" validate:"required"`
Modules []string `json:"modules"`
Force bool `json:"force"`
} }
type NginxModuleUpdate struct { type NginxModuleUpdate struct {
Operate string `json:"operate" validate:"required,oneof=create delete update"` Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Script string `json:"script"` Script string `json:"script"`
Packages string `json:"packages"` Packages string `json:"packages"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
Params string `json:"params"` Params string `json:"params"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
} }
type NginxOperateReq struct { type NginxOperateReq struct {
+3 -8
View File
@@ -21,14 +21,13 @@ type RuntimeCreate struct {
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"` CodeDir string `json:"codeDir"`
Remark string `json:"remark"` Remark string `json:"remark"`
TaskID string `json:"taskID"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
} }
type NodeConfig struct { type NodeConfig struct {
Install *bool `json:"install"` Install bool `json:"install"`
Clean bool `json:"clean"` Clean bool `json:"clean"`
ExposedPorts []ExposedPort `json:"exposedPorts"` ExposedPorts []ExposedPort `json:"exposedPorts"`
Environments []Environment `json:"environments"` Environments []Environment `json:"environments"`
@@ -43,14 +42,12 @@ type Environment struct {
type Volume struct { type Volume struct {
Source string `json:"source"` Source string `json:"source"`
Target string `json:"target"` Target string `json:"target"`
Mode string `json:"mode"`
} }
type ExposedPort struct { type ExposedPort struct {
HostPort int `json:"hostPort"` HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"` ContainerPort int `json:"containerPort"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
Protocol string `json:"protocol"`
} }
type ExtraHost struct { type ExtraHost struct {
@@ -59,10 +56,8 @@ type ExtraHost struct {
} }
type RuntimeDelete struct { type RuntimeDelete struct {
ID uint `json:"id"` ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"` ForceDelete bool `json:"forceDelete"`
DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"`
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
-2
View File
@@ -34,8 +34,6 @@ type WebsiteCreate struct {
SiteDir string `json:"siteDir"` SiteDir string `json:"siteDir"`
TemplateOutputID uint `json:"templateOutputID"`
RuntimeConfig RuntimeConfig
FtpConfig FtpConfig
DataBaseConfig DataBaseConfig
-14
View File
@@ -126,16 +126,6 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"` Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
Description string `json:"description"` Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
Sync bool `json:"sync"`
} }
type WebsiteCASearch struct { type WebsiteCASearch struct {
@@ -167,8 +157,6 @@ type WebsiteCAObtain struct {
Description string `json:"description"` Description string `json:"description"`
ExecShell bool `json:"execShell"` ExecShell bool `json:"execShell"`
Shell string `json:"shell"` Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
type WebsiteCARenew struct { type WebsiteCARenew struct {
@@ -179,6 +167,4 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"` Type string `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SSLID uint64 `json:"sslID"` SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
-46
View File
@@ -1,46 +0,0 @@
package request
import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
type WebsiteTemplateSearch struct {
dto.PageInfo
Name string `json:"name"`
Type string `json:"type"`
}
type WebsiteTemplateCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required,oneof=single multi"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Variables string `json:"variables"`
Remark string `json:"remark"`
}
type WebsiteTemplateUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required,oneof=single multi"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Variables string `json:"variables"`
Remark string `json:"remark"`
}
type WebsiteTemplateOutputSearch struct {
dto.PageInfo
TemplateID uint `json:"templateID"`
}
type WebsiteTemplateOutputCreate struct {
TemplateID uint `json:"templateID" validate:"required"`
Name string `json:"name" validate:"required"`
VariableValues map[string]string `json:"variableValues"`
}
type WebsitePreviewReq struct {
TemplateID uint `json:"templateID" validate:"required"`
VariableValues map[string]string `json:"variableValues"`
}
+28 -30
View File
@@ -104,36 +104,34 @@ type AppDetail struct {
} }
type AppInstallDTO struct { type AppInstallDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"` AppDetailID uint `json:"appDetailID"`
Version string `json:"version"` Version string `json:"version"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
HttpPort int `json:"httpPort"` HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"` HttpsPort int `json:"httpsPort"`
Path string `json:"path"` Path string `json:"path"`
CanUpdate bool `json:"canUpdate"` CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"` Icon string `json:"icon"`
AppName string `json:"appName"` AppName string `json:"appName"`
Ready int `json:"ready"` Ready int `json:"ready"`
Total int `json:"total"` Total int `json:"total"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
AppType string `json:"appType"` AppType string `json:"appType"`
AppStatus string `json:"appStatus"` AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"` WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"` SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"` App AppDetail `json:"app"`
Container string `json:"container"` Container string `json:"container"`
IsEdit bool `json:"isEdit"` IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"` LinkDB bool `json:"linkDB"`
ResourceKeys []string `json:"resourceKeys"` ServiceName string `json:"serviceName"`
ServiceName string `json:"serviceName"`
Env map[string]interface{} `json:"env"`
} }
type AppInstallInfo struct { type AppInstallInfo struct {
-14
View File
@@ -16,12 +16,6 @@ type McpServerDTO struct {
Volumes []request.Volume `json:"volumes"` Volumes []request.Volume `json:"volumes"`
} }
type McpServerStatusDTO struct {
ID uint `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}
type McpBindDomainRes struct { type McpBindDomainRes struct {
Domain string `json:"domain"` Domain string `json:"domain"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
@@ -30,11 +24,3 @@ type McpBindDomainRes struct {
WebsiteID uint `json:"websiteID"` WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"` ConnUrl string `json:"connUrl"`
} }
type McpServerConnectionTestRes struct {
Success bool `json:"success"`
Endpoint string `json:"endpoint"`
OutputTransport string `json:"outputTransport"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
Message string `json:"message"`
}
+7 -16
View File
@@ -69,25 +69,16 @@ type NginxProxyCache struct {
} }
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Custom bool `json:"custom"` Script string `json:"script"`
Script string `json:"script"` Packages string `json:"packages"`
Packages string `json:"packages"` Params string `json:"params"`
Params string `json:"params"` Enable bool `json:"enable"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode"`
Provider string `json:"provider"`
LoadOrder int `json:"loadOrder"`
BuildStatus string `json:"buildStatus"`
LoadStatus string `json:"loadStatus"`
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
LastError string `json:"lastError"`
} }
type NginxBuildConfig struct { type NginxBuildConfig struct {
Mirror string `json:"mirror"` Mirror string `json:"mirror"`
DynamicSupported bool `json:"dynamicSupported"` Modules []NginxModule `json:"modules"`
Modules []NginxModule `json:"modules"`
} }
type NginxConfigRes struct { type NginxConfigRes struct {
@@ -1,18 +0,0 @@
package response
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type WebsiteTemplateDTO struct {
model.WebsiteTemplate
}
type WebsiteTemplateOutputDTO struct {
model.WebsiteTemplateOutput
TemplateName string `json:"templateName"`
}
type WebsitePreviewDTO struct {
HTML string `json:"html"`
}
-30
View File
@@ -1,30 +0,0 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
-2
View File
@@ -7,8 +7,6 @@ type AgentAccount struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
+8 -4
View File
@@ -2,10 +2,14 @@ package model
type AgentAccountModel struct { type AgentAccountModel struct {
BaseModel BaseModel
AccountID uint `json:"accountId" gorm:"index"` AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"` Model string `json:"model" gorm:"index"`
Name string `json:"name"` Name string `json:"name"`
SortOrder int `json:"sortOrder" gorm:"index"` ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
} }
func (AgentAccountModel) TableName() string { func (AgentAccountModel) TableName() string {
-1
View File
@@ -31,5 +31,4 @@ type BackupRecord struct {
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Description string `json:"description"` Description string `json:"description"`
Args string `gorm:"not null;default:''" json:"args"`
} }
+2 -3
View File
@@ -11,7 +11,6 @@ type ComposeTemplate struct {
type Compose struct { type Compose struct {
BaseModel BaseModel
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
IsPinned bool `json:"isPinned"`
} }
-21
View File
@@ -1,21 +0,0 @@
package model
type DatabaseUser struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user"`
Host string `json:"host" gorm:"uniqueIndex:idx_database_user"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type DatabaseUserGrant struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
}
-2
View File
@@ -8,6 +8,4 @@ type Ftp struct {
Status string `gorm:"not null" json:"status"` Status string `gorm:"not null" json:"status"`
Path string `gorm:"not null" json:"path"` Path string `gorm:"not null" json:"path"`
Description string `gorm:"not null" json:"description"` Description string `gorm:"not null" json:"description"`
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
} }
-3
View File
@@ -18,7 +18,4 @@ type McpServer struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"` OutputTransport string `json:"outputTransport"`
Type string `json:"type"` Type string `json:"type"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs"`
} }
-28
View File
@@ -1,28 +0,0 @@
package model
type WebsiteTemplate struct {
BaseModel
Name string `gorm:"not null" json:"name"`
Type string `gorm:"not null" json:"type"` // single | multi
Content string `gorm:"type:longtext" json:"content"`
FilePath string `json:"filePath"`
Variables string `gorm:"type:text" json:"variables"`
Remark string `json:"remark"`
}
func (w WebsiteTemplate) TableName() string {
return "website_templates"
}
type WebsiteTemplateOutput struct {
BaseModel
Name string `gorm:"not null" json:"name"`
TemplateID uint `gorm:"not null" json:"templateID"`
TemplateType string `json:"templateType"`
VariableValues string `gorm:"type:text" json:"variableValues"`
OutputPath string `json:"outputPath"`
}
func (w WebsiteTemplateOutput) TableName() string {
return "website_template_outputs"
}
+328 -359
View File
@@ -1,248 +1,319 @@
package provider package provider
import ( import (
"fmt"
"net/url"
"strings" "strings"
) )
type APIConfig struct { type Model struct {
APIType string ID string
BaseURL string Name string
EditableBaseURL bool ContextWindow int
DiscoverModels bool MaxTokens int
DefaultAuthMode string Reasoning bool
AuthModes []string Input []string
Models []Model
} }
const ( type RuntimeDefault struct {
AuthModeBearer = "bearer" APIType string
AuthModeXAPIKey = "x-api-key" ContextWindow int
) MaxTokens int
Input []string
type Model struct {
ID string
Name string
} }
type Meta struct { type Meta struct {
Key string Key string
DisplayName string DisplayName string
DisplayNameKey string
Sort uint Sort uint
DefaultAPIType string DefaultBaseURL string
APIConfigs []APIConfig
EnvKey string EnvKey string
Default RuntimeDefault
Models []Model Models []Model
} }
var catalog = map[string]Meta{ var catalog = map[string]Meta{
"custom": { "custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY", Key: "custom",
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"), DisplayName: "Custom",
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"ollama": { "ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses", Key: "ollama",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions"), DisplayName: "Ollama",
Sort: 15,
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 160000,
MaxTokens: 8192,
Input: []string{"text"},
},
}, },
"vllm": { "vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY", Key: "vllm",
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"), DisplayName: "vLLM",
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"deepseek": { "deepseek": {
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY", Key: "deepseek",
APIConfigs: []APIConfig{ DisplayName: "DeepSeek",
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"}, Sort: 25,
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey), DefaultBaseURL: "https://api.deepseek.com/v1",
}, EnvKey: "DEEPSEEK_API_KEY",
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}}, Default: RuntimeDefault{
}, APIType: "openai-completions",
"bailian-coding-plan": { ContextWindow: 131072,
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY", MaxTokens: 8192,
APIConfigs: []APIConfig{ Input: []string{"text"},
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
}, },
Models: []Model{ Models: []Model{
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"}, {ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"}, {ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"}, },
{ID: "glm-4.7", Name: "GLM-4.7"}, },
{ID: "kimi-k2.5", Name: "Kimi K2.5"}, "bailian-coding-plan": {
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"}, Key: "bailian-coding-plan",
{ID: "glm-5", Name: "GLM-5"}, DisplayName: "阿里云百炼 Coding Plan",
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"}, Sort: 30,
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"}, DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"}, EnvKey: "QWEN_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
}, },
}, },
"ark-coding-plan": { "ark-coding-plan": {
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY", Key: "ark-coding-plan",
APIConfigs: []APIConfig{ DisplayName: "方舟 Coding Plan",
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"}, Sort: 35,
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer), DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 4096,
Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"}, {ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}, {ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"}, {ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
}, },
}, },
"zai": { "zai": {
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY", Key: "zai",
APIConfigs: []APIConfig{ DisplayName: "Z.ai",
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}, Sort: 40,
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}, DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
}, },
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
}, },
"minimax": { "minimax": {
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY", Key: "minimax",
APIConfigs: []APIConfig{ DisplayName: "MiniMax (CN)",
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer), Sort: 45,
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"}, DefaultBaseURL: "https://api.minimaxi.com/anthropic",
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"}, EnvKey: "MINIMAX_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
}, },
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": { "xiaomi": {
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY", Key: "xiaomi",
APIConfigs: []APIConfig{ DisplayName: "Xiaomi",
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"}, Sort: 46,
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"}, DefaultBaseURL: "https://api.xiaomimimo.com/v1",
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer), EnvKey: "XIAOMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
}, },
"kimi": { "kimi": {
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY", Key: "kimi",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}}, DisplayName: "Kimi (CN)",
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, Sort: 50,
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
}, },
"kimi-coding": { "kimi-coding": {
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY", Key: "kimi-coding",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)}, DisplayName: "Kimi Coding",
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}}, Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
},
}, },
"openai": { "openai": {
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY", Key: "openai",
APIConfigs: []APIConfig{ DisplayName: "OpenAI",
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"}, Sort: 55,
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"}, DefaultBaseURL: "https://api.openai.com/v1",
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"}, EnvKey: "OPENAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 272000,
MaxTokens: 128000,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
}, },
"openrouter": { "openrouter": {
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY", Key: "openrouter",
APIConfigs: []APIConfig{ DisplayName: "OpenRouter",
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}, Sort: 56,
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"}, DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
}, },
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
}, },
"anthropic": { "anthropic": {
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY", Key: "anthropic",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)}, DisplayName: "Anthropic",
Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}}, Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"},
{ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"},
{ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"},
},
}, },
"gemini": { "gemini": {
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY", Key: "gemini",
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}}, DisplayName: "Gemini",
Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}}, Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
}, },
"moonshot": { "moonshot": {
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY", Key: "moonshot",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}}, DisplayName: "Moonshot (Global)",
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, Sort: 70,
}, DefaultBaseURL: "https://api.moonshot.ai/v1",
"bailian": { EnvKey: "MOONSHOT_API_KEY",
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY", Default: RuntimeDefault{
APIConfigs: []APIConfig{ APIType: "openai-completions",
{ ContextWindow: 256000,
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", MaxTokens: 8192,
DiscoverModels: true, Input: []string{"text"},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}}, },
}, Models: []Model{
{ {ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", {ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
DiscoverModels: true, {ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
},
{
APIType: "anthropic-messages", BaseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
DefaultAuthMode: AuthModeBearer,
AuthModes: []string{AuthModeBearer},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
},
{
APIType: "dashscope-images", BaseURL: "https://dashscope.aliyuncs.com",
Models: []Model{
{ID: "qwen-image-2.0-pro", Name: "qwen-image-2.0-pro"},
{ID: "qwen-image-2.0", Name: "qwen-image-2.0"},
{ID: "wan2.7-image-pro", Name: "wan2.7-image-pro"},
{ID: "wan2.7-image", Name: "wan2.7-image"},
},
},
}, },
}, },
"ark": {
Key: "ark", DisplayNameKey: "AIProviderArk", Sort: 36, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
APIConfigs: []APIConfig{
{
APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
DiscoverModels: true,
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
},
{
APIType: "openai-responses", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
DiscoverModels: true,
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
},
{
APIType: "openai-images", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
Models: []Model{
{ID: "doubao-seedream-5-0-260128", Name: "doubao-seedream-5-0-260128"},
{ID: "doubao-seedream-5-0-lite-260128", Name: "doubao-seedream-5-0-lite-260128"},
{ID: "doubao-seedream-4-5-251128", Name: "doubao-seedream-4-5-251128"},
},
},
},
},
}
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
configs := make([]APIConfig, 0, len(apiTypes))
for _, apiType := range apiTypes {
if apiType == "anthropic-messages" {
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
config.EditableBaseURL = true
configs = append(configs, config)
continue
}
configs = append(configs, APIConfig{
APIType: apiType,
EditableBaseURL: true,
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
})
}
return configs
}
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
authModes := []string{defaultAuthMode}
for _, authMode := range additionalAuthModes {
if authMode != defaultAuthMode {
authModes = append(authModes, authMode)
}
}
return APIConfig{
APIType: "anthropic-messages",
BaseURL: baseURL,
DefaultAuthMode: defaultAuthMode,
AuthModes: authModes,
}
} }
func Get(key string) (Meta, bool) { func Get(key string) (Meta, bool) {
@@ -261,142 +332,12 @@ func All() map[string]Meta {
return result return result
} }
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
meta, ok := catalog[key]
if !ok {
return APIConfig{}, false
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType == target {
config.AuthModes = append([]string(nil), config.AuthModes...)
config.Models = append([]Model(nil), config.Models...)
return config, true
}
}
return APIConfig{}, false
}
func DefaultModels(key, apiType string) []Model {
meta, ok := catalog[key]
if !ok {
return nil
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType != target {
continue
}
if len(config.Models) > 0 {
return append([]Model(nil), config.Models...)
}
if IsImageAPIType(config.APIType) {
return nil
}
break
}
return append([]Model(nil), meta.Models...)
}
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(provider, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
}
if config.APIType != "anthropic-messages" {
return "", nil
}
authMode := strings.TrimSpace(requested)
if authMode == "" {
authMode = config.DefaultAuthMode
}
for _, allowed := range config.AuthModes {
if authMode == allowed {
return authMode, nil
}
}
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
}
func DefaultBaseURL(key string) (string, bool) { func DefaultBaseURL(key string) (string, bool) {
config, ok := FindAPIConfig(key, "") meta, ok := catalog[key]
if !ok || strings.TrimSpace(config.BaseURL) == "" { if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
return "", false return "", false
} }
return config.BaseURL, true return meta.DefaultBaseURL, true
}
func DefaultAPIType(key string) string {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
return "openai-completions"
}
return meta.DefaultAPIType
}
func ResolveBaseURL(key, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(key, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
}
if !config.EditableBaseURL {
return strings.TrimRight(config.BaseURL, "/"), nil
}
baseURL := strings.TrimSpace(requested)
if baseURL == "" {
baseURL = config.BaseURL
}
if baseURL == "" {
return "", fmt.Errorf("base url is required")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid base url")
}
if key == "custom" && IsImageAPIType(config.APIType) {
return baseURL, nil
}
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
parsed.RawQuery = ""
parsed.Fragment = ""
return strings.TrimRight(parsed.String(), "/"), nil
}
func normalizeEndpointPath(apiType, value string) string {
if IsImageAPIType(apiType) {
return strings.TrimRight(value, "/")
}
path := strings.TrimRight(value, "/")
suffixes := []string{}
switch apiType {
case "openai-completions":
suffixes = []string{"/chat/completions"}
case "openai-responses":
suffixes = []string{"/responses"}
case "anthropic-messages":
suffixes = []string{"/v1/messages", "/messages"}
}
for _, suffix := range suffixes {
if strings.HasSuffix(strings.ToLower(path), suffix) {
return path[:len(path)-len(suffix)]
}
}
return path
}
func IsImageAPIType(apiType string) bool {
switch apiType {
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
return true
default:
return false
}
} }
func EnvKey(key string) string { func EnvKey(key string) string {
@@ -409,61 +350,89 @@ func EnvKey(key string) string {
func DisplayName(key string) string { func DisplayName(key string) string {
meta, ok := catalog[key] meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DisplayName) == "" { if !ok {
return key
}
if strings.TrimSpace(meta.DisplayName) == "" {
return key return key
} }
return meta.DisplayName return meta.DisplayName
} }
func DisplayNameKey(key string) string { func FindModel(key, modelID string) (Model, bool) {
meta, ok := catalog[key] meta, ok := Get(key)
if !ok { if !ok {
return "" return Model{}, false
} }
return meta.DisplayNameKey for _, item := range meta.Models {
} if item.ID == modelID {
return item, true
func NormalizeModelID(provider, modelID string) string {
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
for _, prefix := range legacyModelPrefixes[provider] {
legacyPrefix := prefix + "/"
if !strings.HasPrefix(target, legacyPrefix) {
continue
}
candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
if candidate != "" {
return candidate
} }
} }
return target return Model{}, false
}
var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},
"zai": {"zai"},
"minimax": {"minimax"},
"xiaomi": {"xiaomi"},
"kimi": {"kimi", "moonshot"},
"kimi-coding": {"kimi-coding"},
"openai": {"openai"},
"anthropic": {"anthropic"},
"gemini": {"google", "gemini"},
"moonshot": {"moonshot"},
} }
func cloneMeta(meta Meta) Meta { func cloneMeta(meta Meta) Meta {
clone := meta clone := meta
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs)) if len(meta.Default.Input) > 0 {
for index, config := range meta.APIConfigs { clone.Default.Input = make([]string, len(meta.Default.Input))
clone.APIConfigs[index] = config copy(clone.Default.Input, meta.Default.Input)
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...) }
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...) if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta, item)
}
} }
clone.Models = append([]Model(nil), meta.Models...)
return clone return clone
} }
func normalizeModel(meta Meta, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 {
clone.MaxTokens = meta.Default.MaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = meta.Default.ContextWindow
}
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
clone.Input = make([]string, len(meta.Default.Input))
copy(clone.Input, meta.Default.Input)
}
return clone
}
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
defaultAPIType := "openai-completions"
defaultMaxTokens := 8192
defaultContextWindow := 256000
if meta, ok := Get(provider); ok {
if meta.Default.APIType != "" {
defaultAPIType = meta.Default.APIType
}
if meta.Default.MaxTokens > 0 {
defaultMaxTokens = meta.Default.MaxTokens
}
if meta.Default.ContextWindow > 0 {
defaultContextWindow = meta.Default.ContextWindow
}
}
resolvedAPI := apiType
if strings.TrimSpace(apiType) == "" {
resolvedAPI = defaultAPIType
}
resolvedMaxTokens := defaultMaxTokens
resolvedContextWindow := defaultContextWindow
if maxTokens > 0 {
resolvedMaxTokens = maxTokens
}
if contextWindow > 0 {
resolvedContextWindow = contextWindow
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
-74
View File
@@ -1,74 +0,0 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
func DiscoverModels(baseURL, apiKey string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("request failed: %s", resp.Status)
}
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
models := make([]string, 0, len(payload.Data))
seen := make(map[string]struct{}, len(payload.Data))
for _, item := range payload.Data {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
models = append(models, id)
}
if len(models) == 0 {
return nil, fmt.Errorf("no models found")
}
return models, nil
}
func buildModelDiscoveryURL(baseURL string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} {
base = normalizeEndpointPath(apiType, base)
}
switch {
case strings.HasSuffix(base, "/models"):
return base
case hasAPIVersionSuffix(base):
return base + "/models"
default:
return base + "/v1/models"
}
}
func hasAPIVersionSuffix(value string) bool {
segment := value[strings.LastIndex(value, "/")+1:]
if len(segment) < 2 || segment[0] != 'v' {
return false
}
_, err := strconv.Atoi(segment[1:])
return err == nil
}
+52 -32
View File
@@ -15,52 +15,72 @@ type OpenClawProviderPatch struct {
AuthHeader bool AuthHeader bool
} }
func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) { func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
if modelName == "" { if modelName == "" {
return nil, fmt.Errorf("model is required") return nil, fmt.Errorf("model is required")
} }
resolvedAPIType := apiType resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok { modelID := resolveOpenClawModelID(provider, modelName)
resolvedAPIType = DefaultAPIType(provider)
}
if IsImageAPIType(resolvedAPIType) {
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
}
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
return nil, err
}
usesBearer := resolvedAuthMode == AuthModeBearer
modelID := NormalizeModelID(provider, modelName)
providerKey := provider
preserveQualifiedModel := false
switch provider { switch provider {
case "deepseek":
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
case "gemini": case "gemini":
providerKey = "google" return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
resolvedAPIType = "google-generative-ai"
usesBearer = false
case "moonshot", "kimi": case "moonshot", "kimi":
providerKey = "moonshot" return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
resolvedAPIType = "openai-completions" case "bailian-coding-plan":
usesBearer = false return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "ark-coding-plan":
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "minimax":
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
case "xiaomi":
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
case "custom", "vllm":
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "ollama": case "ollama":
apiKey = "ollama" return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
usesBearer = false case "kimi-coding":
case "openai", "openrouter", "anthropic": return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
preserveQualifiedModel = strings.Contains(modelName, "/") case "zai":
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
default:
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
} }
}
primaryModel := providerKey + "/" + modelID func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
if preserveQualifiedModel { providerKey := provider
primaryModel = modelName primaryModel := modelName
if provider == "kimi" {
providerKey = "moonshot"
primaryModel = "moonshot/" + modelID
} }
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
}
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
return &OpenClawProviderPatch{ return &OpenClawProviderPatch{
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
ProviderKey: providerKey, ProviderKey: providerKey,
ModelID: modelID, ModelID: modelID,
APIKey: apiKey, APIKey: apiKey,
BaseURL: baseURL, BaseURL: baseURL,
APIType: resolvedAPIType, APIType: apiType,
AuthHeader: usesBearer, AuthHeader: authHeader,
}, nil }
}
func resolveOpenClawModelID(provider, modelName string) string {
if provider == "custom" || provider == "vllm" {
target := strings.TrimLeft(modelName, "/")
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
return strings.TrimLeft(parts[1], "/")
}
return target
}
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
return parts[1]
}
return modelName
} }
+121 -103
View File
@@ -3,9 +3,7 @@ package provider
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -20,140 +18,160 @@ type VerifyRequest struct {
Body []byte Body []byte
} }
type verifyErrorResponse struct { const (
Error struct { defaultVerifyTimeout = 30 * time.Second
Message string `json:"message"` )
} `json:"error"`
Message string `json:"message"`
}
const defaultVerifyTimeout = 30 * time.Second func SkipVerification(key string) bool {
switch key {
func SkipVerification(provider string) bool { case "custom", "vllm", "ollama", "kimi-coding":
switch provider {
case "vllm", "ollama", "kimi-coding":
return true return true
default: default:
return false return false
} }
} }
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error { func VerifyAccount(provider, baseURL, apiKey string) error {
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model) req := BuildVerifyRequest(provider, baseURL, apiKey)
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body)) var body *bytes.Buffer
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil { if err != nil {
return err return err
} }
for key, value := range req.Headers { for key, value := range req.Headers {
httpReq.Header.Set(key, value) httpReq.Header.Set(key, value)
} }
httpReq.Header.Set("Accept", "application/json") resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
if err != nil { if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err) return buserr.WithErr("ErrAgentAccountUnavailable", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { if resp.StatusCode >= 400 {
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
if readErr != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
}
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
} }
return nil return nil
} }
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest { func verifyTimeout() time.Duration {
baseURL = strings.TrimSpace(baseURL) return defaultVerifyTimeout
if provider != "custom" || !IsImageAPIType(apiType) { }
baseURL = strings.TrimRight(baseURL, "/")
}
headers := map[string]string{"Content-Type": "application/json"}
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
if provider == "gemini" { func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent" base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers["x-goog-api-key"] = apiKey headers := map[string]string{}
request.Body = mustJSON(map[string]interface{}{ request := VerifyRequest{Method: http.MethodGet, Headers: headers}
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
})
return request
}
switch apiType { switch provider {
case "openai-images": case "anthropic", "kimi-coding":
request.URL = imageVerifyURL(provider, baseURL, "/images/generations") headers["x-api-key"] = apiKey
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "dashscope-images":
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/services/aigc/multimodal-generation/generation")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{
"model": model,
"input": map[string]interface{}{"messages": []map[string]interface{}{
{"role": "user", "content": []map[string]string{{"text": "test"}}},
}},
"parameters": map[string]interface{}{"n": 1},
})
case "minimax-images":
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "openrouter-images":
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "anthropic-messages":
request.URL = baseURL + "/v1/messages"
if authMode == AuthModeBearer {
headers["Authorization"] = "Bearer " + apiKey
} else {
headers["x-api-key"] = apiKey
}
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
request.Method = http.MethodPost
if strings.Contains(base, "/v1beta") {
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
} else {
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
}
headers["x-goog-api-key"] = apiKey
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "max_tokens": 1, "stream": false, "contents": []map[string]interface{}{{
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, "parts": []map[string]string{{
"text": "Explain how AI works in a few words",
}},
}},
}) })
case "openai-responses": case "zai":
request.URL = baseURL + "/responses" headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Authorization"] = "Bearer " + apiKey request.URL = base + "/models"
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false}) case "bailian-coding-plan":
default: request.Method = http.MethodPost
request.URL = baseURL + "/chat/completions" if !strings.Contains(base, "/v1") {
if provider != "ollama" || strings.TrimSpace(apiKey) != "" { base = base + "/v1"
headers["Authorization"] = "Bearer " + apiKey }
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "ark-code-latest",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
} }
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false, "model": "MiniMax-M2.5",
"max_tokens": 1,
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
}) })
case "xiaomi":
request.Method = http.MethodPost
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
request.Body = mustJSON(map[string]interface{}{
"model": "mimo-v2-flash",
"max_tokens": 1,
"messages": []map[string]string{{"role": "user", "content": "test"}},
})
case "openrouter":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/key"
} else {
request.URL = base + "/v1/key"
}
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
} }
return request return request
} }
func imageVerifyURL(provider, baseURL, endpoint string) string {
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
return baseURL
}
return baseURL + endpoint
}
func verifyHTTPError(statusCode int, body []byte) string {
message := strings.TrimSpace(string(body))
var payload verifyErrorResponse
if err := json.Unmarshal(body, &payload); err == nil {
if value := strings.TrimSpace(payload.Error.Message); value != "" {
message = value
} else if value := strings.TrimSpace(payload.Message); value != "" {
message = value
}
}
if message == "" {
return fmt.Sprintf("validation request returned status %d", statusCode)
}
return message
}
func mustJSON(value interface{}) []byte { func mustJSON(value interface{}) []byte {
payload, err := json.Marshal(value) payload, err := json.Marshal(value)
if err != nil { if err != nil {
+2 -3
View File
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
Save(account *model.AgentAccount) error Save(account *model.AgentAccount) error
DeleteByID(id uint) error DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error) List(opts ...DBOption) ([]model.AgentAccount, error)
CountTextByProviders(providers []string) (map[string]int64, error) CountByProviders(providers []string) (map[string]int64, error)
} }
func NewIAgentAccountRepo() IAgentAccountRepo { func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
return accounts, nil return accounts, nil
} }
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) { func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
normalizedProviders := normalizeProviders(providers) normalizedProviders := normalizeProviders(providers)
counts := make(map[string]int64, len(normalizedProviders)) counts := make(map[string]int64, len(normalizedProviders))
for _, provider := range normalizedProviders { for _, provider := range normalizedProviders {
@@ -86,7 +86,6 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
Model(&model.AgentAccount{}). Model(&model.AgentAccount{}).
Select("provider, COUNT(*) as count"). Select("provider, COUNT(*) as count").
Where("provider IN ?", normalizedProviders). Where("provider IN ?", normalizedProviders).
Where("api_type NOT LIKE ?", "%-images").
Group("provider"). Group("provider").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return nil, err return nil, err
-7
View File
@@ -15,7 +15,6 @@ type IAppInstallResourceRpo interface {
WithAppInstallId(appInstallId uint) DBOption WithAppInstallId(appInstallId uint) DBOption
WithLinkId(linkId uint) DBOption WithLinkId(linkId uint) DBOption
WithResourceId(resourceId uint) DBOption WithResourceId(resourceId uint) DBOption
WithResourceIds(resourceIds []uint) DBOption
GetBy(opts ...DBOption) ([]model.AppInstallResource, error) GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
GetFirst(opts ...DBOption) (model.AppInstallResource, error) GetFirst(opts ...DBOption) (model.AppInstallResource, error)
Create(ctx context.Context, resource *model.AppInstallResource) error Create(ctx context.Context, resource *model.AppInstallResource) error
@@ -45,12 +44,6 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
} }
} }
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("resource_id IN ?", resourceIds)
}
}
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) { func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
db := global.DB.Model(&model.AppInstallResource{}) db := global.DB.Model(&model.AppInstallResource{})
var resources []model.AppInstallResource var resources []model.AppInstallResource
-18
View File
@@ -49,12 +49,6 @@ func WithByName(name string) DBOption {
} }
} }
func WithByPath(path string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("path = ?", path)
}
}
func WithByAddr(addr string) DBOption { func WithByAddr(addr string) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("addr = ?", addr) return g.Where("addr = ?", addr)
@@ -100,18 +94,6 @@ func WithByProvider(provider string) DBOption {
} }
} }
func WithByAPIType(apiType string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type = ?", apiType)
}
}
func WithTextAPIType() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type NOT LIKE ?", "%-images")
}
}
func WithByModel(model string) DBOption { func WithByModel(model string) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
if len(model) == 0 { if len(model) == 0 {
-121
View File
@@ -1,121 +0,0 @@
package repo
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)
type DatabaseUserRepo struct{}
type IDatabaseUserRepo interface {
Get(opts ...DBOption) (model.DatabaseUser, error)
List(opts ...DBOption) ([]model.DatabaseUser, error)
Save(user *model.DatabaseUser) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByUser(username, host string) DBOption
WithByUserList(users [][2]string) DBOption
}
func NewIDatabaseUserRepo() IDatabaseUserRepo {
return &DatabaseUserRepo{}
}
func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
var user model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.First(&user).Error; err != nil {
return user, err
}
password, err := encrypt.StringDecrypt(user.Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
return user, nil
}
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
var users []model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.Find(&users).Error; err != nil {
return users, err
}
for i := 0; i < len(users); i++ {
password, err := encrypt.StringDecrypt(users[i].Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", users[i].Username, err)
}
users[i].Password = password
}
return users, nil
}
func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
if len(user.Password) != 0 {
password, err := encrypt.StringEncrypt(user.Password)
if err != nil {
return fmt.Errorf("encrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
}
return global.DB.Save(user).Error
}
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserRepo) WithByDatabase(database string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("database = ?", database)
}
}
func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("username = ? AND host = ?", username, host)
}
}
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(users) == 0 {
return g.Where("1 = 0")
}
values := make([][]interface{}, 0, len(users))
for _, user := range users {
values = append(values, []interface{}{user[0], user[1]})
}
return g.Where("(username, host) IN ?", values)
}
}
-109
View File
@@ -1,109 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type DatabaseUserGrantRepo struct{}
type IDatabaseUserGrantRepo interface {
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
Save(grant *model.DatabaseUserGrant) error
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByDBName(dbName string) DBOption
WithByDBNames(dbNames []string) DBOption
WithByUser(username, host string) DBOption
}
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
var grant model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.First(&grant).Error
return grant, err
}
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
return &DatabaseUserGrantRepo{}
}
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
var grants []model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&grants).Error
return grants, err
}
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
return global.DB.Save(grant).Error
}
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
return err
}
if len(grants) != 0 {
return tx.Create(&grants).Error
}
return nil
})
}
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("database = ?", database)
}
}
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name = ?", dbName)
}
}
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name IN ?", dbNames)
}
}
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("username = ? AND host = ?", username, host)
}
}
-21
View File
@@ -17,12 +17,10 @@ type IWebsiteRepo interface {
WithGroupID(groupId uint) DBOption WithGroupID(groupId uint) DBOption
WithDefaultServer() DBOption WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption WithDomainLike(domain string) DBOption
WithSearchKeyword(keyword string, ids []uint) DBOption
WithRuntimeID(runtimeID uint) DBOption WithRuntimeID(runtimeID uint) DBOption
WithParentID(websiteID uint) DBOption WithParentID(websiteID uint) DBOption
WithType(websiteType string) DBOption WithType(websiteType string) DBOption
WithDBType(dbType string) DBOption WithDBType(dbType string) DBOption
WithDBTypes(dbTypes []string) DBOption
WithDBID(dbID uint) DBOption WithDBID(dbID uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.Website, error) Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
@@ -78,19 +76,6 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
} }
} }
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
if keyword == "" {
return db
}
keyword = "%" + keyword + "%"
if len(ids) == 0 {
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
}
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
}
}
func (w *WebsiteRepo) WithAlias(alias string) DBOption { func (w *WebsiteRepo) WithAlias(alias string) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("alias = ?", alias) return db.Where("alias = ?", alias)
@@ -133,12 +118,6 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
} }
} }
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_type IN ?", dbTypes)
}
}
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption { func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("db_id = ?", dbID) return db.Where("db_id = ?", dbID)
-131
View File
@@ -1,131 +0,0 @@
package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"gorm.io/gorm"
)
type IWebsiteTemplateRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error)
GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error)
List(opts ...DBOption) ([]model.WebsiteTemplate, error)
Create(template *model.WebsiteTemplate) error
Save(template *model.WebsiteTemplate) error
DeleteBy(opts ...DBOption) error
WithName(name string) DBOption
WithType(templateType string) DBOption
}
func NewIWebsiteTemplateRepo() IWebsiteTemplateRepo {
return &WebsiteTemplateRepo{}
}
type WebsiteTemplateRepo struct {
}
func (w *WebsiteTemplateRepo) WithName(name string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("name like ?", "%"+name+"%")
}
}
func (w *WebsiteTemplateRepo) WithType(templateType string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("type = ?", templateType)
}
}
func (w *WebsiteTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) {
var templates []model.WebsiteTemplate
db := getDb(opts...).Model(&model.WebsiteTemplate{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&templates).Error
return count, templates, err
}
func (w *WebsiteTemplateRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) {
var template model.WebsiteTemplate
db := getDb(opts...).Model(&model.WebsiteTemplate{})
if err := db.First(&template).Error; err != nil {
return nil, err
}
return &template, nil
}
func (w *WebsiteTemplateRepo) List(opts ...DBOption) ([]model.WebsiteTemplate, error) {
var templates []model.WebsiteTemplate
err := getDb(opts...).Model(&model.WebsiteTemplate{}).Find(&templates).Error
return templates, err
}
func (w *WebsiteTemplateRepo) Create(template *model.WebsiteTemplate) error {
return getDb().Create(template).Error
}
func (w *WebsiteTemplateRepo) Save(template *model.WebsiteTemplate) error {
return getDb().Save(template).Error
}
func (w *WebsiteTemplateRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.WebsiteTemplate{}).Error
}
type IWebsiteTemplateOutputRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error)
GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error)
List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error)
Create(output *model.WebsiteTemplateOutput) error
Save(output *model.WebsiteTemplateOutput) error
DeleteBy(opts ...DBOption) error
WithByTemplateID(templateID uint) DBOption
}
func NewIWebsiteTemplateOutputRepo() IWebsiteTemplateOutputRepo {
return &WebsiteTemplateOutputRepo{}
}
type WebsiteTemplateOutputRepo struct {
}
func (w *WebsiteTemplateOutputRepo) WithByTemplateID(templateID uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("template_id = ?", templateID)
}
}
func (w *WebsiteTemplateOutputRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) {
var outputs []model.WebsiteTemplateOutput
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&outputs).Error
return count, outputs, err
}
func (w *WebsiteTemplateOutputRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) {
var output model.WebsiteTemplateOutput
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
if err := db.First(&output).Error; err != nil {
return nil, err
}
return &output, nil
}
func (w *WebsiteTemplateOutputRepo) List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) {
var outputs []model.WebsiteTemplateOutput
err := getDb(opts...).Model(&model.WebsiteTemplateOutput{}).Find(&outputs).Error
return outputs, err
}
func (w *WebsiteTemplateOutputRepo) Create(output *model.WebsiteTemplateOutput) error {
return getDb().Create(output).Error
}
func (w *WebsiteTemplateOutputRepo) Save(output *model.WebsiteTemplateOutput) error {
return getDb().Save(output).Error
}
func (w *WebsiteTemplateOutputRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.WebsiteTemplateOutput{}).Error
}
+141 -280
View File
@@ -8,8 +8,8 @@ import (
"os" "os"
"path" "path"
"sort" "sort"
"strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,8 +30,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var agentAccountMutationMu sync.Mutex
type IAgentService interface { type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error) Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
@@ -79,7 +77,6 @@ type IAgentService interface {
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
@@ -105,10 +102,6 @@ type IAgentService interface {
UpgradePlugin(req dto.AgentPluginUpgradeReq) error UpgradePlugin(req dto.AgentPluginUpgradeReq) error
UninstallPlugin(req dto.AgentPluginUninstallReq) error UninstallPlugin(req dto.AgentPluginUninstallReq) error
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
OperatePlugin(req dto.AgentPluginOperateReq) error
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
} }
@@ -151,7 +144,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist") return nil, buserr.New("ErrNameIsExist")
} }
if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() { if !xpack.MultiNodeProvider.IsXpack() {
count, _, err := agentRepo.Page(1, 1) count, _, err := agentRepo.Page(1, 1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -172,6 +165,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
provider := "" provider := ""
baseURL := "" baseURL := ""
apiType := "" apiType := ""
maxTokens := 0
contextWindow := 0
apiKey := "" apiKey := ""
runtimeModel := "" runtimeModel := ""
accountID := uint(0) accountID := uint(0)
@@ -181,7 +176,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
var allowedOrigins []string var allowedOrigins []string
var account *model.AgentAccount var account *model.AgentAccount
var installHooks *appInstallHooks var installHooks *appInstallHooks
var dashboardAuth agentDashboardAuth
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent { if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
if req.AccountID == 0 { if req.AccountID == 0 {
@@ -202,6 +196,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
} }
storedModel = resolvedRuntime.StoredModel storedModel = resolvedRuntime.StoredModel
apiType = resolvedRuntime.APIType apiType = resolvedRuntime.APIType
maxTokens = resolvedRuntime.MaxTokens
contextWindow = resolvedRuntime.ContextWindow
runtimeModel = resolvedRuntime.PrimaryModel runtimeModel = resolvedRuntime.PrimaryModel
apiKey = account.APIKey apiKey = account.APIKey
accountID = account.ID accountID = account.ID
@@ -226,17 +222,11 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}, },
} }
} else if agentType == constant.AppHermesAgent { } else if agentType == constant.AppHermesAgent {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
installHooks = &appInstallHooks{ installHooks = &appInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error { AfterCopyData: func(appInstall *model.AppInstall) error {
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil { return prepareHermesInstallFiles(appInstall, account, storedModel)
return err
}
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
}, },
} }
} else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} }
params := map[string]interface{}{ params := map[string]interface{}{
@@ -252,17 +242,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params["PROVIDER"] = provider params["PROVIDER"] = provider
params["MODEL"] = runtimeModel params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType params["API_TYPE"] = apiType
params["MAX_TOKENS"] = maxTokens
params["CONTEXT_WINDOW"] = contextWindow
params["BASE_URL"] = baseURL params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token params["OPENCLAW_GATEWAY_TOKEN"] = token
} }
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
params[usernameKey] = dashboardAuth.Username
params[passwordKey] = dashboardAuth.Password
if agentType == constant.AppCopaw {
params[qwenPawAuthEnabledEnvKey] = "true"
}
}
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
return nil, buserr.New("ErrAgentComposeRequired") return nil, buserr.New("ErrAgentComposeRequired")
@@ -297,20 +282,22 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml") configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
} }
agent := &model.Agent{ agent := &model.Agent{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AgentType: agentType, AgentType: agentType,
Provider: provider, Provider: provider,
Model: storedModel, Model: storedModel,
APIType: apiType, APIType: apiType,
BaseURL: baseURL, MaxTokens: maxTokens,
APIKey: apiKey, ContextWindow: contextWindow,
Token: token, BaseURL: baseURL,
Status: appInstall.Status, APIKey: apiKey,
Message: appInstall.Message, Token: token,
AppInstallID: appInstall.ID, Status: appInstall.Status,
AccountID: accountID, Message: appInstall.Message,
ConfigPath: configPath, AppInstallID: appInstall.ID,
AccountID: accountID,
ConfigPath: configPath,
} }
if err := agentRepo.Create(agent); err != nil { if err := agentRepo.Create(agent); err != nil {
return nil, err return nil, err
@@ -408,7 +395,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
results = append(results, result) results = append(results, result)
continue continue
} }
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID) installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
results = append(results, result) results = append(results, result)
@@ -450,7 +437,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if operate == constant.Delete { if operate == constant.Delete {
if err := a.Delete(dto.AgentDeleteReq{ if err := a.Delete(dto.AgentDeleteReq{
ID: agent.ID, ID: agent.ID,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
@@ -478,7 +465,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{ if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: operate, Operate: operate,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {
@@ -562,19 +549,35 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
DetailID: detail.ID, DetailID: detail.ID,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID), TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
}, },
}) })
} }
return plans, results, nil return plans, results, nil
} }
func buildBatchTaskID(taskID, prefix string, id uint) string { func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
taskID = strings.TrimSpace(taskID) taskID = strings.TrimSpace(taskID)
if taskID == "" { if taskID == "" {
taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano()) taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
} }
return fmt.Sprintf("%s-%d", taskID, id) return fmt.Sprintf("%s-%d", taskID, appInstallID)
}
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func buildBatchOperateTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
} }
func batchOperateSkipMessage(operate constant.AppOperate, status string) string { func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
@@ -601,30 +604,28 @@ func batchOperateSkipMessage(operate constant.AppOperate, status string) string
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq { func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
return dto.AgentCreateReq{ return dto.AgentCreateReq{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AppVersion: req.AppVersion, AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort, WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort, BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins, AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType, AgentType: req.AgentType,
Model: req.Model, Model: req.Model,
AccountID: req.AccountID, AccountID: req.AccountID,
Token: req.Token, Token: req.Token,
DashboardUsername: req.DashboardUsername, TaskID: req.TaskID,
DashboardPassword: req.DashboardPassword, Advanced: req.Advanced,
TaskID: req.TaskID, ContainerName: req.ContainerName,
Advanced: req.Advanced, AllowPort: req.AllowPort,
ContainerName: req.ContainerName, SpecifyIP: req.SpecifyIP,
AllowPort: req.AllowPort, RestartPolicy: req.RestartPolicy,
SpecifyIP: req.SpecifyIP, CpuQuota: req.CpuQuota,
RestartPolicy: req.RestartPolicy, MemoryLimit: req.MemoryLimit,
CpuQuota: req.CpuQuota, MemoryUnit: req.MemoryUnit,
MemoryLimit: req.MemoryLimit, PullImage: req.PullImage,
MemoryUnit: req.MemoryUnit, EditCompose: req.EditCompose,
PullImage: req.PullImage, DockerCompose: req.DockerCompose,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
} }
} }
@@ -703,7 +704,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
account.RememberAPIKey = snapshot.RememberAPIKey account.RememberAPIKey = snapshot.RememberAPIKey
account.BaseURL = snapshot.BaseURL account.BaseURL = snapshot.BaseURL
account.APIType = snapshot.APIType account.APIType = snapshot.APIType
account.AuthMode = snapshot.AuthMode
account.Remark = snapshot.Remark account.Remark = snapshot.Remark
account.Verified = true account.Verified = true
@@ -711,11 +711,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
if err != nil { if err != nil {
return 0, err return 0, err
} }
verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
if err != nil {
return 0, err
}
account.VerifyModel = verifyModel
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if account.ID == 0 { if account.ID == 0 {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
@@ -953,7 +948,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
cfg, err := readHermesConfig(agent.ConfigPath) cfg, err := readHermesConfig(agent.ConfigPath)
@@ -974,6 +969,8 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
agent.BaseURL = account.BaseURL agent.BaseURL = account.BaseURL
agent.APIKey = account.APIKey agent.APIKey = account.APIKey
agent.AccountID = account.ID agent.AccountID = account.ID
@@ -987,38 +984,20 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
models := make([]dto.ProviderModelInfo, 0, len(def.Models)) models := make([]dto.ProviderModelInfo, 0, len(def.Models))
for _, item := range def.Models { for _, item := range def.Models {
models = append(models, dto.ProviderModelInfo{ models = append(models, dto.ProviderModelInfo{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
for _, item := range def.APIConfigs {
apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
for _, model := range item.Models {
apiModels = append(apiModels, dto.ProviderModelInfo{
ID: model.ID,
Name: model.Name,
})
}
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
APIType: item.APIType,
BaseURL: item.BaseURL,
EditableBaseURL: item.EditableBaseURL,
SupportsModelDiscovery: item.DiscoverModels,
DefaultAuthMode: item.DefaultAuthMode,
AuthModes: item.AuthModes,
Models: apiModels,
})
}
baseURL, _ := providercatalog.DefaultBaseURL(key)
providers = append(providers, dto.ProviderInfo{ providers = append(providers, dto.ProviderInfo{
Sort: def.Sort, Sort: def.Sort,
Provider: key, Provider: key,
DisplayName: localizedAgentProviderName(key), DisplayName: def.DisplayName,
BaseURL: baseURL, BaseURL: def.DefaultBaseURL,
DefaultAPIType: def.DefaultAPIType, Models: models,
APITypes: apiTypes,
Models: models,
}) })
} }
sort.Slice(providers, func(i, j int) bool { sort.Slice(providers, func(i, j int) bool {
@@ -1028,22 +1007,11 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
} }
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
provider := req.Provider provider := req.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil { if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
return err return buserr.New("ErrRecordExist")
} }
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models) resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
if err != nil {
return err
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
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)
if err != nil { if err != nil {
return err return err
} }
@@ -1053,12 +1021,14 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
APIKey: resolvedInput.APIKey, APIKey: resolvedInput.APIKey,
RememberAPIKey: req.RememberAPIKey, RememberAPIKey: req.RememberAPIKey,
BaseURL: resolvedInput.BaseURL, BaseURL: resolvedInput.BaseURL,
APIType: resolvedInput.APIType, APIType: req.APIType,
AuthMode: resolvedInput.AuthMode,
VerifyModel: verifyModel,
Verified: true, Verified: true,
Remark: req.Remark, Remark: req.Remark,
} }
initialModels, err := buildInitialAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
return err return err
@@ -1075,33 +1045,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
} }
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
} }
if req.APIType != account.APIType {
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
}
provider := account.Provider provider := account.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil { resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
return err
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
}
requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" {
requestedVerifyModel = account.VerifyModel
}
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)
if err != nil { if err != nil {
return err return err
} }
@@ -1109,9 +1058,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.APIKey = resolvedInput.APIKey account.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey account.RememberAPIKey = req.RememberAPIKey
account.BaseURL = resolvedInput.BaseURL account.BaseURL = resolvedInput.BaseURL
account.APIType = resolvedInput.APIType account.APIType = req.APIType
account.AuthMode = resolvedInput.AuthMode
account.VerifyModel = verifyModel
account.Remark = req.Remark account.Remark = req.Remark
account.Verified = true account.Verified = true
@@ -1133,12 +1080,6 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
if strings.TrimSpace(req.Provider) != "" { if strings.TrimSpace(req.Provider) != "" {
opts = append(opts, repo.WithByProvider(req.Provider)) opts = append(opts, repo.WithByProvider(req.Provider))
} }
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
opts = append(opts, repo.WithByAPIType(apiType))
}
if req.TextOnly {
opts = append(opts, repo.WithTextAPIType())
}
if strings.TrimSpace(req.Name) != "" { if strings.TrimSpace(req.Name) != "" {
opts = append(opts, repo.WithByLikeName(req.Name)) opts = append(opts, repo.WithByLikeName(req.Name))
} }
@@ -1156,46 +1097,30 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
ID: item.ID, ID: item.ID,
MasterAccountID: item.MasterAccountID, MasterAccountID: item.MasterAccountID,
Provider: item.Provider, Provider: item.Provider,
ProviderName: localizedAgentProviderName(item.Provider), ProviderName: providercatalog.DisplayName(item.Provider),
Name: item.Name, Name: item.Name,
APIKey: apiKey, APIKey: apiKey,
RememberAPIKey: item.RememberAPIKey, RememberAPIKey: item.RememberAPIKey,
BaseURL: item.BaseURL, BaseURL: item.BaseURL,
Models: nil, Models: nil,
APIType: item.APIType, APIType: item.APIType,
AuthMode: item.AuthMode,
VerifyModel: item.VerifyModel,
Verified: item.Verified, Verified: item.Verified,
Remark: item.Remark, Remark: item.Remark,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
}) })
} }
if len(list) > 0 { for i := range items {
accountIDs := make([]uint, 0, len(list)) models, err := loadAgentAccountModels(&list[i])
for _, account := range list { if err != nil {
accountIDs = append(accountIDs, account.ID)
}
var rows []model.AgentAccountModel
if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
return 0, nil, err return 0, nil, err
} }
modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list)) items[i].Models = models
for _, row := range rows {
modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
})
}
for index, account := range list {
items[index].Models = modelsByAccount[account.ID]
}
} }
return count, items, nil return count, items, nil
} }
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) { func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
return agentAccountRepo.CountTextByProviders(req.Providers) return agentAccountRepo.CountByProviders(req.Providers)
} }
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) { func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
@@ -1206,22 +1131,6 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
return loadAgentAccountModels(account) return loadAgentAccountModels(account)
} }
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
if !ok || !config.DiscoverModels {
return nil, buserr.New("ErrAgentAccountModelsRequired")
}
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
return buildDiscoveredAgentAccountModels(models), nil
}
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil { if err != nil {
@@ -1231,19 +1140,24 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel, err := normalizeAgentAccountModel(account, req.Model) nextModel := cloneAgentAccountModel(req.Model)
if err != nil {
return err
}
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
} }
inputPayload, err := json.Marshal(nextModel.Input)
if err != nil {
return err
}
sortOrder := len(models) + 1 sortOrder := len(models) + 1
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: account.ID, AccountID: account.ID,
Model: nextModel.ID, Model: nextModel.ID,
Name: nextModel.Name, Name: nextModel.Name,
SortOrder: sortOrder, ContextWindow: nextModel.ContextWindow,
MaxTokens: nextModel.MaxTokens,
Reasoning: nextModel.Reasoning,
Input: string(inputPayload),
SortOrder: sortOrder,
} }
if err := agentAccountModelRepo.Create(record); err != nil { if err := agentAccountModelRepo.Create(record); err != nil {
return err return err
@@ -1264,11 +1178,7 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel, err := normalizeAgentAccountModel(account, req.Model) nextModel := cloneAgentAccountModel(req.Model)
if err != nil {
return err
}
nextModel.RecordID = req.Model.RecordID
for _, item := range models { for _, item := range models {
if item.RecordID == req.Model.RecordID { if item.RecordID == req.Model.RecordID {
continue continue
@@ -1288,17 +1198,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil { if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
return err return err
} }
previousModelID := record.Model inputPayload, err := json.Marshal(nextModel.Input)
record.Model = nextModel.ID if err != nil {
record.Name = nextModel.Name
if err := agentAccountModelRepo.Save(record); err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) { record.Model = nextModel.ID
account.VerifyModel = nextModel.ID record.Name = nextModel.Name
if err := agentAccountRepo.Save(account); err != nil { record.ContextWindow = nextModel.ContextWindow
return err record.MaxTokens = nextModel.MaxTokens
} record.Reasoning = nextModel.Reasoning
record.Input = string(inputPayload)
if err := agentAccountModelRepo.Save(record); err != nil {
return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
@@ -1310,13 +1221,9 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err != nil { if err != nil {
return err return err
} }
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)) if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil {
if err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
return buserr.New("ErrAgentVerifyModelInUse")
}
models, err := loadAgentAccountModels(account) models, err := loadAgentAccountModels(account)
if err != nil { if err != nil {
return err return err
@@ -1350,45 +1257,25 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
} }
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true) _, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL)
return err return err
} }
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
agentAccountMutationMu.Lock() if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
defer agentAccountMutationMu.Unlock() return buserr.New("ErrAgentAccountBound")
if err := global.DB.Transaction(func(tx *gorm.DB) error { }
var agentCount int64 if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil { if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
return err
}
if agentCount > 0 {
return buserr.New("ErrAgentAccountBound")
}
used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
if err != nil {
return err
}
if used {
return buserr.New("ErrTerminalAIAccountInUse") return buserr.New("ErrTerminalAIAccountInUse")
} }
used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID") }
if err != nil { if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
return err
}
if used {
return buserr.New("ErrFileAIAccountInUse")
}
if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
return err
}
return tx.Delete(&model.AgentAccount{}, req.ID).Error
}); err != nil {
return err return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
return nil return agentAccountRepo.DeleteByID(req.ID)
} }
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) { func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
@@ -1440,20 +1327,10 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
if err != nil { if err != nil {
return nil, err return nil, err
} }
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
return &dto.AgentOtherConfig{ return &dto.AgentOtherConfig{
UserTimezone: cfg.Timezone, UserTimezone: cfg.Timezone,
BrowserEnabled: true, BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/", NPMRegistry: "https://registry.npmjs.org/",
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil
}
if agent.AgentType == constant.AppCopaw {
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
return &dto.AgentOtherConfig{
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil }, nil
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -1474,36 +1351,18 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
return err return err
} }
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
if strings.TrimSpace(req.UserTimezone) == "" {
return buserr.New("ErrInvalidParams")
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
if err != nil { if err != nil {
return err return err
} }
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil { if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
return err return err
} }
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
return err
}
operate := constant.Restart
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
operate = constant.Rebuild
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{ return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: operate, Operate: constant.Restart,
}) })
} }
if agent.AgentType == constant.AppCopaw {
return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
}
if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
return buserr.New("ErrInvalidParams")
}
if err := ensureContainerRunning(install.ContainerName); err != nil { if err := ensureContainerRunning(install.ContainerName); err != nil {
return err return err
} }
@@ -1675,7 +1534,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
switch agent.AgentType { switch agent.AgentType {
case constant.AppOpenclaw: case constant.AppOpenclaw:
@@ -1703,6 +1562,8 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
_ = agentRepo.Save(&agent) _ = agentRepo.Save(&agent)
} }
return nil return nil
+6 -27
View File
@@ -811,7 +811,9 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
result.RequireMention = result.GroupPolicy == "allowlist" result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"]) result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"]) result.Proxy = extractStringValue(telegram["proxy"])
result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming) if streaming := extractStringValue(telegram["streaming"]); streaming != "" {
result.Streaming = streaming
}
accounts := childMap(telegram, "accounts") accounts := childMap(telegram, "accounts")
if len(accounts) == 0 { if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"]) botToken := extractStringValue(telegram["botToken"])
@@ -837,7 +839,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
BotToken: extractStringValue(account["botToken"]), BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]), DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]), GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming), Streaming: extractStringValue(account["streaming"]),
}) })
} }
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots)) result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
@@ -877,7 +879,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
} else { } else {
delete(telegram, "proxy") delete(telegram, "proxy")
} }
telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming) telegram["streaming"] = config.Streaming
accounts := make(map[string]interface{}, len(config.Bots)) accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots { for _, bot := range config.Bots {
account := map[string]interface{}{ account := map[string]interface{}{
@@ -886,7 +888,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
"botToken": bot.BotToken, "botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy, "dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy, "groupPolicy": bot.GroupPolicy,
"streaming": buildTelegramStreamingConfig(bot.Streaming), "streaming": bot.Streaming,
} }
if bot.DmPolicy == "open" { if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"} account["allowFrom"] = []string{"*"}
@@ -897,29 +899,6 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
delete(telegram, "botToken") delete(telegram, "botToken")
} }
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"} result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord") discord := getChannelConfig(conf, "discord")
-128
View File
@@ -1,128 +0,0 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
type qwenPawAuthStatus struct {
Enabled bool `json:"enabled"`
HasUsers bool `json:"has_users"`
}
type qwenPawLoginResponse struct {
Token string `json:"token"`
}
func updateQwenPawDashboardAuth(install *model.AppInstall, next agentDashboardAuth) error {
if install == nil || install.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
current, err := readAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw)
if err != nil {
return err
}
if current == next {
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d/api/auth", install.HttpPort)
var status qwenPawAuthStatus
if _, err := requestQwenPawAuth(http.MethodGet, baseURL+"/status", nil, "", &status); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
if !status.Enabled {
return buserr.New("ErrQwenPawAuthDisabled")
}
if !status.HasUsers {
payload := map[string]string{"username": next.Username, "password": next.Password}
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/register", payload, "", nil); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
} else {
var login qwenPawLoginResponse
payload := map[string]string{"username": current.Username, "password": current.Password}
statusCode, err := requestQwenPawAuth(http.MethodPost, baseURL+"/login", payload, "", &login)
if statusCode == http.StatusUnauthorized {
return buserr.New("ErrQwenPawAuthOutOfSync")
}
if err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
payload = map[string]string{"current_password": current.Password}
if current.Username != next.Username {
payload["new_username"] = next.Username
}
if current.Password != next.Password {
payload["new_password"] = next.Password
}
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/update-profile", payload, login.Token, nil); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
}
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
}
func requestQwenPawAuth(method, reqURL string, payload interface{}, token string, result interface{}) (int, error) {
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return 0, err
}
body = bytes.NewReader(data)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return resp.StatusCode, err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
detail := strings.TrimSpace(string(data))
var errorResponse struct {
Detail string `json:"detail"`
}
if json.Unmarshal(data, &errorResponse) == nil && strings.TrimSpace(errorResponse.Detail) != "" {
detail = strings.TrimSpace(errorResponse.Detail)
}
if detail == "" {
detail = resp.Status
}
return resp.StatusCode, errors.New(detail)
}
if result != nil && len(data) > 0 {
if err := json.Unmarshal(data, result); err != nil {
return resp.StatusCode, err
}
}
return resp.StatusCode, nil
}
+68 -31
View File
@@ -13,7 +13,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -69,7 +71,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
} }
} }
provider := resolveHermesProvider(account.Provider, account.APIType) provider := resolveHermesProvider(account.Provider)
configPath := path.Join(confDir, "config.yaml") configPath := path.Join(confDir, "config.yaml")
cfg, err := readHermesConfigMap(configPath) cfg, err := readHermesConfigMap(configPath)
if err != nil { if err != nil {
@@ -150,7 +152,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
} }
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) { func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -207,7 +209,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error { func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -227,7 +229,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
envMap["TELEGRAM_ALLOWED_USERS"] = allow envMap["TELEGRAM_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS", "TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS", "TELEGRAM_ALLOW_ALL_USERS",
@@ -246,7 +248,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
} }
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) { func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -298,7 +300,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error { func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -318,7 +320,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
envMap["DISCORD_ALLOWED_USERS"] = allow envMap["DISCORD_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"DISCORD_BOT_TOKEN", "DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS", "DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS", "DISCORD_ALLOW_ALL_USERS",
@@ -338,14 +340,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
func deleteHermesEnvKeys(confDir string, keys ...string) error { func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
for _, key := range keys { for _, key := range keys {
delete(envMap, key) delete(envMap, key)
} }
return writeAgentEnvMap(envPath, envMap, keys) return writeHermesEnvMap(envPath, envMap, keys)
} }
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error { func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
@@ -405,18 +407,11 @@ func normalizeHermesTimezone(timezone string) string {
return timezone return timezone
} }
func resolveHermesProvider(provider, apiType string) string { func resolveHermesProvider(provider string) string {
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
return "anthropic"
}
switch provider { switch provider {
case "": case "":
return "custom" return "custom"
case "moonshot": case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
return "kimi-coding"
case "kimi":
return "kimi-coding-cn"
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
return provider return provider
case "minimax": case "minimax":
return "minimax-cn" return "minimax-cn"
@@ -433,7 +428,17 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
if targetProvider != "custom" { if targetProvider != "custom" {
return target return target
} }
return providercatalog.NormalizeModelID(sourceProvider, target) if sourceProvider == "custom" || sourceProvider == "vllm" {
return normalizeCustomModel(target)
}
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
model := strings.TrimSpace(parts[1])
if model != "" {
return model
}
}
return target
} }
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) { func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
@@ -444,7 +449,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
if configuredModel == "" { if configuredModel == "" {
return "", buserr.New("ErrAgentModelNotInAccount") return "", buserr.New("ErrAgentModelNotInAccount")
} }
provider := resolveHermesProvider(account.Provider, account.APIType) provider := resolveHermesProvider(account.Provider)
for _, item := range accountModels { for _, item := range accountModels {
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel { if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
return item.ID, nil return item.ID, nil
@@ -457,8 +462,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil { if account == nil {
return nil return nil
} }
resolvedProvider := resolveHermesProvider(account.Provider, account.APIType) if resolveHermesProvider(account.Provider) == "custom" {
if resolvedProvider == "custom" {
if account.APIKey == "" { if account.APIKey == "" {
return nil return nil
} }
@@ -476,11 +480,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
entries = append(entries, hermesEnvEntry{Key: key, Value: value}) entries = append(entries, hermesEnvEntry{Key: key, Value: value})
} }
if resolvedProvider == "anthropic" {
appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
return entries
}
switch account.Provider { switch account.Provider {
case "openrouter": case "openrouter":
@@ -488,7 +487,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
appendEntry("OPENROUTER_BASE_URL", baseURL) appendEntry("OPENROUTER_BASE_URL", baseURL)
case "anthropic": case "anthropic":
appendEntry("ANTHROPIC_API_KEY", apiKey) appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
case "gemini": case "gemini":
appendEntry("GOOGLE_API_KEY", apiKey) appendEntry("GOOGLE_API_KEY", apiKey)
appendEntry("GEMINI_API_KEY", apiKey) appendEntry("GEMINI_API_KEY", apiKey)
@@ -534,7 +532,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error { func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -550,7 +548,7 @@ func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap[entry.Key] = entry.Value envMap[entry.Key] = entry.Value
order = append(order, entry.Key) order = append(order, entry.Key)
} }
return writeAgentEnvMap(envPath, envMap, order) return writeHermesEnvMap(envPath, envMap, order)
} }
func hermesManagedModelEnvKeys() []string { func hermesManagedModelEnvKeys() []string {
@@ -558,7 +556,6 @@ func hermesManagedModelEnvKeys() []string {
"OPENROUTER_API_KEY", "OPENROUTER_API_KEY",
"OPENROUTER_BASE_URL", "OPENROUTER_BASE_URL",
"ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"GOOGLE_API_KEY", "GOOGLE_API_KEY",
"GEMINI_API_KEY", "GEMINI_API_KEY",
"GEMINI_BASE_URL", "GEMINI_BASE_URL",
@@ -601,6 +598,46 @@ func hermesManagedModelEnvKeys() []string {
return result return result
} }
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error {
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, entry := range entries {
if entry.Key == "" || entry.Value == "" {
continue
}
envMap[entry.Key] = entry.Value
}
order := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Key == "" {
continue
}
order = append(order, entry.Key)
}
return writeHermesEnvMap(envPath, envMap, order)
}
func readHermesEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeHermesEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func splitHermesEnvList(value string) []string { func splitHermesEnvList(value string) []string {
if value == "" { if value == "" {
return []string{} return []string{}
+13 -13
View File
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
` `
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) { func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error { func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
envMap["QQ_GROUP_ALLOWED_USERS"] = allow envMap["QQ_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"QQ_APP_ID", "QQ_APP_ID",
"QQ_CLIENT_SECRET", "QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS", "QQ_ALLOW_ALL_USERS",
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
} }
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) { func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error { func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"WECOM_BOT_ID", "WECOM_BOT_ID",
"WECOM_SECRET", "WECOM_SECRET",
"WECOM_DM_POLICY", "WECOM_DM_POLICY",
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
} }
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) { func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error { func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
} else if config.DmPolicy == "open" { } else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true" envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET", "DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS", "DINGTALK_ALLOW_ALL_USERS",
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
} }
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) { func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error { func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
envMap["FEISHU_ALLOWED_USERS"] = allow envMap["FEISHU_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"FEISHU_APP_ID", "FEISHU_APP_ID",
"FEISHU_APP_SECRET", "FEISHU_APP_SECRET",
"FEISHU_DOMAIN", "FEISHU_DOMAIN",
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
} }
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) { func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
-315
View File
@@ -1,315 +0,0 @@
package service
import (
"database/sql"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"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/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
var (
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
)
type openclawPluginListOutput struct {
Plugins []struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
} `json:"plugins"`
}
type openclawPluginIndexItem struct {
PluginID string `json:"pluginId"`
PackageName string `json:"packageName"`
PackageVersion string `json:"packageVersion"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type openclawPluginSearchOutput struct {
Results []struct {
Score float64 `json:"score"`
Package struct {
Name string `json:"name"`
RuntimeID string `json:"runtimeId"`
DisplayName string `json:"displayName"`
Summary string `json:"summary"`
LatestVersion string `json:"latestVersion"`
Categories []string `json:"categories"`
Channel string `json:"channel"`
IsOfficial bool `json:"isOfficial"`
VerificationTier string `json:"verificationTier"`
Stats struct {
Downloads int64 `json:"downloads"`
} `json:"stats"`
} `json:"package"`
} `json:"results"`
}
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
return plugins, nil
}
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
if err != nil {
return nil, err
}
return parseOpenclawPluginList([]byte(output))
}
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
limit := req.Limit
if limit == 0 {
limit = 20
}
output, err := cmd.RunDockerExecWithStdout(
2*time.Minute,
install.ContainerName,
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
)
if err != nil {
return nil, err
}
return parseOpenclawPluginSearch([]byte(output))
}
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
if err != nil {
return err
}
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask(taskName, func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", "install", spec)
}, nil)
addOpenclawPluginRestartTask(installTask, install)
go executeAgentPluginTask(installTask)
return nil
}
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
if !openclawPluginIDPattern.MatchString(req.PluginID) {
return buserr.New("ErrInvalidChar")
}
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if req.Operate == "update" || req.Operate == "uninstall" {
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
if err != nil {
return err
}
for _, plugin := range plugins {
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
return buserr.WithName("ErrNotSupportType", req.Operate)
}
}
}
taskType := map[string]string{
"enable": task.TaskUpdate,
"disable": task.TaskUpdate,
"update": task.TaskUpgrade,
"uninstall": task.TaskUninstall,
}[req.Operate]
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
"enable": "AgentPluginEnable",
"disable": "AgentPluginDisable",
"update": "AgentPluginUpdate",
"uninstall": "AgentPluginUninstall",
}[req.Operate]), req.PluginID)
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
operateTask.AddSubTask(taskName, func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Operate == "uninstall" {
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
return err
}
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
}
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
}, nil)
addOpenclawPluginRestartTask(operateTask, install)
go executeAgentPluginTask(operateTask)
return nil
}
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginItem{}, nil
}
var output openclawPluginListOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
for _, plugin := range output.Plugins {
items = append(items, dto.AgentPluginItem{
ID: plugin.ID,
Name: plugin.Name,
Version: plugin.Version,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
if err != nil {
return nil, err
}
defer db.Close()
var raw []byte
if err := db.QueryRow(
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
"installed-plugin-index",
).Scan(&raw); err != nil {
return nil, err
}
var plugins []openclawPluginIndexItem
if err := json.Unmarshal(raw, &plugins); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(plugins))
for _, plugin := range plugins {
name := plugin.PackageName
if name == "" {
name = plugin.PluginID
}
items = append(items, dto.AgentPluginItem{
ID: plugin.PluginID,
Name: name,
Version: plugin.PackageVersion,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginSearchItem{}, nil
}
var output openclawPluginSearchOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
for _, result := range output.Results {
items = append(items, dto.AgentPluginSearchItem{
Package: result.Package.Name,
PluginID: result.Package.RuntimeID,
Name: result.Package.DisplayName,
Description: result.Package.Summary,
Version: result.Package.LatestVersion,
Channel: result.Package.Channel,
VerificationTier: result.Package.VerificationTier,
Categories: append([]string{}, result.Package.Categories...),
Official: result.Package.IsOfficial,
Downloads: result.Package.Stats.Downloads,
Score: result.Score,
})
}
return items, nil
}
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
packageName = strings.TrimSpace(packageName)
version = strings.TrimSpace(version)
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
return "", buserr.New("ErrInvalidChar")
}
return "clawhub:" + packageName + "@" + version, nil
}
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
pluginType := map[string]string{
"openclaw-lark": "feishu",
"openclaw-qqbot": "qqbot",
"wecom-openclaw-plugin": "wecom",
"dingtalk-connector": "dingtalk",
"openclaw-weixin": "weixin",
}[pluginID]
if pluginType == "" {
return nil
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
cleanupOpenclawPluginConfig(conf, pluginType)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
output, err := compose.Restart(install.GetComposePath())
if output != "" {
t.Log(output)
}
return err
}, nil)
}
func executeAgentPluginTask(t *task.Task) {
if err := t.Execute(); err != nil {
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
}
}
+272 -276
View File
@@ -4,12 +4,10 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -21,12 +19,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -40,39 +35,6 @@ type resolvedAgentAccountInput struct {
Provider string Provider string
APIKey string APIKey string
BaseURL string BaseURL string
APIType string
AuthMode string
}
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
if excludeID > 0 {
opts = append(opts, repo.WithByNOTID(excludeID))
}
account, err := agentAccountRepo.GetFirst(opts...)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if account != nil && account.ID > 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
var settings []model.Setting
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
return false, err
}
values := make(map[string]string, len(settings))
for _, setting := range settings {
values[setting.Key] = setting.Value
}
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
} }
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) { func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
@@ -97,27 +59,19 @@ func ensureContainerRunning(containerName string) error {
return nil return nil
} }
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) { func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey) resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedAPIType := strings.TrimSpace(apiType) resolvedBaseURL := strings.TrimSpace(baseURL)
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode) if resolvedBaseURL == "" {
if err != nil { if requiresInitialAgentAccountModels(provider) {
return resolvedAgentAccountInput{}, err
}
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
if err != nil {
if strings.Contains(err.Error(), "base url is required") {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired") return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
} }
return resolvedAgentAccountInput{}, err if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
resolvedBaseURL = defaultURL
}
} }
modelID = strings.TrimSpace(modelID) if !providercatalog.SkipVerification(provider) {
if modelID == "" { if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
}
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || !providercatalog.SkipVerification(provider)) {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err return resolvedAgentAccountInput{}, err
} }
} }
@@ -125,8 +79,6 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
Provider: provider, Provider: provider,
APIKey: resolvedAPIKey, APIKey: resolvedAPIKey,
BaseURL: resolvedBaseURL, BaseURL: resolvedBaseURL,
APIType: resolvedAPIType,
AuthMode: resolvedAuthMode,
}, nil }, nil
} }
@@ -383,24 +335,26 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := agent.AgentType agentType := agent.AgentType
item := dto.AgentItem{ item := dto.AgentItem{
ID: agent.ID, ID: agent.ID,
Name: agent.Name, Name: agent.Name,
Remark: agent.Remark, Remark: agent.Remark,
AgentType: agentType, AgentType: agentType,
Provider: agent.Provider, Provider: agent.Provider,
ProviderName: localizedAgentProviderName(agent.Provider), ProviderName: providercatalog.DisplayName(agent.Provider),
Model: agent.Model, Model: agent.Model,
APIType: agent.APIType, APIType: agent.APIType,
BaseURL: agent.BaseURL, MaxTokens: agent.MaxTokens,
APIKey: maskKey(agent.APIKey), ContextWindow: agent.ContextWindow,
Token: agent.Token, BaseURL: agent.BaseURL,
Status: agent.Status, APIKey: maskKey(agent.APIKey),
Message: agent.Message, Token: agent.Token,
AppInstallID: agent.AppInstallID, Status: agent.Status,
WebsiteID: agent.WebsiteID, Message: agent.Message,
AccountID: agent.AccountID, AppInstallID: agent.AppInstallID,
ConfigPath: agent.ConfigPath, WebsiteID: agent.WebsiteID,
CreatedAt: agent.CreatedAt, AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
} }
if appInstall != nil && appInstall.ID > 0 { if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName item.Container = appInstall.ContainerName
@@ -422,24 +376,10 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
item.BridgePort = toInt(bridge) item.BridgePort = toInt(bridge)
} }
} }
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
} }
return item return item
} }
func localizedAgentProviderName(provider string) string {
if key := providercatalog.DisplayNameKey(provider); key != "" {
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
return name
}
}
return providercatalog.DisplayName(provider)
}
func isAgentAppKey(appKey string) bool { func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
} }
@@ -737,9 +677,13 @@ type modelProvider struct {
} }
type modelEntry struct { type modelEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Input []string `json:"input,omitempty"` Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Cost modelCost `json:"cost"`
} }
func requiresOpenclawProviderModels(provider string) bool { func requiresOpenclawProviderModels(provider string) bool {
@@ -759,6 +703,13 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
return nil return nil
} }
type modelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cacheRead"`
CacheWrite float64 `json:"cacheWrite"`
}
type browserConfig struct { type browserConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"` ExecutablePath string `json:"executablePath"`
@@ -909,15 +860,13 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
envMap := map[string]string{ envPath := path.Join(confDir, ".env")
"OPENCLAW_GATEWAY_TOKEN": token, lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
}
order := []string{"OPENCLAW_GATEWAY_TOKEN"}
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" { if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
order = append(order, envKey)
} }
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order) content := strings.Join(lines, "\n") + "\n"
return fileOp.SaveFile(envPath, content, 0600)
} }
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
@@ -1104,7 +1053,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
} }
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey) providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey)
if err != nil { if err != nil {
return "", modelEntry{}, "", modelProvider{}, err return "", modelEntry{}, "", modelProvider{}, err
} }
@@ -1116,24 +1065,28 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
}, nil }, nil
} }
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry { func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = strings.TrimSpace(modelID) name = strings.TrimSpace(modelID)
} }
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name} return modelEntry{
if openclawVisionModelPattern.MatchString(modelID) { ID: strings.TrimSpace(modelID),
entry.Input = []string{"text", "image"} Name: name,
Reasoning: model.Reasoning,
Input: sanitizeAgentAccountModelInputs(model.Input),
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Cost: modelCost{},
} }
return entry
} }
type openclawAccountModelRuntime struct { type openclawAccountModelRuntime struct {
StoredModel string StoredModel string
PrimaryModel string PrimaryModel string
APIType string APIType string
MaxTokens int
ContextWindow int
} }
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) { func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
@@ -1142,9 +1095,11 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
return openclawAccountModelRuntime{}, err return openclawAccountModelRuntime{}, err
} }
return openclawAccountModelRuntime{ return openclawAccountModelRuntime{
StoredModel: model.ID, StoredModel: model.ID,
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
APIType: account.APIType, APIType: account.APIType,
MaxTokens: model.MaxTokens,
ContextWindow: model.ContextWindow,
}, nil }, nil
} }
@@ -1164,38 +1119,35 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
} }
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel") return nil, buserr.New("ErrAgentAccountSingleInitialModel")
} }
if len(requested) > 0 { if len(requested) > 0 {
return normalizeAgentAccountModels(account, requested) models := make([]dto.AgentAccountModel, 0, len(requested))
for _, item := range requested {
models = append(models, cloneAgentAccountModel(item))
}
return models, nil
} }
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType) meta, ok := providercatalog.Get(account.Provider)
if len(defaultModels) == 0 { if !ok || len(meta.Models) == 0 {
if requiresInitialAgentAccountModels(account.Provider) { if requiresInitialAgentAccountModels(account.Provider) {
return nil, buserr.New("ErrAgentAccountModelsRequired") return nil, buserr.New("ErrAgentAccountModelsRequired")
} }
return nil, nil return nil, nil
} }
requested = make([]dto.AgentAccountModel, 0, len(defaultModels)) requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
for _, item := range defaultModels { for _, item := range meta.Models {
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
return normalizeAgentAccountModels(account, requested) return requested, nil
}
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
for _, modelID := range modelIDs {
models = append(models, dto.AgentAccountModel{
ID: modelID,
Name: modelID,
})
}
return models
} }
func compactPersistedAgentAccountModelSortOrder(accountID uint) error { func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
@@ -1223,6 +1175,18 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
return listPersistedAgentAccountModels(account.ID) return listPersistedAgentAccountModels(account.ID)
} }
func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel {
return dto.AgentAccountModel{
RecordID: model.RecordID,
ID: model.ID,
Name: model.Name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: append([]string(nil), model.Input...),
}
}
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
@@ -1246,8 +1210,12 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
continue continue
} }
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
if len(requested) == len(existing) { if len(requested) == len(existing) {
@@ -1266,10 +1234,18 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
} }
result := make([]dto.AgentAccountModel, 0, len(rows)) result := make([]dto.AgentAccountModel, 0, len(rows))
for _, row := range rows { for _, row := range rows {
inputs := []string{}
if strings.TrimSpace(row.Input) != "" {
_ = json.Unmarshal([]byte(row.Input), &inputs)
}
result = append(result, dto.AgentAccountModel{ result = append(result, dto.AgentAccountModel{
RecordID: row.ID, RecordID: row.ID,
ID: strings.TrimSpace(row.Model), ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name), Name: strings.TrimSpace(row.Name),
ContextWindow: row.ContextWindow,
MaxTokens: row.MaxTokens,
Reasoning: row.Reasoning,
Input: sanitizeAgentAccountModelInputs(inputs),
}) })
} }
return result, nil return result, nil
@@ -1280,11 +1256,19 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
return err return err
} }
for index, item := range models { for index, item := range models {
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
if err != nil {
return err
}
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: accountID, AccountID: accountID,
Model: strings.TrimSpace(item.ID), Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name), Name: strings.TrimSpace(item.Name),
SortOrder: index + 1, ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
} }
if err := tx.Create(record).Error; err != nil { if err := tx.Create(record).Error; err != nil {
return err return err
@@ -1325,17 +1309,86 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
if modelID == "" { if modelID == "" {
return dto.AgentAccountModel{}, fmt.Errorf("model is required") return dto.AgentAccountModel{}, fmt.Errorf("model is required")
} }
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = modelID name = modelID
} }
inputs := sanitizeAgentAccountModelInputs(model.Input)
return dto.AgentAccountModel{ return dto.AgentAccountModel{
ID: modelID, ID: normalizeAgentAccountModelID(account.Provider, modelID),
Name: name, Name: name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: inputs,
}, nil }, nil
} }
func normalizeAgentAccountModelID(provider, requestedID string) string {
switch provider {
case "custom", "vllm":
return normalizeCustomModel(requestedID)
case "ollama":
target := strings.TrimSpace(requestedID)
if strings.HasPrefix(target, "ollama/") {
return target
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if target == "" {
return ""
}
return "ollama/" + target
default:
target := strings.TrimSpace(requestedID)
if target == "" {
return ""
}
prefix := poolModelPrefix(provider)
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
targetPrefix := parts[0]
targetModel := strings.TrimSpace(parts[1])
if targetModel == "" {
return strings.TrimSpace(target)
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == targetPrefix {
if prefix != "" {
return prefix + "/" + targetModel
}
return strings.TrimSpace(target)
}
}
return strings.TrimSpace(target)
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if prefix == "" {
return target
}
return prefix + "/" + target
}
}
func sanitizeAgentAccountModelInputs(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
normalized := value
if normalized != "text" && normalized != "image" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
result = append(result, normalized)
}
if len(result) == 0 {
return []string{"text"}
}
return result
}
func requiresInitialAgentAccountModels(provider string) bool { func requiresInitialAgentAccountModels(provider string) bool {
switch provider { switch provider {
case "custom", "vllm", "ollama": case "custom", "vllm", "ollama":
@@ -1345,14 +1398,36 @@ func requiresInitialAgentAccountModels(provider string) bool {
} }
} }
func normalizeComparableProviderModelID(provider, modelID string) string {
target := strings.TrimSpace(modelID)
if target == "" {
return ""
}
if !strings.Contains(target, "/") {
return target
}
parts := strings.SplitN(target, "/", 2)
prefix := parts[0]
model := strings.TrimSpace(parts[1])
if model == "" {
return target
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == prefix {
return model
}
}
return target
}
func sameProviderModelID(provider, left, right string) bool { func sameProviderModelID(provider, left, right string) bool {
leftTrimmed := strings.TrimSpace(left) leftTrimmed := strings.TrimSpace(left)
rightTrimmed := strings.TrimSpace(right) rightTrimmed := strings.TrimSpace(right)
if leftTrimmed == rightTrimmed { if leftTrimmed == rightTrimmed {
return true return true
} }
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed) leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed) rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed)
return leftComparable != "" && leftComparable == rightComparable return leftComparable != "" && leftComparable == rightComparable
} }
@@ -1373,20 +1448,6 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
return selectedAccountModel, nil return selectedAccountModel, nil
} }
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
if len(models) == 0 {
return "", buserr.New("ErrAgentAccountModelsRequired")
}
if strings.TrimSpace(requested) == "" {
return models[0].ID, nil
}
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
if !ok {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return selected.ID, nil
}
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error { func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
if account == nil || account.ID == 0 { if account == nil || account.ID == 0 {
return nil return nil
@@ -1463,122 +1524,6 @@ func readInstallEnv(envStr string) map[string]interface{} {
return data return data
} }
const (
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
)
type agentDashboardAuth struct {
Username string
Password string
}
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
auth := agentDashboardAuth{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
if auth.Username == "" {
auth.Username = "admin"
}
if auth.Password == "" {
auth.Password = common.RandStr(8)
}
return auth
}
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
switch agentType {
case constant.AppHermesAgent:
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
default:
return "", "", false
}
}
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
values := map[string]string{
usernameKey: auth.Username,
passwordKey: auth.Password,
}
order := []string{usernameKey, passwordKey}
if agentType == constant.AppCopaw {
values[qwenPawAuthEnabledEnvKey] = "true"
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
}
return upsertAgentEnv(envPath, values, order, overwrite)
}
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return agentDashboardAuth{}, err
}
return agentDashboardAuth{
Username: strings.TrimSpace(envMap[usernameKey]),
Password: strings.TrimSpace(envMap[passwordKey]),
}, nil
}
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
if appInstall == nil || appInstall.ID == 0 {
return agentDashboardAuth{}
}
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
if err != nil {
return agentDashboardAuth{}
}
return auth
}
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string { func maskKey(value string) string {
trim := strings.TrimSpace(value) trim := strings.TrimSpace(value)
if len(trim) <= 6 { if len(trim) <= 6 {
@@ -1606,6 +1551,57 @@ func toInt(value interface{}) int {
} }
} }
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
if strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
}
return trim
}
func runtimeProviderModelPrefix(provider string) string {
switch provider {
case "gemini":
return "google"
case "kimi":
return "moonshot"
default:
return provider
}
}
func poolModelPrefix(provider string) string {
meta, ok := providercatalog.Get(provider)
if ok && len(meta.Models) > 0 {
parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
return parts[0]
}
}
return provider
}
func supportedProviderModelPrefixes(provider string) []string {
values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
target := value
if target == "" {
continue
}
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
func generateToken() string { func generateToken() string {
bytes := make([]byte, 24) bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
+5 -1
View File
@@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
} }
} }
func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
websiteMap := make(map[uint]model.Website) websiteMap := make(map[uint]model.Website)
duplicateAppInstallIDs := make(map[uint]struct{}) duplicateAppInstallIDs := make(map[uint]struct{})
for _, website := range websites { for _, website := range websites {
@@ -182,6 +182,10 @@ func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
return websiteMap return websiteMap
} }
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
return uniqueDeploymentWebsiteMapByAppInstall(websites)
}
func uniqueUintList(items []uint) []uint { func uniqueUintList(items []uint) []uint {
itemMap := make(map[uint]struct{}, len(items)) itemMap := make(map[uint]struct{}, len(items))
uniq := make([]uint, 0, len(items)) uniq := make([]uint, 0, len(items))
-139
View File
@@ -1,139 +0,0 @@
package service
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const (
vllmAppKeyForUpgrade = "vllm"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
)
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia
}
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
}
}
return trimmed
}
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag
}
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag
}
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
targetFamily := resolveVllmVersionFamily(targetVersion, "")
return currentFamily == targetFamily
}
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
if strings.TrimSpace(currentVersion) == strings.TrimSpace(targetVersion) {
return false
}
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
return false
}
if common.CompareVersion(targetVersion, currentVersion) {
return true
}
return !hasVllmVersionFamilyPrefix(currentVersion) &&
resolveVllmVersionFamily(targetVersion, "") == vllmImageTypeNvidia &&
trimVllmVersionFamily(currentVersion) == trimVllmVersionFamily(targetVersion)
}
func buildVllmUpgradeImage(currentImage, currentVersion, targetVersion string) string {
trimmedImage := strings.TrimSpace(currentImage)
if trimmedImage == "" || trimmedImage == buildDefaultVllmImageByVersion(currentVersion) {
return buildDefaultVllmImageByVersion(targetVersion)
}
return trimmedImage
}
func loadVllmImageFromEnv(raw string) string {
envs := make(map[string]interface{})
if strings.TrimSpace(raw) == "" {
return ""
}
if err := json.Unmarshal([]byte(raw), &envs); err != nil {
return ""
}
if image, ok := envs[vllmImageEnvKey].(string); ok {
return strings.TrimSpace(image)
}
return ""
}
func setVllmImageInEnvContent(content []byte, image string) []byte {
normalizedImage := strings.TrimSpace(image)
if normalizedImage == "" {
return content
}
lines := strings.Split(string(content), "\n")
replaced := false
for index, line := range lines {
if strings.HasPrefix(line, vllmImageEnvKey+"=") {
lines[index] = vllmImageEnvKey + "=" + normalizedImage
replaced = true
break
}
}
if !replaced {
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines[len(lines)-1] = vllmImageEnvKey + "=" + normalizedImage
lines = append(lines, "")
} else {
lines = append(lines, vllmImageEnvKey+"="+normalizedImage)
}
}
return []byte(strings.Join(lines, "\n"))
}
-3
View File
@@ -384,9 +384,6 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
if search.Count != 0 { if search.Count != 0 {
opts = append(opts, alertRepo.WithByCount(search.Count)) opts = append(opts, alertRepo.WithByCount(search.Count))
} }
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
}
opts = append(opts, repo.WithOrderDesc("created_at")) opts = append(opts, repo.WithOrderDesc("created_at"))
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...) total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
+1
View File
@@ -705,6 +705,7 @@ func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType st
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
doSendAlert(alert, alertType, quota, quotaType, params, config) doSendAlert(alert, alertType, quota, quotaType, params, config)
+1
View File
@@ -93,6 +93,7 @@ func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []d
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
if !alertUtil.IsAlertConfigEnabled(config) { if !alertUtil.IsAlertConfigEnabled(config) {
+1 -9
View File
@@ -303,9 +303,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
filename := filepath.Base(appDetailDTO.DownloadUrl) filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err) return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
} }
@@ -429,12 +426,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
} else { } else {
if appDetail.DockerCompose == "" { if appDetail.DockerCompose == "" {
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version) dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
var statusCode int _, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
err = buserr.New("ErrAppVersionUnavailable")
return
}
if err != nil { if err != nil {
return return
} }
-4
View File
@@ -294,7 +294,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
DetailID: req.DetailId, DetailID: req.DetailId,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose, DockerCompose: req.DockerCompose,
TaskID: req.TaskID, TaskID: req.TaskID,
} }
@@ -601,9 +600,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return versions, err return versions, err
} }
-928
View File
@@ -1,928 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"path"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"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/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/joho/godotenv"
)
type appUpgradePhase int
const (
appUpgradePreparing appUpgradePhase = iota
appUpgradePrepared
appUpgradeStopped
appUpgradeBackedUp
appUpgradeDown
appUpgradeMutated
appUpgradeStarted
appUpgradeReady
appUpgradeCommitted
)
const composeServiceLabel = "com.docker.compose.service"
var appUpgradeLocks sync.Map
type appUpgradeSnapshot interface {
Restore() error
Cleanup()
}
type upgradeFileSnapshot struct {
installPath string
backupPath string
paths []string
existing map[string]bool
}
type appUpgradeContext struct {
req request.AppInstallUpgrade
original model.AppInstall
candidate model.AppInstall
detail model.AppDetail
phase appUpgradePhase
stopAttempted bool
downAttempted bool
rollbackErr error
detailDir string
stageDir string
envContent []byte
oldEnvContent []byte
oldDockerCompose string
oldImageIDs []appImageID
backupFile string
snapshot appUpgradeSnapshot
createdPaths []string
}
func upgradeInstall(req request.AppInstallUpgrade) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil {
return err
}
if install.Status == constant.StatusUpgrading {
return buserr.New("TaskIsExecuting")
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting")
}
releaseLock := true
defer func() {
if releaseLock {
appUpgradeLocks.Delete(install.ID)
}
}()
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
if install.Version == detail.Version {
return errors.New("two version is same")
}
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
return err
}
ctx := &appUpgradeContext{
req: req,
original: install,
candidate: install,
detail: detail,
phase: appUpgradePreparing,
oldDockerCompose: install.DockerCompose,
}
upgradeTask.AddSubTaskWithOps(i18n.GetMsgByKey("UpgradePrepare"), ctx.prepare, nil, 0, 0)
upgradeTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp),
ctx.cutover,
func(t *task.Task) {
ctx.rollbackErr = ctx.rollback(t)
},
0,
0,
)
upgradingInstall := install
upgradingInstall.Status = constant.StatusUpgrading
upgradingInstall.Message = ""
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
return err
}
releaseLock = false
go func() {
defer appUpgradeLocks.Delete(install.ID)
defer ctx.cleanup()
taskErr := upgradeTask.Execute()
if taskErr == nil {
return
}
if ctx.rollbackErr != nil {
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
upgradeTask.Task.ErrorMsg = taskErr.Error()
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
}
if !ctx.stopAttempted || ctx.rollbackErr == nil {
restored := ctx.original
_ = appInstallRepo.Save(context.Background(), &restored)
return
}
failed := ctx.original
failed.Status = constant.StatusUpgradeErr
failed.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &failed)
}()
return nil
}
func (u *appUpgradeContext) prepare(t *task.Task) error {
fileOp := files.NewFileOp()
u.detailDir = path.Join(u.original.App.GetAppResourcePath(), u.detail.Version)
if u.original.App.Resource == constant.AppResourceRemote {
if err := downloadApp(u.original.App, u.detail, nil, t.Logger); err != nil {
return err
}
}
if !fileOp.Stat(u.detailDir) {
return buserr.WithName("ErrFileNotFound", u.detailDir)
}
if u.detail.DockerCompose == "" {
composeContent, err := fileOp.GetContent(path.Join(u.detailDir, "docker-compose.yml"))
if err != nil {
return err
}
u.detail.DockerCompose = string(composeContent)
_ = appDetailRepo.Update(context.Background(), u.detail)
}
if strings.TrimSpace(u.detail.DockerCompose) == "" && strings.TrimSpace(u.req.DockerCompose) == "" {
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
}
var err error
u.oldEnvContent, err = fileOp.GetContent(u.original.GetEnvPath())
if err != nil {
return err
}
u.stageDir, err = os.MkdirTemp(u.original.GetAppPath(), "."+u.original.Name+"-upgrade-")
if err != nil {
return err
}
if err = fileOp.CopyDirWithNewName(u.detailDir, u.stageDir, "."); err != nil {
return err
}
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, ".env"); err != nil {
return err
}
if u.original.App.Key == constant.AppOpenclaw {
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
return err
}
}
if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{
nginxModuleBuildDir,
nginxModuleModulesDir,
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
} {
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, relativePath); err != nil {
return err
}
}
}
stagedInstall := u.original
stagedInstall.Name = path.Base(u.stageDir)
stagedInstall.Version = u.detail.Version
stagedInstall.AppDetailId = u.req.DetailID
if stagedInstall.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(stagedInstall.Env), &envs); err != nil {
return err
}
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
envs[vllmImageEnvKey] = image
paramBytes, marshalErr := json.Marshal(envs)
if marshalErr != nil {
return marshalErr
}
stagedInstall.Env = string(paramBytes)
}
if err = migrateOpenclawProtocolUpgrade(&stagedInstall, u.original.Version, u.detail.Version); err != nil {
return err
}
u.candidate = stagedInstall
u.candidate.Name = u.original.Name
u.candidate.DockerCompose, err = renderUpgradeCompose(u.candidate, u.detail, u.req.DockerCompose)
if err != nil {
return err
}
if strings.TrimSpace(u.candidate.DockerCompose) == "" {
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
}
u.envContent, err = renderUpgradeEnv(&u.candidate, u.oldEnvContent)
if err != nil {
return err
}
if err = writeUpgradeFile(path.Join(u.stageDir, ".env"), u.envContent, constant.FilePerm); err != nil {
return err
}
if err = writeUpgradeFile(path.Join(u.stageDir, "docker-compose.yml"), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
return err
}
project, err := docker.GetComposeProject(u.original.Name, u.stageDir, []byte(u.candidate.DockerCompose), u.envContent, false)
if err != nil {
return err
}
hasBuild := false
for _, service := range project.Services {
if service.Image == "" && service.Build == nil {
return fmt.Errorf("compose service %s has neither image nor build configuration", service.Name)
}
hasBuild = hasBuild || service.Build != nil
}
if u.req.DeleteImage {
dockerClient, clientErr := docker.NewClient()
if clientErr != nil {
return clientErr
}
u.oldImageIDs, err = getAppImageIDsByCompose(dockerClient, u.oldEnvContent, []byte(u.oldDockerCompose))
dockerClient.Close()
if err != nil {
return err
}
}
images := make([]string, 0, len(project.Services))
for _, service := range project.Services {
if service.Image != "" {
images = append(images, service.Image)
}
}
if err = prepareUpgradeImages(t, images, u.req.PullImage); err != nil {
return err
}
if u.candidate.App.Key == constant.AppOpenresty {
if err = u.prepareOpenresty(t, stagedInstall); err != nil {
return err
}
if err = verifyUpgradeImages(images); err != nil {
return err
}
} else if hasBuild {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
t.LogStart(logStr)
if err = compose.BuildWithTask(path.Join(u.stageDir, "docker-compose.yml"), project.Name, t); err != nil {
t.LogFailedWithErr(logStr, err)
return err
}
t.LogSuccess(logStr)
if err = verifyUpgradeImages(images); err != nil {
return err
}
}
if u.original.App.Resource == constant.AppResourceRemote {
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
}
u.phase = appUpgradePrepared
return nil
}
func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.AppInstall) error {
fileOp := files.NewFileOp()
detailBuildDir := path.Join(u.detailDir, nginxModuleBuildDir)
installBuildDir := path.Join(u.stageDir, nginxModuleBuildDir)
if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err
}
}
if err := copyAppDetailMissing(fileOp, detailBuildDir, installBuildDir); err != nil {
return err
}
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
return err
}
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
return err
}
for _, fileName := range []string{"Dockerfile", "nginx.conf", "nginx.vh.default.conf"} {
if err := fileOp.CopyFile(path.Join(detailBuildDir, fileName), installBuildDir); err != nil {
return err
}
}
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
return err
}
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
if !fileOp.Stat(targetCatalogSource) {
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
}
targetCatalogPath := path.Join(installBuildDir, nginxModuleCatalogPendingFile)
if err := stageNginxModuleCatalog(targetCatalogSource, targetCatalogPath); err != nil {
return err
}
stagedInstall.Name = path.Base(u.stageDir)
stagedInstall.Version = u.candidate.Version
stagedInstall.Env = u.candidate.Env
stagedInstall.DockerCompose = u.candidate.DockerCompose
return buildNginx(t, stagedInstall, targetCatalogPath)
}
func (u *appUpgradeContext) cutover(t *task.Task) error {
u.stopAttempted = true
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
u.phase = appUpgradeStopped
var err error
if u.original.App.Key == constant.AppOpenresty {
u.snapshot, err = createOpenrestyUpgradeSnapshot(u.original.GetPath())
} else {
snapshotPaths := []string{".env", "docker-compose.yml", "scripts"}
if u.original.App.Key == constant.AppOpenclaw {
snapshotPaths = append(snapshotPaths, path.Join("data", "conf", "openclaw.json"))
}
u.snapshot, err = createUpgradeFileSnapshot(u.original.GetPath(), snapshotPaths)
}
if err != nil {
return err
}
if u.req.Backup {
if err = u.backup(t); err != nil {
return err
}
u.phase = appUpgradeBackedUp
} else {
t.Log(i18n.GetMsgByKey("UpgradeBackupDisabled"))
}
u.downAttempted = true
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
if out != "" {
downErr = fmt.Errorf("%s: %w", out, downErr)
}
return downErr
}
u.phase = appUpgradeDown
u.phase = appUpgradeMutated
if err = u.applyStagedFiles(); err != nil {
return err
}
if err = writeUpgradeFile(u.original.GetEnvPath(), u.envContent, constant.FilePerm); err != nil {
return err
}
if err = runScript(t, &u.candidate, "upgrade"); err != nil {
return err
}
if err = writeUpgradeFile(u.original.GetComposePath(), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
return err
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.LogStart(logStr)
if out, upErr := compose.UpWithoutPull(u.original.GetComposePath()); upErr != nil {
if out != "" {
upErr = fmt.Errorf("%s: %w", out, upErr)
}
t.LogFailedWithErr(logStr, upErr)
return upErr
}
t.LogSuccess(logStr)
u.phase = appUpgradeStarted
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
u.phase = appUpgradeReady
u.candidate.ContainerName = strings.Join(containerNames, ",")
u.candidate.Status = constant.StatusRunning
u.candidate.Message = ""
if u.candidate.App.Key == constant.AppOpenresty {
liveCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogPendingFile)
if err = commitStaticNginxModuleBuilds(u.candidate, liveCatalogPath, t); err != nil {
return err
}
activeCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
if err = activateNginxModuleCatalogAndCommit(liveCatalogPath, activeCatalogPath, func() error {
return appInstallRepo.Save(context.Background(), &u.candidate)
}); err != nil {
return err
}
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err
}
u.phase = appUpgradeCommitted
u.deleteOldImages(t)
return nil
}
func (u *appUpgradeContext) backup(t *task.Task) error {
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", u.original.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
record, err := backupAppWithParentTask(&u.original, t, fileName)
if err != nil {
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
}
u.backupFile = path.Join(global.Dir.LocalBackupDir, record.FileDir, record.FileName)
info, err := os.Stat(u.backupFile)
if err != nil || info.Size() == 0 || record.Status != constant.StatusSuccess {
if err == nil {
err = errors.New("backup archive is empty or incomplete")
}
markBackupFailed(record.ID, err)
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
}
backupRecordService := NewIBackupRecordService()
backups, _ := backupRecordService.ListAppRecords(u.original.App.Key, u.original.Name, "upgrade_backup")
if len(backups) > 3 {
deleteIDs := make([]uint, 0, len(backups)-3)
for _, backup := range backups[:len(backups)-3] {
deleteIDs = append(deleteIDs, backup.ID)
}
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
}
return nil
}
func (u *appUpgradeContext) applyStagedFiles() error {
fileOp := files.NewFileOp()
if err := copyAppDetailMissingTracked(fileOp, u.detailDir, u.original.GetPath(), &u.createdPaths); err != nil {
return err
}
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), "scripts"); err != nil {
return err
}
if u.original.App.Key == constant.AppOpenclaw {
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), path.Join("data", "conf", "openclaw.json")); err != nil {
return err
}
}
if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{
nginxModuleBuildDir,
nginxModuleModulesDir,
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
path.Join(nginxModuleConfDir, "nginx.conf"),
} {
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil {
return err
}
}
}
return nil
}
func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if !u.stopAttempted {
return nil
}
logStr := i18n.GetWithName("AppRecover", u.original.Name)
t.LogStart(logStr)
defer func() {
if rollbackErr != nil {
t.LogFailedWithErr(logStr, rollbackErr)
} else {
t.LogSuccess(logStr)
}
}()
if !u.downAttempted {
if out, err := compose.Operate(u.original.GetComposePath(), "start"); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return err
}
return u.finishRollback()
}
if u.phase < appUpgradeMutated {
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return err
}
return u.finishRollback()
}
if out, err := compose.Down(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
rollbackErr = err
}
if u.backupFile != "" {
_ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
_, _ = compose.UpWithoutPull(u.original.GetComposePath())
return errors.Join(rollbackErr, err)
}
} else {
if err := u.restoreManagedFiles(); err != nil {
return errors.Join(rollbackErr, err)
}
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return errors.Join(rollbackErr, err)
}
}
return errors.Join(rollbackErr, u.finishRollback())
}
func (u *appUpgradeContext) finishRollback() error {
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
return err
}
restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err
}
return nil
}
func (u *appUpgradeContext) restoreManagedFiles() error {
var restoreErr error
if u.snapshot != nil {
restoreErr = u.snapshot.Restore()
}
for index := len(u.createdPaths) - 1; index >= 0; index-- {
if err := os.RemoveAll(u.createdPaths[index]); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
}
return restoreErr
}
func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
if !u.req.DeleteImage {
return
}
excludeImages, err := docker.GetImagesFromDockerCompose(u.envContent, []byte(u.candidate.DockerCompose))
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return
}
dockerClient, err := docker.NewClient()
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return
}
defer dockerClient.Close()
if err = deleteAppImagesByIDs(t, dockerClient, u.oldImageIDs, excludeImages); err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
}
}
func (u *appUpgradeContext) cleanup() {
if u.snapshot != nil {
u.snapshot.Cleanup()
}
if u.stageDir != "" {
_ = os.RemoveAll(u.stageDir)
}
}
type upgradeImageClient interface {
PullImageWithProcess(*task.Task, string) error
ImageExists(string) (bool, error)
Close()
}
func prepareUpgradeImages(t *task.Task, images []string, pull bool) error {
dockerClient, err := docker.NewClient()
if err != nil {
return err
}
return prepareUpgradeImagesWithClient(t, dockerClient, images, pull)
}
func prepareUpgradeImagesWithClient(t *task.Task, dockerClient upgradeImageClient, images []string, pull bool) error {
defer dockerClient.Close()
seen := make(map[string]struct{}, len(images))
for _, image := range images {
image = strings.TrimSpace(image)
if image == "" {
continue
}
if _, ok := seen[image]; ok {
continue
}
seen[image] = struct{}{}
if pull {
if t != nil {
t.Log(i18n.GetWithName("PullImageStart", image))
}
if pullErr := dockerClient.PullImageWithProcess(t, image); pullErr != nil {
if exists, _ := dockerClient.ImageExists(image); exists {
if t != nil {
t.Log(i18n.GetMsgByKey("UseExistImage"))
}
continue
}
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
}
}
exists, inspectErr := dockerClient.ImageExists(image)
if inspectErr != nil || !exists {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
}
if pull && t != nil {
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
return nil
}
func verifyUpgradeImages(images []string) error {
dockerClient, err := docker.NewClient()
if err != nil {
return err
}
defer dockerClient.Close()
for _, image := range images {
exists, inspectErr := dockerClient.ImageExists(image)
if inspectErr != nil || !exists {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
}
}
return nil
}
func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error) {
originalEnv := make(map[string]string)
if len(original) > 0 {
var err error
originalEnv, err = godotenv.UnmarshalBytes(original)
if err != nil {
return nil, err
}
}
params := make(map[string]string, len(originalEnv))
maps.Copy(params, originalEnv)
envs := make(map[string]interface{})
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
return nil, err
}
handleMap(envs, params)
if install.App.Key == constant.AppOpenresty {
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
if value, ok := originalEnv[key]; ok {
params[key] = value
}
}
if websiteDir := strings.TrimSpace(originalEnv["WEBSITE_DIR"]); websiteDir != "" {
params["WEBSITE_DIR"] = websiteDir
}
websiteDir := strings.TrimSpace(params["WEBSITE_DIR"])
if websiteDir == "" {
websiteDir = NewISettingService().GetWebsiteDir()
}
if !path.IsAbs(websiteDir) {
websiteDir = path.Join(global.Dir.DataDir, websiteDir)
}
params["WEBSITE_DIR"] = websiteDir
envs["WEBSITE_DIR"] = websiteDir
content, marshalErr := json.Marshal(envs)
if marshalErr != nil {
return nil, marshalErr
}
install.Env = string(content)
}
content, err := godotenv.Marshal(params)
if err != nil {
return nil, err
}
return []byte(content), nil
}
func renderUpgradeCompose(install model.AppInstall, detail model.AppDetail, customCompose string) (string, error) {
if customCompose != "" {
return customCompose, nil
}
if install.App.Key == vllmAppKeyForUpgrade {
return install.DockerCompose, nil
}
return getUpgradeCompose(install, detail)
}
func writeUpgradeFile(filePath string, content []byte, mode os.FileMode) error {
tmp, err := os.CreateTemp(path.Dir(filePath), "."+path.Base(filePath)+".*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err = tmp.Chmod(mode); err == nil {
_, err = tmp.Write(content)
}
if err == nil {
err = tmp.Sync()
}
if closeErr := tmp.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return os.Rename(tmpPath, filePath)
}
func copyUpgradeStageFile(sourceRoot, targetRoot, relativePath string) error {
source := path.Join(sourceRoot, relativePath)
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
target := path.Join(targetRoot, relativePath)
_ = os.RemoveAll(target)
return copyOpenrestyUpgradeSnapshotEntry(source, target)
}
func replaceUpgradePath(sourceRoot, targetRoot, relativePath string) error {
source := path.Join(sourceRoot, relativePath)
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
target := path.Join(targetRoot, relativePath)
if err := os.RemoveAll(target); err != nil {
return err
}
return copyOpenrestyUpgradeSnapshotEntry(source, target)
}
func createUpgradeFileSnapshot(installPath string, paths []string) (*upgradeFileSnapshot, error) {
backupPath, err := os.MkdirTemp("", "1panel-app-upgrade-*")
if err != nil {
return nil, err
}
snapshot := &upgradeFileSnapshot{
installPath: installPath,
backupPath: backupPath,
paths: paths,
existing: make(map[string]bool, len(paths)),
}
for _, relativePath := range paths {
source := path.Join(installPath, relativePath)
if _, err = os.Stat(source); err != nil {
if os.IsNotExist(err) {
continue
}
snapshot.Cleanup()
return nil, err
}
snapshot.existing[relativePath] = true
if err = copyOpenrestyUpgradeSnapshotEntry(source, path.Join(backupPath, relativePath)); err != nil {
snapshot.Cleanup()
return nil, err
}
}
return snapshot, nil
}
func (s *upgradeFileSnapshot) Restore() error {
for _, relativePath := range s.paths {
target := path.Join(s.installPath, relativePath)
if err := os.RemoveAll(target); err != nil {
return err
}
if !s.existing[relativePath] {
continue
}
if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), target); err != nil {
return err
}
}
return nil
}
func (s *upgradeFileSnapshot) Cleanup() {
if s != nil && s.backupPath != "" {
_ = os.RemoveAll(s.backupPath)
}
}
type appContainerReadinessClient interface {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
}
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
return waitAppContainersReadyWithClient(ctx, client, install)
}
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
envContent, err := os.ReadFile(install.GetEnvPath())
if err != nil {
envContent, err = renderUpgradeEnv(&install, nil)
if err != nil {
return nil, err
}
}
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
if err != nil {
return nil, err
}
expectedServices := make(map[string]struct{})
for _, service := range project.Services {
if !skipCheckStatus(service) {
expectedServices[service.Name] = struct{}{}
}
}
if len(expectedServices) == 0 {
return strings.Split(install.ContainerName, ","), nil
}
options := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
),
}
containers, err := client.ContainerList(ctx, options)
if err != nil {
return nil, err
}
foundServices := make(map[string]bool, len(expectedServices))
containerNames := make([]string, 0, len(containers))
for _, item := range containers {
serviceName := item.Labels[composeServiceLabel]
if _, ok := expectedServices[serviceName]; !ok {
continue
}
if err = waitContainerReady(ctx, client, item.ID); err != nil {
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
}
foundServices[serviceName] = true
if len(item.Names) > 0 {
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
}
}
for serviceName := range expectedServices {
if !foundServices[serviceName] {
return nil, fmt.Errorf("container for service %s was not created", serviceName)
}
}
sort.Strings(containerNames)
return containerNames, nil
}
+368 -236
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"bufio"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -9,6 +10,7 @@ import (
"math" "math"
"net/http" "net/http"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -87,15 +89,7 @@ func checkPort(key string, params map[string]interface{}) (int, error) {
return 0, nil return 0, nil
} }
func isPortInUse(port int, protocol string) bool {
return common.ScanPortWithProto(port, normalizeComposeProtocol(protocol))
}
func checkPortExist(port int) error { func checkPortExist(port int) error {
return checkPortExistWithProtocol(port, "")
}
func checkPortExistWithProtocol(port int, protocol string) error {
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["port"] = port errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -116,7 +110,7 @@ func checkPortExistWithProtocol(port int, protocol string) error {
errMap["name"] = domain.Domain errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil) return buserr.WithMap("ErrPortExist", errMap, nil)
} }
if isPortInUse(port, protocol) { if common.ScanPort(port) {
return buserr.WithDetail("ErrPortInUsed", port, nil) return buserr.WithDetail("ErrPortInUsed", port, nil)
} }
return nil return nil
@@ -297,8 +291,8 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
} }
resourceId = oldMysqlDb.ID resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 { if oldMysqlDb.ID > 0 {
if err := ensureAppMysqlDBUser(database, dbConfig); err != nil { if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
return err return buserr.New("ErrDbUserNotValid")
} }
} else { } else {
var createMysql dto.MysqlDBCreate var createMysql dto.MysqlDBCreate
@@ -363,13 +357,33 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return err return err
} }
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
if err = deleteAppImagesByCompose(t, content, []byte(install.DockerCompose), nil); err != nil { images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err return err
} }
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil {
imgStr := delImageStr + image
t.Log(imgStr)
if err = client.DeleteImage(imageID); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(delImageStr + image)
}
}
} }
} }
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
@@ -410,20 +424,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
switch install.App.Key { switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster: case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
if err = databaseUserGrantRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserGrantRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
if err = databaseUserRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name)) _ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppMongodb: case constant.AppMongodb:
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name)) _ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
@@ -468,107 +468,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return nil return nil
} }
type appImageID struct {
name string
id string
}
func getAppImageIDsByCompose(client docker.Client, envContent, composeContent []byte) ([]appImageID, error) {
images, err := docker.GetImagesFromDockerCompose(envContent, composeContent)
if err != nil {
return nil, err
}
imageIDs := make([]appImageID, 0, len(images))
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
imageIDs = append(imageIDs, appImageID{name: image, id: imageID})
}
}
return imageIDs, nil
}
func deleteAppImagesByCompose(t *task.Task, envContent, composeContent []byte, excludeImages []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageIDs, err := getAppImageIDsByCompose(client, envContent, composeContent)
if err != nil {
return err
}
return deleteAppImagesByIDs(t, client, imageIDs, excludeImages)
}
func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImageID, excludeImages []string) error {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
excludeImageIDs := make(map[string]struct{}, len(excludeImages))
for _, image := range excludeImages {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
excludeImageIDs[imageID] = struct{}{}
}
}
deletedImageIDs := make(map[string]struct{}, len(imageIDs))
for _, image := range imageIDs {
if _, ok := excludeImageIDs[image.id]; ok {
continue
}
if _, ok := deletedImageIDs[image.id]; ok {
continue
}
deletedImageIDs[image.id] = struct{}{}
imgStr := delImageStr + image.name
t.Log(imgStr)
if err := client.DeleteImage(image.id); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(imgStr)
}
return nil
}
func ensureAppMysqlDBUser(database model.Database, dbConfig dto.AppDatabase) error {
const host = "%"
mysqlService := NewIMysqlService()
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
userExists := false
passwordValid := false
for _, user := range users {
if user.Username != dbConfig.DbUser || user.Host != host || user.IsDelete {
continue
}
userExists = true
passwordValid = user.Password == dbConfig.Password
break
}
if !userExists || !passwordValid {
return buserr.New("ErrDbUserNotValid")
}
grants, err := mysqlService.ListGrants(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
for _, grant := range grants {
if grant.Database == dbConfig.DbName && grant.Username == dbConfig.DbUser && grant.Host == host {
return nil
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbConfig.DbName,
Username: dbConfig.DbUser,
Host: host,
})
}
func deleteLink(del dto.DelAppLink) error { func deleteLink(del dto.DelAppLink) error {
install := del.Install install := del.Install
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
@@ -578,20 +477,19 @@ func deleteLink(del dto.DelAppLink) error {
for _, re := range resources { for _, re := range resources {
switch re.Key { switch re.Key {
case constant.AppMysql, constant.AppMariaDB: case constant.AppMysql, constant.AppMariaDB:
mysqlService := NewIMysqlService()
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId)) database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
if reflect.DeepEqual(database, model.DatabaseMysql{}) { if reflect.DeepEqual(database, model.DatabaseMysql{}) {
continue continue
} }
if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{ if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{
ID: database.ID, ID: database.ID,
ForceDelete: del.ForceDelete, ForceDelete: del.ForceDelete,
DeleteBackup: true, DeleteBackup: true,
Type: re.Key, Type: re.Key,
Database: database.MysqlName, Database: database.MysqlName,
}, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil { }); err != nil && !del.ForceDelete {
if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete { return err
return err
}
} }
case constant.AppPostgresql: case constant.AppPostgresql:
pgsqlService := NewIPostgresqlService() pgsqlService := NewIPostgresqlService()
@@ -666,58 +564,11 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
if oldServiceValue["restart"] != nil { if oldServiceValue["restart"] != nil {
serviceValue["restart"] = oldServiceValue["restart"] serviceValue["restart"] = oldServiceValue["restart"]
} }
if install.App.Key == constant.AppOpenresty {
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
}
servicesMap[install.ServiceName] = serviceValue servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap composeMap["services"] = servicesMap
return composeMap, nil return composeMap, nil
} }
// mergeOpenrestyModuleVolumes carries the dynamic module mounts of the old
// compose over to the upgraded one when it does not declare them, so built
// module artifacts and their load configuration stay mounted across upgrades.
func mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue map[string]interface{}) {
oldVolumes, ok := oldServiceValue["volumes"].([]interface{})
if !ok {
return
}
newVolumes, _ := serviceValue["volumes"].([]interface{})
existing := make(map[string]struct{}, len(newVolumes))
for _, volume := range newVolumes {
if containerPath, ok := composeVolumeContainerPath(volume); ok {
existing[containerPath] = struct{}{}
}
}
for _, volume := range oldVolumes {
containerPath, ok := composeVolumeContainerPath(volume)
if !ok {
continue
}
if !strings.Contains(containerPath, nginxModuleEnabledConfDir) && !strings.Contains(containerPath, "nginx/modules/1panel") {
continue
}
if _, ok = existing[containerPath]; ok {
continue
}
newVolumes = append(newVolumes, volume)
existing[containerPath] = struct{}{}
}
serviceValue["volumes"] = newVolumes
}
func composeVolumeContainerPath(volume interface{}) (string, bool) {
volumeStr, ok := volume.(string)
if !ok {
return "", false
}
parts := strings.Split(volumeStr, ":")
if len(parts) < 2 {
return "", false
}
return parts[1], true
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" { if detail.DockerCompose == "" {
return "", nil return "", nil
@@ -751,35 +602,340 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
return string(composeByte), nil return string(composeByte), nil
} }
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error { func buildNginx(parentTask *task.Task) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir) buildPath := path.Join(nginxInstall.GetPath(), "build")
if !fileOp.Stat(buildPath) { if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound") return buserr.New("ErrBuildDirNotFound")
} }
modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath) moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil { if err != nil {
return err return err
} }
previousModules := cloneNginxModules(modules) var (
staticBuild := hasEnabledStaticNginxModules(modules) modules []dto.NginxModule
if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil { addModuleParams []string
return err addPackages []string
} )
if staticBuild { if len(moduleContent) > 0 {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image")) _ = json.Unmarshal(moduleContent, &modules)
parentTask.LogStart(logStr) bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute)) if err != nil {
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil { return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err return err
} }
parentTask.LogSuccess(logStr)
} }
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask) envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath) envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
if len(addModuleParams) == 0 && len(addPackages) == 0 {
return nil
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
return nil
}
func upgradeInstall(req request.AppInstallUpgrade) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil {
return err
}
oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
if install.Version == detail.Version {
return errors.New("two version is same")
}
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
return err
}
install.Status = constant.StatusUpgrading
var (
upErr error
backupFile string
)
backUpApp := func(t *task.Task) error {
backupService := NewIBackupService()
backupRecordService := NewIBackupRecordService()
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
backupRecord, err := backupService.AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name, FileName: fileName})
if err == nil {
backups, _ := backupRecordService.ListAppRecords(install.App.Key, install.Name, "upgrade_backup")
if len(backups) > 3 {
backupsToDelete := backups[:len(backups)-3]
var deleteIDs []uint
for _, backup := range backupsToDelete {
deleteIDs = append(deleteIDs, backup.ID)
}
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
}
backupFile = path.Join(global.Dir.LocalBackupDir, backupRecord.FileDir, backupRecord.FileName)
} else {
return buserr.WithNameAndErr("ErrAppBackup", install.Name, err)
}
return nil
}
if req.Backup {
upgradeTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backUpApp, nil)
}
upgradeApp := func(t *task.Task) error {
fileOp := files.NewFileOp()
detailDir := path.Join(global.Dir.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version)
if install.App.Resource == constant.AppResourceRemote {
if err = downloadApp(install.App, detail, &install, t.Logger); err != nil {
return err
}
if detail.DockerCompose == "" {
composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml"))
if err != nil {
return err
}
detail.DockerCompose = string(composeDetail)
_ = appDetailRepo.Update(context.Background(), detail)
}
go func() {
RequestDownloadCallBack(detail.DownloadCallBackUrl)
}()
}
if install.App.Resource == constant.AppResourceLocal {
detailDir = path.Join(global.Dir.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version)
}
content, err := fileOp.GetContent(install.GetEnvPath())
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
}
image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
envs[vllmImageEnvKey] = image
paramByte, err := json.Marshal(envs)
if err != nil {
return err
}
install.Env = string(paramByte)
content = setVllmImageInEnvContent(content, image)
}
if req.PullImage {
composeContent := []byte(detail.DockerCompose)
if install.App.Key == vllmAppKeyForUpgrade {
composeContent = []byte(install.DockerCompose)
}
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
exist, err := dockerCLi.ImageExists(image)
if err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
if !exist {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s not found", image))
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), "build")
detailBuildDir := path.Join(detailDir, "build")
if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err
}
}
if err := fileOp.DeleteDir(path.Join(installBuildDir, "tmp")); err != nil {
return err
}
if err := fileOp.CopyDir(path.Join(detailBuildDir, "tmp"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.vh.default.conf"), installBuildDir); err != nil {
return err
}
}
sourceScripts := path.Join(detailDir, "scripts")
if fileOp.Stat(sourceScripts) {
dstScripts := path.Join(install.GetPath(), "scripts")
_ = fileOp.DeleteDir(dstScripts)
_ = fileOp.CreateDir(dstScripts, constant.DirPerm)
scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/")
_, _ = scriptCmd.CombinedOutput()
}
var newCompose string
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
return err
}
if req.DockerCompose == "" {
if install.App.Key == vllmAppKeyForUpgrade {
newCompose = install.DockerCompose
} else {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
return err
}
}
} else {
newCompose = req.DockerCompose
}
install.DockerCompose = newCompose
install.Version = detail.Version
install.AppDetailId = req.DetailID
if out, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" {
upErr = errors.New(out)
return upErr
}
return err
}
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
}
envParams := make(map[string]string, len(envs))
if install.App.Key == constant.AppOpenresty {
packageUrl, _ := env.GetEnvValueByKey(install.GetEnvPath(), "CONTAINER_PACKAGE_URL")
addPackage, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_ADD_PACKAGE_BUILDDEPS")
options, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_CONFIG_OPTIONS_MORE")
envParams["CONTAINER_PACKAGE_URL"] = packageUrl
envParams["RESTY_ADD_PACKAGE_BUILDDEPS"] = addPackage
envParams["RESTY_CONFIG_OPTIONS_MORE"] = options
}
handleMap(envs, envParams)
if err = env.Write(envParams, install.GetEnvPath()); err != nil {
return err
}
if err = runScript(t, &install, "upgrade"); err != nil {
return err
}
if err = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), constant.FilePerm); err != nil {
return err
}
if install.App.Key == constant.AppOpenresty {
if err = buildNginx(t); err != nil {
t.Log(err.Error())
return err
}
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.Log(logStr)
if out, err := compose.Up(install.GetComposePath()); err != nil {
if out != "" {
return errors.New(out)
}
return err
}
t.LogSuccess(logStr)
install.Status = constant.StatusRunning
return appInstallRepo.Save(context.Background(), &install)
}
rollBackApp := func(t *task.Task) {
if req.Backup {
t.Log(i18n.GetWithName("AppRecover", install.Name))
if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile}); err != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), err)
return
}
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return
}
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
go func() {
err = upgradeTask.Execute()
if err != nil {
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
existInstall.Status = constant.StatusUpgradeErr
existInstall.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), &existInstall)
}
}
}()
return appInstallRepo.Save(context.Background(), &install)
} }
func skipCheckStatus(service types.ServiceConfig) bool { func skipCheckStatus(service types.ServiceConfig) bool {
@@ -1477,9 +1633,6 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false) synAppInstall(containersMap, &installed, false)
} }
resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap)
installDTO := response.AppInstallDTO{ installDTO := response.AppInstallDTO{
ID: installed.ID, ID: installed.ID,
Name: installed.Name, Name: installed.Name,
@@ -1501,16 +1654,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website, Website: installed.App.Website,
Document: installed.App.Document, Document: installed.App.Document,
}, },
Favorite: installed.Favorite, Favorite: installed.Favorite,
SortOrder: installed.SortOrder, SortOrder: installed.SortOrder,
Container: installed.ContainerName, Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName), ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
Env: envMap,
} }
if !updated && !checkUpdate { if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
continue continue
} }
@@ -1518,7 +1669,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" { if installed.Version == "latest" {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1547,7 +1698,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 { if len(versions) == 0 {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1579,7 +1730,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO) res = append(res, installDTO)
} }
} else if checkUpdate { } else if checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
} }
@@ -1822,10 +1973,6 @@ func isHostModel(dockerCompose string) bool {
} }
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error { func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil)
}
func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error {
entries, err := os.ReadDir(srcDir) entries, err := os.ReadDir(srcDir)
if err != nil { if err != nil {
return err return err
@@ -1837,9 +1984,6 @@ func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, cre
srcPath := path.Join(srcDir, entry.Name()) srcPath := path.Join(srcDir, entry.Name())
dstPath := path.Join(dstDir, entry.Name()) dstPath := path.Join(dstDir, entry.Name())
if !fileOp.Stat(dstPath) { if !fileOp.Stat(dstPath) {
if createdPaths != nil {
*createdPaths = append(*createdPaths, dstPath)
}
if entry.IsDir() { if entry.IsDir() {
if err := fileOp.CopyDir(srcPath, dstDir); err != nil { if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
return err return err
@@ -1854,7 +1998,7 @@ func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, cre
if !entry.IsDir() { if !entry.IsDir() {
continue continue
} }
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil { if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
return err return err
} }
} }
@@ -2017,7 +2161,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
func handleDefaultServer(appInstall *model.AppInstall) error { func handleDefaultServer(appInstall *model.AppInstall) error {
installDir := appInstall.GetPath() installDir := appInstall.GetPath()
defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf") defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
content, err := fileOp.GetContent(defaultConfigPath) content, err := fileOp.GetContent(defaultConfigPath)
if err != nil { if err != nil {
@@ -2031,7 +2175,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
} }
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error { func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl") sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if !fileOp.Stat(sslDir) { if !fileOp.Stat(sslDir) {
return errors.New("ssl dir not found") return errors.New("ssl dir not found")
@@ -2061,7 +2205,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID}) _ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}() }()
} }
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf") defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath) content, err := os.ReadFile(defaultConfigPath)
if err != nil { if err != nil {
return err return err
@@ -2153,25 +2297,13 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
} }
func hasLinkDB(installID uint) bool { func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false hasDB := false
if len(resourceKeys) > 0 { if len(resources) > 0 {
for _, resourceKey := range resourceKeys { for _, resource := range resources {
if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql || if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql ||
resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster || resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster ||
resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster { resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster {
hasDB = true hasDB = true
break break
} }
+6 -78
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"bufio" "bufio"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -373,7 +372,7 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
targetPath = strings.TrimPrefix(targetPath, "/") targetPath = strings.TrimPrefix(targetPath, "/")
} }
if _, err := client.Upload(context.Background(), fileItem, targetPath); err != nil { if _, err := client.Upload(fileItem, targetPath); err != nil {
return false, err return false, err
} }
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel")) _, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
@@ -438,10 +437,6 @@ type backupClientHelper struct {
} }
func NewBackupClientMap(ids []string) map[string]backupClientHelper { func NewBackupClientMap(ids []string) map[string]backupClientHelper {
return NewBackupClientMapWithContext(context.Background(), ids)
}
func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string]backupClientHelper {
var accounts []model.BackupAccount var accounts []model.BackupAccount
var idItems []uint var idItems []uint
for i := 0; i < len(ids); i++ { for i := 0; i < len(ids); i++ {
@@ -451,7 +446,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
accounts, _ = backupRepo.List(repo.WithByIDs(idItems)) accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
clientMap := make(map[string]backupClientHelper) clientMap := make(map[string]backupClientHelper)
for _, item := range accounts { for _, item := range accounts {
backClient, err := newClientWithContext(ctx, &item, true) backClient, err := newClient(&item, true)
itemHelper := backupClientHelper{ itemHelper := backupClientHelper{
client: backClient, client: backClient,
name: item.Name, name: item.Name,
@@ -468,11 +463,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
return clientMap return clientMap
} }
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure bool) error { func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint) error {
return uploadWithMapWithContext(context.Background(), taskItem, accountMap, src, dst, accountIDs, downloadAccountID, retry, cleanOnFailure, true)
}
func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure, removeSrc bool) error {
accounts := strings.Split(accountIDs, ",") accounts := strings.Split(accountIDs, ",")
for _, account := range accounts { for _, account := range accounts {
if len(account) == 0 { if len(account) == 0 {
@@ -498,13 +489,10 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
"backup": name, "backup": name,
})) }))
for i := 0; i < int(retry)+1; i++ { for i := 0; i < int(retry)+1; i++ {
_, err := itemBackup.client.Upload(ctx, src, path.Join(itemBackup.backupPath, dst)) _, err := itemBackup.client.Upload(src, path.Join(itemBackup.backupPath, dst))
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err) taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
if err != nil { if err != nil {
if account == fmt.Sprintf("%d", downloadAccountID) { if account == fmt.Sprintf("%d", downloadAccountID) {
if cleanOnFailure {
cleanupCronjobBackupArtifacts(accountMap, src, dst)
}
return err return err
} }
} else { } else {
@@ -514,71 +502,11 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
itemBackup.hasBackup = true itemBackup.hasBackup = true
accountMap[account] = itemBackup accountMap[account] = itemBackup
} }
if removeSrc { os.RemoveAll(src)
os.RemoveAll(src)
}
return nil return nil
} }
func cleanupCronjobBackupArtifacts(accountMap map[string]backupClientHelper, src, dst string) {
if err := os.RemoveAll(src); err != nil {
global.LOG.Errorf("remove failed local cronjob backup file %s failed, err: %v", src, err)
}
for _, account := range accountMap {
if !account.isOk {
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, dst)); err != nil {
global.LOG.Errorf("remove failed cronjob backup file %s failed, err: %v", dst, err)
}
}
}
func markBackupFailed(recordID uint, backupErr error) {
_ = backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": backupErr.Error()})
record, err := backupRepo.GetRecord(repo.WithByID(recordID))
if err != nil || record.ID == 0 {
global.LOG.Errorf("load failed backup record %d for cleanup failed, err: %v", recordID, err)
return
}
filePath := path.Join(record.FileDir, record.FileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, filePath)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed local backup file %s failed, err: %v", filePath, err)
}
cleaned := make(map[string]struct{})
for _, accountID := range strings.Split(record.SourceAccountIDs, ",") {
if accountID == "" {
continue
}
if _, ok := cleaned[accountID]; ok {
continue
}
cleaned[accountID] = struct{}{}
id, err := strconv.Atoi(accountID)
if err != nil {
global.LOG.Errorf("parse backup account %s for failed backup cleanup failed, err: %v", accountID, err)
continue
}
account, storageClient, err := NewBackupClientWithID(uint(id))
if err != nil {
global.LOG.Errorf("new backup client for failed backup cleanup failed, err: %v", err)
continue
}
if _, err := storageClient.Delete(path.Join(account.BackupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed backup file %s failed, err: %v", filePath, err)
}
}
}
func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) { func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
return newClientWithContext(context.Background(), account, isEncrypt)
}
func newClientWithContext(ctx context.Context, account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
varMap := make(map[string]interface{}) varMap := make(map[string]interface{})
if len(account.Vars) != 0 { if len(account.Vars) != 0 {
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
@@ -603,7 +531,7 @@ func newClientWithContext(ctx context.Context, account *model.BackupAccount, isE
varMap["password"] = account.Credential varMap["password"] = account.Credential
} }
client, err := cloud_storage.NewCloudStorageClientWithContext(ctx, account.Type, varMap) client, err := cloud_storage.NewCloudStorageClient(account.Type, varMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+10 -107
View File
@@ -63,58 +63,11 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err return nil, err
} }
if !req.IsImmediate { if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
markBackupFailed(record.ID, err) return nil, err
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
} }
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil
}
func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) {
itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
record := &model.BackupRecord{
Type: "app",
Name: install.App.Key,
DetailName: install.Name,
SourceAccountIDs: "1",
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
TaskID: parentTask.TaskID,
Status: constant.StatusWaiting,
}
if err := backupRepo.CreateRecord(record); err != nil {
return nil, err
}
if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return record, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil return record, nil
} }
@@ -190,7 +143,7 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -231,11 +184,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
return err return err
} }
defer func() { defer func() {
if isRollback { _, _ = compose.Up(install.GetComposePath())
_, _ = compose.UpWithoutPull(install.GetComposePath())
} else {
_, _ = compose.Up(install.GetComposePath())
}
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
}() }()
@@ -312,7 +261,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if err != nil { if err != nil {
return err return err
} }
newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name) newDB, err := reCreateDB(db.ID, database, backupEnvMap)
if err != nil { if err != nil {
return err return err
} }
@@ -440,16 +389,10 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
return nil return nil
} }
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) { func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) {
mysqlService := NewIMysqlService() mysqlService := NewIMysqlService()
ctx := context.Background() ctx := context.Background()
if err := deleteMysqlDatabaseForResourceOwner( _ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true})
ctx,
dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true},
dto.DBResource{Type: constant.TypeApp, Name: appInstallName},
); err != nil {
return nil, err
}
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4") dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{ createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
@@ -457,57 +400,17 @@ func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{
From: database.From, From: database.From,
Database: database.Name, Database: database.Name,
Format: dbInfo.Format, Format: dbInfo.Format,
Username: dbInfo.User,
Password: dbInfo.Password,
Permission: "%", Permission: "%",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(dbInfo.User) != 0 {
if err := ensureMysqlDBUser(mysqlService, database, dbInfo); err != nil {
return nil, err
}
}
updateCronjobsDBRef(dbID, createDB.ID) updateCronjobsDBRef(dbID, createDB.ID)
return createDB, nil return createDB, nil
} }
func ensureMysqlDBUser(mysqlService IMysqlService, database model.Database, dbInfo dbRecreateInfo) error {
const host = "%"
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
var oldUser dto.MysqlUser
exists := false
for _, user := range users {
if user.Username == dbInfo.User && user.Host == host && !user.IsDelete {
oldUser = user
exists = true
break
}
}
if exists {
if len(oldUser.Password) != 0 && oldUser.Password != dbInfo.Password {
return buserr.New("ErrDbUserNotValid")
}
} else {
if err := mysqlService.CreateUser(dto.MysqlUserCreate{
Database: database.Name,
Username: dbInfo.User,
Host: host,
Password: dbInfo.Password,
}); err != nil {
return err
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbInfo.Name,
Username: dbInfo.User,
Host: host,
})
}
func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) { func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) {
postgresqlService := NewIPostgresqlService() postgresqlService := NewIPostgresqlService()
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{ _ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
+2 -2
View File
@@ -98,7 +98,7 @@ func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil { if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -163,7 +163,7 @@ func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
go func() { go func() {
defer composeCtx.close() defer composeCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+18 -82
View File
@@ -106,7 +106,7 @@ func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil { if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -158,7 +158,7 @@ func handleContainerBackup(containerName string, parentTask *task.Task, recordID
go func() { go func() {
defer backupCtx.close() defer backupCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -604,40 +604,23 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil return nil
} }
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras)
}
endpointGroups := []map[string]*network.EndpointSettings{extras}
if primary != nil { if primary != nil {
endpointGroups = append(endpointGroups, primary.EndpointsConfig) removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig)
}
for _, endpoints := range endpointGroups {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil {
continue
}
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
} }
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras)
} }
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
if primary != nil { for netName, endpoint := range endpoints {
for _, endpoint := range primary.EndpointsConfig { if endpoint == nil || endpoint.IPAMConfig == nil {
if endpoint != nil { continue
endpoint.MacAddress = ""
}
} }
} info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
for _, endpoint := range extras { if err != nil {
if endpoint != nil { continue
endpoint.MacAddress = ""
} }
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
} }
} }
@@ -656,7 +639,7 @@ func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, end
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) { if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
endpoint.IPAMConfig.IPv6Address = "" endpoint.IPAMConfig.IPv6Address = ""
} }
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 { if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" {
endpoint.IPAMConfig = nil endpoint.IPAMConfig = nil
} }
} }
@@ -793,27 +776,13 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
if name == "host" || name == "none" { if name == "host" || name == "none" {
continue continue
} }
if endpoint == nil { endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
if name == primaryName {
config.EndpointsConfig[name] = &network.EndpointSettings{}
} else {
extraNetworks[name] = &network.EndpointSettings{}
}
continue
}
endpointSetting := &network.EndpointSettings{
Links: append([]string(nil), endpoint.Links...),
Aliases: append([]string(nil), endpoint.Aliases...),
DriverOpts: cloneStringMap(endpoint.DriverOpts),
GwPriority: endpoint.GwPriority,
}
if endpoint.IPAMConfig != nil { if endpoint.IPAMConfig != nil {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAMConfig.IPv4Address, IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address, IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
} }
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") { } else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress, IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address, IPv6Address: endpoint.GlobalIPv6Address,
@@ -831,39 +800,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
return config, extraNetworks return config, extraNetworks
} }
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
return false
}
for name, endpoint := range endpoints {
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
if strings.Contains(err.Error(), "network "+name+":") {
endpoint.IPAMConfig = nil
return true
}
}
cleared := false
for name, endpoint := range endpoints {
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
endpoint.IPAMConfig = nil
cleared = true
}
}
return cleared
}
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
if networkSettings == nil || name == "bridge" {
return false
}
endpoint := networkSettings.Networks[name]
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
}
func cloneContainerConfig(config *container.Config) *container.Config { func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil { if config == nil {
return &container.Config{} return &container.Config{}
+2 -2
View File
@@ -54,7 +54,7 @@ func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
} }
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil { if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -98,7 +98,7 @@ func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
) )
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+2 -43
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path" "path"
@@ -40,7 +39,6 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
TaskID: req.TaskID, TaskID: req.TaskID,
Status: constant.StatusWaiting, Status: constant.StatusWaiting,
Description: req.Description, Description: req.Description,
Args: encodeBackupArgs(req.Args),
} }
if err := backupRepo.CreateRecord(record); err != nil { if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err) global.LOG.Errorf("save backup record failed, err: %v", err)
@@ -49,7 +47,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args} databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args}
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil { if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -102,7 +100,7 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -145,14 +143,6 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
if !isRollback { if !isRollback {
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout))) rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
var rollbackArgs []string
if req.BackupRecordID != 0 {
record, err := backupRepo.GetRecord(repo.WithByID(req.BackupRecordID))
if err != nil {
return err
}
rollbackArgs = decodeBackupArgs(record.Args)
}
if err := cli.Backup(client.BackupInfo{ if err := cli.Backup(client.BackupInfo{
Name: req.DetailName, Name: req.DetailName,
Type: req.Type, Type: req.Type,
@@ -160,7 +150,6 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
Format: dbInfo.Format, Format: dbInfo.Format,
TargetDir: path.Dir(rollbackFile), TargetDir: path.Dir(rollbackFile),
FileName: path.Base(rollbackFile), FileName: path.Base(rollbackFile),
Args: rollbackArgs,
}); err != nil { }); err != nil {
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err) return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
} }
@@ -253,36 +242,6 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
return nil return nil
} }
func encodeBackupArgs(args []string) string {
var items []string
for _, arg := range args {
if len(arg) != 0 {
items = append(items, arg)
}
}
if len(items) == 0 {
return ""
}
data, err := json.Marshal(items)
if err != nil {
global.LOG.Warnf("marshal backup args failed: %v", err)
return ""
}
return string(data)
}
func decodeBackupArgs(value string) []string {
if len(value) == 0 {
return nil
}
var args []string
if err := json.Unmarshal([]byte(value), &args); err != nil {
global.LOG.Warnf("unmarshal backup args failed: %v", err)
return nil
}
return args
}
func loadSqlFile(file string) (string, error) { func loadSqlFile(file string) (string, error) {
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") { if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
return file, nil return file, nil
+1 -3
View File
@@ -39,12 +39,10 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
} }
if err := backupRepo.CreateRecord(record); err != nil { if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err) global.LOG.Errorf("save backup record failed, err: %v", err)
return err
} }
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName} databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil { if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -90,7 +88,7 @@ func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID u
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+1 -2
View File
@@ -59,7 +59,6 @@ func (u *BackupService) RedisBackup(req dto.CommonBackup) error {
} }
if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil { if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -130,7 +129,7 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour) itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour)
go func() { go func() {
if err := itemTask.Execute(); err != nil { if err := itemTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+1 -2
View File
@@ -54,7 +54,6 @@ func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error {
return err return err
} }
if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err) global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err)
return err return err
} }
@@ -236,7 +235,7 @@ func handleWebsiteBackup(website *model.Website, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+238 -141
View File
@@ -15,7 +15,6 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -33,6 +32,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -53,8 +53,6 @@ import (
type ContainerService struct{} type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface { type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions List() []dto.ContainerOptions
@@ -71,7 +69,6 @@ type IContainerService interface {
ComposeOperation(req dto.ComposeOperation) error ComposeOperation(req dto.ComposeOperation) error
TestCompose(req dto.ComposeCreate) (bool, error) TestCompose(req dto.ComposeCreate) (bool, error)
ComposeUpdate(req dto.ComposeUpdate) error ComposeUpdate(req dto.ComposeUpdate) error
ComposePin(req dto.ComposePin) error
ComposeLogClean(req dto.ComposeLogClean) error ComposeLogClean(req dto.ComposeLogClean) error
ContainerCreate(req dto.ContainerOperate, inThread bool) error ContainerCreate(req dto.ContainerOperate, inThread bool) error
@@ -486,19 +483,15 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
unlock := containerOperationLock.lock(req.Name) defer client.Close()
ctx := context.Background() ctx := context.Background()
newContainer, _ := client.ContainerInspect(ctx, req.Name) newContainer, _ := client.ContainerInspect(ctx, req.Name)
if newContainer.ContainerJSONBase != nil { if newContainer.ContainerJSONBase != nil {
unlock()
_ = client.Close()
return buserr.New("ErrContainerName") return buserr.New("ErrContainerName")
} }
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
unlock()
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err) global.LOG.Errorf("new task for create container failed, err: %v", err)
return err return err
} }
@@ -534,20 +527,18 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
normalizeContainerEndpointSettings(ctx, client, networkConf, nil) removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
if con.ID != "" { _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
}
return err return err
} }
err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}) _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
return fmt.Errorf("create successful but start failed, err: %v", err) return fmt.Errorf("create successful but start failed, err: %v", err)
} }
return nil return nil
@@ -555,16 +546,12 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if inThread { if inThread {
go func() { go func() {
defer unlock()
defer client.Close()
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error()) global.LOG.Error(err.Error())
} }
}() }()
return nil return nil
} }
defer unlock()
defer client.Close()
return taskItem.Execute() return taskItem.Execute()
} }
@@ -585,7 +572,21 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Image = oldContainer.Config.Image data.Image = oldContainer.Config.Image
if oldContainer.NetworkSettings != nil { if oldContainer.NetworkSettings != nil {
for net, val := range oldContainer.NetworkSettings.Networks { for net, val := range oldContainer.NetworkSettings.Networks {
data.Networks = append(data.Networks, loadContainerNetworkInfo(net, val)) netItem := dto.ContainerNetwork{
Network: net,
MacAddr: val.MacAddress,
}
if val.IPAMConfig != nil {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAMConfig.IPv4Address
netItem.Ipv6 = val.IPAMConfig.IPv6Address
}
} else {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAddress
}
}
data.Networks = append(data.Networks, netItem)
} }
} }
@@ -631,40 +632,141 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
return &data, nil return &data, nil
} }
func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) dto.ContainerNetwork { func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
item := dto.ContainerNetwork{Network: name} client, err := docker.NewDockerClient()
if endpoint == nil { if err != nil {
return item return err
} }
item.MacAddr = endpoint.MacAddress defer client.Close()
item.Links = append([]string(nil), endpoint.Links...) ctx := context.Background()
item.Aliases = append([]string(nil), endpoint.Aliases...) oldContainer, err := client.ContainerInspect(ctx, req.Name)
item.DriverOpts = cloneStringMap(endpoint.DriverOpts) if err != nil {
item.GwPriority = endpoint.GwPriority return err
if endpoint.IPAMConfig != nil {
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
} }
if name != "bridge" {
if endpoint.IPAMConfig != nil { taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
item.Ipv4 = endpoint.IPAMConfig.IPv4Address if err != nil {
item.Ipv6 = endpoint.IPAMConfig.IPv6Address global.LOG.Errorf("new task for create container failed, err: %v", err)
} else { return err
item.Ipv4 = endpoint.IPAddress }
item.Ipv6 = endpoint.GlobalIPv6Address go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error {
err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err)
if err != nil {
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return err
}
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("update container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("update successful but start failed, err: %v", err)
}
return nil
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
} }
} }()
return item
return nil
} }
func cloneStringMap(source map[string]string) map[string]string { func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
if len(source) == 0 { client, err := docker.NewDockerClient()
return nil if err != nil {
return err
} }
result := make(map[string]string, len(source)) defer client.Close()
for key, value := range source { ctx := context.Background()
result[key] = value taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
} }
return result go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
for _, item := range req.Names {
var oldContainer container.InspectResponse
taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", item)
oldContainer, err = client.ContainerInspect(ctx, item)
if err != nil {
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error {
config := oldContainer.Config
config.Image = req.Image
hostConf := oldContainer.HostConfig
err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
if err != nil {
return err
}
con, err := createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("upgrade container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
}
return nil
}, nil)
}
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
} }
func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
@@ -674,8 +776,6 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
return err return err
} }
defer client.Close() defer client.Close()
unlock := containerOperationLock.lock(req.Name, req.NewName)
defer unlock()
newContainer, _ := client.ContainerInspect(ctx, req.NewName) newContainer, _ := client.ContainerInspect(ctx, req.NewName)
if newContainer.ContainerJSONBase != nil { if newContainer.ContainerJSONBase != nil {
@@ -720,48 +820,44 @@ func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error {
} }
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error { func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background() ctx := context.Background()
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return err return err
} }
defer client.Close()
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
_ = client.Close()
return fmt.Errorf("new task for container commit failed, err: %v", err) return fmt.Errorf("new task for container commit failed, err: %v", err)
} }
for _, item := range req.Names { for _, item := range req.Names {
item := item
taskItem.AddSubTask(item, func(t *task.Task) error { taskItem.AddSubTask(item, func(t *task.Task) error {
unlock := containerOperationLock.lock(item)
defer unlock()
var operationErr error
switch req.Operation { switch req.Operation {
case constant.ContainerOpStart: case constant.ContainerOpStart:
operationErr = client.ContainerStart(ctx, item, container.StartOptions{}) err = client.ContainerStart(ctx, item, container.StartOptions{})
case constant.ContainerOpStop: case constant.ContainerOpStop:
operationErr = client.ContainerStop(ctx, item, container.StopOptions{}) err = client.ContainerStop(ctx, item, container.StopOptions{})
case constant.ContainerOpRestart: case constant.ContainerOpRestart:
operationErr = client.ContainerRestart(ctx, item, container.StopOptions{}) err = client.ContainerRestart(ctx, item, container.StopOptions{})
case constant.ContainerOpKill: case constant.ContainerOpKill:
operationErr = client.ContainerKill(ctx, item, "SIGKILL") err = client.ContainerKill(ctx, item, "SIGKILL")
case constant.ContainerOpPause: case constant.ContainerOpPause:
operationErr = client.ContainerPause(ctx, item) err = client.ContainerPause(ctx, item)
case constant.ContainerOpUnpause: case constant.ContainerOpUnpause:
operationErr = client.ContainerUnpause(ctx, item) err = client.ContainerUnpause(ctx, item)
case constant.ContainerOpRemove: case constant.ContainerOpRemove:
operationErr = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
} }
return operationErr return err
}, nil) }, nil)
} }
go func() { go func() {
defer client.Close()
_ = taskItem.Execute() _ = taskItem.Execute()
}() }()
return nil return err
} }
func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
@@ -770,8 +866,6 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
return err return err
} }
defer client.Close() defer client.Close()
unlock := containerOperationLock.lock(req.Name)
defer unlock()
ctx := context.Background() ctx := context.Background()
containerItem, err := client.ContainerInspect(ctx, req.Name) containerItem, err := client.ContainerInspect(ctx, req.Name)
if err != nil { if err != nil {
@@ -942,7 +1036,6 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if cmd.CheckIllegal(container, since, tail) { if cmd.CheckIllegal(container, since, tail) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
ctx := c.Request.Context()
commandArg := []string{"logs", container} commandArg := []string{"logs", container}
dockerCommand := global.CONF.DockerConfig.Command dockerCommand := global.CONF.DockerConfig.Command
if containerType == "compose" { if containerType == "compose" {
@@ -972,11 +1065,10 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
} }
var dockerCmd *exec.Cmd var dockerCmd *exec.Cmd
if containerType == "compose" && dockerCommand == "docker-compose" { if containerType == "compose" && dockerCommand == "docker-compose" {
dockerCmd = exec.CommandContext(ctx, "docker-compose", commandArg...) dockerCmd = exec.Command("docker-compose", commandArg...)
} else { } else {
dockerCmd = exec.CommandContext(ctx, "docker", commandArg...) dockerCmd = exec.Command("docker", commandArg...)
} }
dockerCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := dockerCmd.StdoutPipe() stdout, err := dockerCmd.StdoutPipe()
if err != nil { if err != nil {
return err return err
@@ -985,20 +1077,15 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if err := dockerCmd.Start(); err != nil { if err := dockerCmd.Start(); err != nil {
return err return err
} }
done := make(chan struct{}) defer func() {
go func() { if dockerCmd.Process != nil {
select { _ = dockerCmd.Process.Kill()
case <-ctx.Done(): _ = dockerCmd.Wait()
killContainerLogProcess(dockerCmd)
case <-done:
} }
}() }()
defer close(done)
tempFile, err := os.CreateTemp("", "cmd_output_*.txt") tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil { if err != nil {
killContainerLogProcess(dockerCmd)
_ = dockerCmd.Wait()
return err return err
} }
defer tempFile.Close() defer tempFile.Close()
@@ -1007,19 +1094,31 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
global.LOG.Errorf("os.Remove() failed: %v", err) global.LOG.Errorf("os.Remove() failed: %v", err)
} }
}() }()
copyErr := copyContainerLogOutput(tempFile, stdout) errCh := make(chan error, 1)
waitErr := dockerCmd.Wait() go func() {
if copyErr != nil { scanner := bufio.NewScanner(stdout)
return copyErr var ansiRegex = re.GetRegex(re.AnsiEscapePattern)
} for scanner.Scan() {
if waitErr != nil { line := scanner.Text()
if ctx.Err() != nil { cleanLine := ansiRegex.ReplaceAllString(line, "")
return ctx.Err() if _, err := tempFile.WriteString(cleanLine + "\n"); err != nil {
errCh <- err
return
}
} }
return waitErr if err := scanner.Err(); err != nil {
} errCh <- err
if _, err := tempFile.Seek(0, io.SeekStart); err != nil { return
return err }
errCh <- nil
}()
select {
case err := <-errCh:
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(40 * time.Second):
global.LOG.Errorf("Download container logs timeout reached")
} }
info, _ := tempFile.Stat() info, _ := tempFile.Stat()
@@ -1029,36 +1128,6 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
return nil return nil
} }
func copyContainerLogOutput(dst io.Writer, src io.Reader) error {
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
cleanLine := containerLogAnsiRegex.ReplaceAllString(line, "")
if _, writeErr := io.WriteString(dst, cleanLine); writeErr != nil {
return writeErr
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func killContainerLogProcess(command *exec.Cmd) {
if command == nil || command.Process == nil {
return
}
if pgid, err := syscall.Getpgid(command.Process.Pid); err == nil {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
return
}
_ = command.Process.Kill()
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) { func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -1736,7 +1805,7 @@ func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerLis
return data return data
} }
func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error) { func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
portMap := make(nat.PortMap) portMap := make(nat.PortMap)
if len(ports) == 0 { if len(ports) == 0 {
return portMap, nil return portMap, nil
@@ -1761,7 +1830,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem} portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
} }
for i := hostStart; i <= hostEnd; i++ { for i := hostStart; i <= hostEnd; i++ {
if checkInUse && common.ScanPortWithIP(port.HostIP, i) { if common.ScanPortWithIP(port.HostIP, i) {
return portMap, buserr.WithDetail("ErrPortInUsed", i, nil) return portMap, buserr.WithDetail("ErrPortInUsed", i, nil)
} }
} }
@@ -1772,7 +1841,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
} else { } else {
portItem, _ = strconv.Atoi(port.HostPort) portItem, _ = strconv.Atoi(port.HostPort)
} }
if checkInUse && common.ScanPortWithIP(port.HostIP, portItem) { if common.ScanPortWithIP(port.HostIP, portItem) {
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil) return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
} }
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
@@ -1791,7 +1860,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
} }
var networkConf network.NetworkingConfig var networkConf network.NetworkingConfig
portMap, err := checkPortStats(req.ExposedPorts, isCreate) portMap, err := checkPortStats(req.ExposedPorts)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@@ -1819,21 +1888,15 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
case "host", "none", "bridge": case "host", "none", "bridge":
hostConf.NetworkMode = container.NetworkMode(item.Network) hostConf.NetworkMode = container.NetworkMode(item.Network)
} }
endpoint := &network.EndpointSettings{ if item.Ipv4 != "" || item.Ipv6 != "" {
Links: append([]string(nil), item.Links...), networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{
Aliases: append([]string(nil), item.Aliases...), IPAMConfig: &network.EndpointIPAMConfig{
DriverOpts: cloneStringMap(item.DriverOpts), IPv4Address: item.Ipv4,
GwPriority: item.GwPriority, IPv6Address: item.Ipv6,
MacAddress: item.MacAddr, }, MacAddress: item.MacAddr}
} else {
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{}
} }
if item.Ipv4 != "" || item.Ipv6 != "" || len(item.LinkLocalIPs) != 0 {
endpoint.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: item.Ipv4,
IPv6Address: item.Ipv6,
LinkLocalIPs: append([]string(nil), item.LinkLocalIPs...),
}
}
networkConf.EndpointsConfig[item.Network] = endpoint
} }
} else { } else {
return nil, nil, nil, fmt.Errorf("please set up the network") return nil, nil, nil, fmt.Errorf("please set up the network")
@@ -1880,6 +1943,43 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
return &config, &hostConf, &networkConf, nil return &config, &hostConf, &networkConf, nil
} }
func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) {
ctx := context.Background()
oldContainer, err := createContainerWithOldNetworks(ctx, client, config, hostConf, networkConf, name)
if err != nil {
global.LOG.Errorf("recreate after container update failed, err: %v", err)
return
}
if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil {
global.LOG.Errorf("restart after container update failed, err: %v", err)
}
global.LOG.Info("recreate after container update successful")
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
removeUnsupportedEndpointStaticIPAM(client, networkConf, extraNetworks)
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]); err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper { func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
var datas []dto.VolumeHelper var datas []dto.VolumeHelper
for _, bind := range binds { for _, bind := range binds {
@@ -2026,9 +2126,6 @@ func loadComposeCount(client *client.Client) int {
} }
} }
for _, compose := range composeCreatedByLocal { for _, compose := range composeCreatedByLocal {
if len(compose.Path) == 0 {
continue
}
if _, has := composeMap[compose.Name]; !has { if _, has := composeMap[compose.Name]; !has {
composeMap[compose.Name] = struct{}{} composeMap[compose.Name] = struct{}{}
} }
+33 -323
View File
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"sort" "sort"
@@ -24,10 +23,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose" "github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"gopkg.in/yaml.v3"
) )
const composeProjectLabel = "com.docker.compose.project" const composeProjectLabel = "com.docker.compose.project"
@@ -55,15 +52,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return 0, nil, err return 0, nil, err
} }
composeRecords, _ := composeRepo.ListRecord() composeCreatedByLocal, _ := composeRepo.ListRecord()
pinnedByName := make(map[string]bool, len(composeRecords))
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
for _, record := range composeRecords {
pinnedByName[record.Name] = record.IsPinned
if len(record.Path) != 0 {
composeCreatedByLocal = append(composeCreatedByLocal, record)
}
}
composeLocalMap := make(map[string]dto.ComposeInfo) composeLocalMap := make(map[string]dto.ComposeInfo)
for _, localItem := range composeCreatedByLocal { for _, localItem := range composeCreatedByLocal {
composeItemLocal := dto.ComposeInfo{ composeItemLocal := dto.ComposeInfo{
@@ -147,7 +136,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap { for key, value := range mergedMap {
value.Name = key value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile) value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
value.IsPinned = pinnedByName[key]
records = append(records, value) records = append(records, value)
} }
if len(req.Info) != 0 { if len(req.Info) != 0 {
@@ -161,21 +149,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
} }
} }
} }
if req.ExcludeAppStore {
length, count := len(records), 0
for count < length {
if records[count].CreatedBy == "Apps" {
records = append(records[:count], records[(count+1):]...)
length--
} else {
count++
}
}
}
sort.Slice(records, func(i, j int) bool { sort.Slice(records, func(i, j int) bool {
if records[i].IsPinned != records[j].IsPinned {
return records[i].IsPinned
}
return records[i].CreatedAt > records[j].CreatedAt return records[i].CreatedAt > records[j].CreatedAt
}) })
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
@@ -215,62 +189,53 @@ func composeFileExists(workdir, configFile string) bool {
} }
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := validateComposeCreateName(req); err != nil { if cmd.CheckIllegal(req.Path) {
return false, err
}
if hasIllegalComposeCreateInput(req) {
return false, buserr.New("ErrCmdIllegal") return false, buserr.New("ErrCmdIllegal")
} }
projectName, err := resolveComposeCreateProjectName(req) composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
if err != nil { if composeItem.ID != 0 {
return false, buserr.New("ErrRecordExist")
}
if err := u.loadPath(&req); err != nil {
return false, err return false, err
} }
if err := checkComposeCreateDuplicate(req, projectName); err != nil { if err := newComposeEnv(req.Path, req.Env); err != nil {
return false, err return false, err
} }
cmd := getComposeCmd(req.Path, "config")
stdout, err := cmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("docker-compose config failed, std: %s, err: %v", string(stdout), err)
}
return true, nil return true, nil
} }
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if err := validateComposeCreateName(req); err != nil { if cmd.CheckIllegal(req.Name, req.Path) {
return err
}
if hasIllegalComposeCreateInput(req) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
projectName, err := resolveComposeCreateProjectName(req)
if err != nil {
return err
}
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
return err
}
if err := u.loadPath(&req); err != nil { if err := u.loadPath(&req); err != nil {
return err return err
} }
if err := newComposeEnv(req.Path, req.Env); err != nil { if req.From == "path" {
return err req.Name = path.Base(path.Dir(req.Path))
} }
req.Name = projectName
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
if err != nil { if err != nil {
return fmt.Errorf("new task for image build failed, err: %v", err) return fmt.Errorf("new task for image build failed, err: %v", err)
} }
if err := newComposeEnv(req.Path, req.Env); err != nil {
return err
}
go func() { go func() {
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name) err := compose.UpWithTask(req.Path, t, req.ForcePull)
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err) t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
if err != nil { if err != nil {
_, _ = compose.Down(req.Path, req.Name) _, _ = compose.Down(req.Path)
return err return err
} }
recordName := strings.ToLower(req.Name) _ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
if record.ID == 0 {
_ = composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: req.Path})
} else {
_ = composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": req.Path})
}
return nil return nil
}, nil) }, nil)
_ = taskItem.Execute() _ = taskItem.Execute()
@@ -279,243 +244,6 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
return nil return nil
} }
func checkComposeRecordName(name string) error {
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func checkComposeCreateDuplicate(req dto.ComposeCreate, projectName string) error {
if err := checkComposeRecordName(projectName); err != nil {
return err
}
if req.From == "path" {
return nil
}
composeItem, _ := composeRepo.GetRecord(repo.WithByPath(composeCreatePath(req)))
if composeItem.ID != 0 && composeItem.Path != "" {
return buserr.New("ErrRecordExist")
}
return nil
}
func validateComposeCreateName(req dto.ComposeCreate) error {
if req.From == "path" {
name := strings.TrimSpace(req.Name)
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
return buserr.New("ErrComposeNameInvalid")
}
return nil
}
if !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
return buserr.New("ErrComposeNameInvalid")
}
return nil
}
func hasIllegalComposeCreateInput(req dto.ComposeCreate) bool {
if req.From == "path" {
return cmd.CheckIllegal(req.Name, req.Path)
}
return cmd.CheckIllegal(composeCreateDirName(req))
}
func composeCreateDirName(req dto.ComposeCreate) string {
dirName := strings.TrimSpace(req.DirName)
if dirName == "" {
// Keep compatibility with callers that used name as both the directory and
// Compose project name before dirName was introduced.
return strings.TrimSpace(req.Name)
}
return dirName
}
func composeCreatePath(req dto.ComposeCreate) string {
return filepath.Join(global.Dir.DataDir, "docker", "compose", composeCreateDirName(req), "docker-compose.yml")
}
func resolveComposeCreateProjectName(req dto.ComposeCreate) (string, error) {
if req.From == "path" {
envPath, err := createComposeTempFile(
filepath.Dir(primaryComposePath(req.Path)),
".1panel-compose-*.env",
req.Env,
)
if err != nil {
return "", err
}
defer os.Remove(envPath)
return resolveComposeProjectName(req.Path, req.Name, envPath)
}
dir := filepath.Dir(composeCreatePath(req))
cleanupDir, err := prepareComposeStagingDir(dir)
if err != nil {
return "", err
}
defer cleanupDir()
composePath, err := createComposeTempFile(dir, ".1panel-compose-*.yml", req.File)
if err != nil {
return "", err
}
defer os.Remove(composePath)
envPath, err := createComposeTempFile(dir, ".1panel-compose-*.env", req.Env)
if err != nil {
return "", err
}
defer os.Remove(envPath)
return resolveComposeProjectName(composePath, "", envPath)
}
func createComposeTempFile(dir, pattern, content string) (string, error) {
file, err := os.CreateTemp(dir, pattern)
if err != nil {
return "", err
}
filePath := file.Name()
if _, err := file.WriteString(content); err != nil {
_ = file.Close()
_ = os.Remove(filePath)
return "", err
}
if err := file.Close(); err != nil {
_ = os.Remove(filePath)
return "", err
}
return filePath, nil
}
func prepareComposeStagingDir(dir string) (func(), error) {
if err := os.MkdirAll(filepath.Dir(dir), os.ModePerm); err != nil {
return nil, err
}
created := false
if err := os.Mkdir(dir, os.ModePerm); err != nil {
if !errors.Is(err, os.ErrExist) {
return nil, err
}
} else {
created = true
}
return func() {
if created {
_ = os.Remove(dir)
}
}, nil
}
func resolveComposeProjectName(composePath, fallbackName, envFile string) (string, error) {
// Preserve the name resolved by Compose (including a top-level name) so the
// container label and the local record always use the same project identity.
parentName := normalizeComposeProjectName(path.Base(path.Dir(primaryComposePath(composePath))))
fallbackName = strings.TrimSpace(fallbackName)
stdout, err := runComposeConfig(composePath, "", envFile)
if err == nil {
projectName, parseErr := loadComposeProjectName(stdout)
if parseErr != nil {
return "", parseErr
}
if projectName != "" {
if !re.GetRegex(re.ComposeNamePattern).MatchString(projectName) {
return "", buserr.New("ErrComposeNameInvalid")
}
return projectName, nil
}
if parentName != "" {
return parentName, nil
}
if fallbackName != "" {
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr != nil {
return "", fallbackErr
}
return fallbackName, nil
}
return "", buserr.New("ErrComposeProjectNameEmpty")
}
if !isComposeProjectNameEmptyError(err) {
return "", err
}
resolveErr := err
if parentName != "" {
if _, parentErr := runComposeConfig(composePath, parentName, envFile); parentErr == nil {
return parentName, nil
} else {
resolveErr = parentErr
}
}
if fallbackName != "" && fallbackName != parentName {
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr == nil {
return fallbackName, nil
} else {
return "", fallbackErr
}
}
if parentName == "" && fallbackName == "" {
return "", buserr.New("ErrComposeProjectNameEmpty")
}
return "", resolveErr
}
func runComposeConfig(composePath, projectName, envFile string) ([]byte, error) {
configCmd := getComposeCmdWithEnv(composePath, "config", envFile, projectName)
stdout, err := configCmd.Output()
if err != nil {
var stderr []byte
if exitErr, ok := err.(*exec.ExitError); ok {
stderr = exitErr.Stderr
}
return nil, fmt.Errorf("docker-compose config failed, std: %s, err: %v", mergeComposeOutput(stdout, stderr), err)
}
return stdout, nil
}
func mergeComposeOutput(stdout, stderr []byte) string {
outputs := make([]string, 0, 2)
if output := strings.TrimSpace(string(stdout)); output != "" {
outputs = append(outputs, output)
}
if output := strings.TrimSpace(string(stderr)); output != "" {
outputs = append(outputs, output)
}
return strings.Join(outputs, "\n")
}
func loadComposeProjectName(config []byte) (string, error) {
var project struct {
Name string `yaml:"name"`
}
if err := yaml.Unmarshal(config, &project); err != nil {
return "", buserr.WithDetail("ErrComposeProjectNameParse", err.Error(), err)
}
return strings.TrimSpace(project.Name), nil
}
func primaryComposePath(composePath string) string {
if index := strings.Index(composePath, ","); index >= 0 {
return composePath[:index]
}
return composePath
}
func normalizeComposeProjectName(name string) string {
name = re.GetRegex(re.ComposeDisallowedCharsPattern).
ReplaceAllString(strings.ToLower(strings.TrimSpace(name)), "")
return strings.TrimLeft(name, "_-")
}
func isComposeProjectNameEmptyError(err error) bool {
message := strings.ToLower(err.Error())
return strings.Contains(message, "project name must not be empty") ||
strings.Contains(message, "project name can't be empty")
}
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if len(req.Path) == 0 && req.Operation == "delete" { if len(req.Path) == 0 && req.Operation == "delete" {
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name)) _ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
@@ -539,15 +267,11 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
return nil return nil
} }
if req.Operation == "up" { if req.Operation == "up" {
if stdout, err := compose.Up(req.Path, req.Name); err != nil { if stdout, err := compose.Up(req.Path); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err) return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
} }
} else if req.Operation == "rebuild" {
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
}
} else { } else {
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil { if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err) return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
} }
} }
@@ -583,9 +307,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
return err return err
} }
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil { if err := compose.UpWithTask(req.Path, t, req.ForcePull); err != nil {
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err) global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil { if err := recreateCompose(string(oldFile), req.Path); err != nil {
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err) return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
} }
return fmt.Errorf("update failed when handle compose up, err: %s", err) return fmt.Errorf("update failed when handle compose up, err: %s", err)
@@ -599,20 +323,6 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
return nil return nil
} }
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
if record.ID == 0 {
if !req.IsPinned {
return nil
}
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
}
if !req.IsPinned && len(record.Path) == 0 {
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
}
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
}
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error { func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -675,15 +385,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error { func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
if req.From == "template" || req.From == "edit" { if req.From == "template" || req.From == "edit" {
composePath := composeCreatePath(*req) dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, req.Name)
dir := filepath.Dir(composePath)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil { if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err return err
} }
} }
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm) path := fmt.Sprintf("%s/docker-compose.yml", dir)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
if err != nil { if err != nil {
return err return err
} }
@@ -691,14 +401,14 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
write := bufio.NewWriter(file) write := bufio.NewWriter(file)
_, _ = write.WriteString(string(req.File)) _, _ = write.WriteString(string(req.File))
write.Flush() write.Flush()
req.Path = composePath req.Path = path
} }
return nil return nil
} }
func removeContainerForCompose(composeName, composePath string) error { func removeContainerForCompose(composeName, composePath string) error {
if _, err := os.Stat(composePath); err == nil { if _, err := os.Stat(composePath); err == nil {
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil { if stdout, err := compose.Operate(composePath, "down"); err != nil {
return errors.New(stdout) return errors.New(stdout)
} }
return nil return nil
@@ -723,7 +433,7 @@ func removeContainerForCompose(composeName, composePath string) error {
return nil return nil
} }
func recreateCompose(content, path, projectName string) error { func recreateCompose(content, path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil { if err != nil {
return err return err
@@ -733,7 +443,7 @@ func recreateCompose(content, path, projectName string) error {
_, _ = write.WriteString(content) _, _ = write.WriteString(content)
write.Flush() write.Flush()
if stdout, err := compose.Up(path, projectName); err != nil { if stdout, err := compose.Up(path); err != nil {
return errors.New(string(stdout)) return errors.New(string(stdout))
} }
return nil return nil
-663
View File
@@ -1,663 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTaskWithOps(task.GetTaskName(req.Name, task.TaskUpdate, task.TaskScopeContainer), func(t *task.Task) error {
t.LogStart(i18n.GetWithName("ContainerAcquireLock", req.Name))
unlock := containerOperationLock.lock(req.Name)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", req.Name), nil)
oldContainer, err := client.ContainerInspect(ctx, req.Name)
if err != nil {
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
}, newContainerSwitchTaskLogger(t))
if err != nil {
return fmt.Errorf("update container failed, err: %v", err)
}
if cleanupErr != nil {
taskItem.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
}, nil, 0, 0)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
var upgradeErrors []error
for _, item := range req.Names {
item := item
taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error {
t.Logf("----------------- %s -----------------", item)
t.LogStart(i18n.GetWithName("ContainerAcquireLock", item))
unlock := containerOperationLock.lock(item)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil)
oldContainer, inspectErr := client.ContainerInspect(ctx, item)
t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr)
if inspectErr != nil {
err := fmt.Errorf("reload container %s failed: %w", item, inspectErr)
upgradeErrors = append(upgradeErrors, err)
return err
}
config := cloneContainerConfig(oldContainer.Config)
config.Image = req.Image
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
}, newContainerSwitchTaskLogger(t))
if err != nil {
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr
}
if cleanupErr != nil {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
})
}
taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error {
return errors.Join(upgradeErrors...)
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
type containerSwitchClient interface {
ContainerStop(context.Context, string, container.StopOptions) error
ContainerRename(context.Context, string, string) error
ContainerStart(context.Context, string, container.StartOptions) error
ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerInspect(context.Context, string) (container.InspectResponse, error)
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
NetworkDisconnect(context.Context, string, string, bool) error
}
type containerInspectClient interface {
ContainerInspect(context.Context, string) (container.InspectResponse, error)
}
type containerOperationMutex struct {
mutex sync.Mutex
locks map[string]*containerOperationLockEntry
}
var containerOperationLock containerOperationMutex
type containerOperationLockEntry struct {
mutex sync.Mutex
references int
}
func (l *containerOperationMutex) lock(names ...string) func() {
nameSet := make(map[string]struct{}, len(names))
for _, name := range names {
if name != "" {
nameSet[name] = struct{}{}
}
}
orderedNames := make([]string, 0, len(nameSet))
for name := range nameSet {
orderedNames = append(orderedNames, name)
}
sort.Strings(orderedNames)
if len(orderedNames) == 0 {
return func() {}
}
l.mutex.Lock()
if l.locks == nil {
l.locks = make(map[string]*containerOperationLockEntry)
}
entries := make([]*containerOperationLockEntry, 0, len(orderedNames))
for _, name := range orderedNames {
entry := l.locks[name]
if entry == nil {
entry = &containerOperationLockEntry{}
l.locks[name] = entry
}
entry.references++
entries = append(entries, entry)
}
l.mutex.Unlock()
for _, entry := range entries {
entry.mutex.Lock()
}
return func() {
for index := len(entries) - 1; index >= 0; index-- {
entries[index].mutex.Unlock()
}
l.mutex.Lock()
defer l.mutex.Unlock()
for index, name := range orderedNames {
entry := entries[index]
entry.references--
if entry.references == 0 {
delete(l.locks, name)
}
}
}
}
type containerNetworkAttachment struct {
name string
endpoint *network.EndpointSettings
isDynamic bool
}
type containerSwitchLogFunc func(messageKey, containerName string, err error)
func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc {
return func(messageKey, containerName string, err error) {
t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
}
}
func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
if logger != nil {
logger(messageKey, containerName, err)
}
}
// switchContainer keeps the stopped original container as the rollback target until the replacement starts.
func switchContainer(
ctx context.Context,
cli containerSwitchClient,
name string,
oldContainer container.InspectResponse,
createNew func() (container.CreateResponse, error),
logger containerSwitchLogFunc,
) (cleanupErr error, err error) {
if oldContainer.ID == "" {
return nil, fmt.Errorf("original container ID is empty")
}
wasRunning := oldContainer.State != nil && oldContainer.State.Running
if wasRunning && oldContainer.HostConfig != nil && oldContainer.HostConfig.AutoRemove {
return nil, fmt.Errorf("cannot safely replace container %s with auto-remove enabled", name)
}
if wasRunning {
if err := cli.ContainerStop(ctx, oldContainer.ID, container.StopOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStopOld", name, err)
current, inspectErr := cli.ContainerInspect(ctx, oldContainer.ID)
if inspectErr == nil && current.State != nil && !current.State.Running {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("stop original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("stop original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerStopOld", name, nil)
}
backupName := containerSwitchBackupName(oldContainer.ID)
if err := cli.ContainerRename(ctx, oldContainer.ID, backupName); err != nil {
logContainerSwitchStep(logger, "ContainerRenameOld", name, err)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("rename original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("rename original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerRenameOld", name, nil)
disconnectedNetworks, disconnectErr := disconnectOriginalContainerNetworks(ctx, cli, oldContainer)
logContainerSwitchStep(logger, "ContainerDisconnectOld", backupName, disconnectErr)
if disconnectErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, "", disconnectedNetworks, logger)
return nil, errors.Join(disconnectErr, rollbackErr)
}
created, createErr := createNew()
logContainerSwitchStep(logger, "ContainerCreateReplacement", name, createErr)
if createErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(createErr, rollbackErr)
}
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerStartReplacement", name, nil)
if wasRunning {
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, nil)
}
cleanupErr = cli.ContainerRemove(ctx, oldContainer.ID, container.RemoveOptions{Force: true, RemoveVolumes: false})
logContainerSwitchStep(logger, "ContainerRemoveOld", backupName, cleanupErr)
return cleanupErr, nil
}
const (
containerStartStabilization = 10 * time.Second
containerStartPollInterval = time.Second
containerHealthCheckMinWait = 30 * time.Second
containerHealthCheckMaxWait = 10 * time.Minute
)
func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error {
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Health == nil {
return waitContainerStable(ctx, cli, containerID, info)
}
initialRestartCount := info.RestartCount
timeout := containerHealthCheckTimeout(info.Config)
deadline := time.NewTimer(timeout)
ticker := time.NewTicker(time.Second)
defer deadline.Stop()
defer ticker.Stop()
for {
if info.State.Restarting || info.RestartCount != initialRestartCount {
return fmt.Errorf("container restart count changed from %d to %d during startup", initialRestartCount, info.RestartCount)
}
if info.State.Health == nil {
return fmt.Errorf("container health status is unavailable")
}
switch info.State.Health.Status {
case container.Healthy:
return nil
case container.Unhealthy:
return fmt.Errorf("container health status is unhealthy")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
return fmt.Errorf("container health check timed out after %s", timeout)
case <-ticker.C:
info, err = cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
}
}
}
func waitContainerStable(ctx context.Context, cli containerInspectClient, containerID string, initial container.InspectResponse) error {
startedAt := initial.State.StartedAt
restartCount := initial.RestartCount
if err := checkContainerStableState(initial, startedAt, restartCount); err != nil {
return err
}
deadline := time.NewTimer(containerStartStabilization)
ticker := time.NewTicker(containerStartPollInterval)
defer deadline.Stop()
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
return checkContainerStableState(info, startedAt, restartCount)
case <-ticker.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerStableState(info, startedAt, restartCount); err != nil {
return err
}
}
}
}
func checkContainerStableState(info container.InspectResponse, startedAt string, restartCount int) error {
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Restarting || info.RestartCount != restartCount {
return fmt.Errorf("container restart count changed from %d to %d during startup", restartCount, info.RestartCount)
}
if startedAt != "" && info.State.StartedAt != startedAt {
return fmt.Errorf("container start time changed during startup")
}
return nil
}
func checkContainerRunningState(info container.InspectResponse) error {
if info.State == nil {
return fmt.Errorf("container state is unavailable")
}
if !info.State.Running {
return fmt.Errorf("container exited with code %d: %s", info.State.ExitCode, info.State.Error)
}
return nil
}
func containerHealthCheckTimeout(config *container.Config) time.Duration {
if config == nil || config.Healthcheck == nil {
return containerHealthCheckMinWait
}
health := config.Healthcheck
interval := health.Interval
if interval <= 0 {
interval = 30 * time.Second
}
checkTimeout := health.Timeout
if checkTimeout <= 0 {
checkTimeout = 30 * time.Second
}
retries := health.Retries
if retries <= 0 {
retries = 3
}
timeout := health.StartPeriod + time.Duration(retries)*(interval+checkTimeout)
if timeout < containerHealthCheckMinWait {
return containerHealthCheckMinWait
}
if timeout > containerHealthCheckMaxWait {
return containerHealthCheckMaxWait
}
return timeout
}
func preserveContainerVolumeMounts(hostConfig *container.HostConfig, oldMounts []container.MountPoint) {
if hostConfig == nil {
return
}
for _, oldMount := range oldMounts {
if oldMount.Type != mount.TypeVolume || oldMount.Name == "" || oldMount.Destination == "" {
continue
}
preserved := false
for index := range hostConfig.Mounts {
if hostConfig.Mounts[index].Target == oldMount.Destination {
hostConfig.Mounts[index].Type = mount.TypeVolume
hostConfig.Mounts[index].Source = oldMount.Name
preserved = true
}
}
for index, raw := range hostConfig.Binds {
destination, mode := containerBindDestinationAndMode(raw)
if destination != oldMount.Destination {
continue
}
hostConfig.Binds[index] = oldMount.Name + ":" + oldMount.Destination
if mode != "" {
hostConfig.Binds[index] += ":" + mode
}
preserved = true
}
if !preserved {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeVolume,
Source: oldMount.Name,
Target: oldMount.Destination,
ReadOnly: !oldMount.RW,
})
}
}
}
func containerBindDestinationAndMode(raw string) (string, string) {
parts := strings.SplitN(raw, ":", 3)
switch len(parts) {
case 1:
return parts[0], ""
case 2:
return parts[1], ""
default:
return parts[1], parts[2]
}
}
func containerSwitchBackupName(containerID string) string {
if len(containerID) > 12 {
containerID = containerID[:12]
}
return "1panel-backup-" + containerID
}
func restartOriginalContainer(ctx context.Context, cli containerSwitchClient, containerID string) error {
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return fmt.Errorf("restart original container failed: %w", err)
}
return nil
}
func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, oldContainer container.InspectResponse) ([]containerNetworkAttachment, error) {
primary, extras := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, oldContainer.HostConfig)
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
if primary != nil {
for name, endpoint := range primary.EndpointsConfig {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
}
for name, endpoint := range extras {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
names := make([]string, 0, len(endpoints))
for name := range endpoints {
names = append(names, name)
}
sort.Strings(names)
disconnected := make([]containerNetworkAttachment, 0, len(names))
for _, name := range names {
if err := cli.NetworkDisconnect(ctx, name, oldContainer.ID, true); err != nil {
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
}
disconnected = append(disconnected, containerNetworkAttachment{
name: name,
endpoint: endpoints[name],
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
})
}
return disconnected, nil
}
func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, containerID string, attachments []containerNetworkAttachment) error {
var reconnectErr error
for _, attachment := range attachments {
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
attachment.endpoint.IPAMConfig = nil
err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
}
if err != nil {
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
}
}
return reconnectErr
}
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error {
var rollbackErr error
backupName := containerSwitchBackupName(oldContainerID)
if newContainer != "" {
removeErr := cli.ContainerRemove(ctx, newContainer, container.RemoveOptions{Force: true, RemoveVolumes: true})
if client.IsErrNotFound(removeErr) {
removeErr = nil
}
logContainerSwitchStep(logger, "ContainerRollbackRemoveReplacement", originalName, removeErr)
if removeErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("remove failed replacement container failed: %w", removeErr))
}
}
renameErr := cli.ContainerRename(ctx, oldContainerID, originalName)
logContainerSwitchStep(logger, "ContainerRollbackRenameOld", backupName, renameErr)
if renameErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("restore original container name failed: %w", renameErr))
}
currentName := originalName
if renameErr != nil {
currentName = backupName
}
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
rollbackErr = errors.Join(rollbackErr, reconnectErr)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
rollbackErr = errors.Join(rollbackErr, restartErr)
}
return rollbackErr
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
var primaryEndpoints map[string]*network.EndpointSettings
if networkConf != nil {
primaryEndpoints = networkConf.EndpointsConfig
}
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
}, primaryEndpoints, networkSettings)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) {
err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
}
if err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func createContainerWithDynamicIPFallback(
create func() (container.CreateResponse, error),
endpoints map[string]*network.EndpointSettings,
networkSettings *container.NetworkSettings,
) (container.CreateResponse, error) {
for {
created, err := create()
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
return created, err
}
}
}
+4 -7
View File
@@ -538,14 +538,11 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
return err return err
} }
for _, del := range delRecords { for _, del := range delRecords {
if del.Status == constant.StatusWaiting || del.Status == constant.StatusRunning {
continue
}
if err := cronjobRepo.DeleteRecord(repo.WithByID(del.ID)); err != nil {
return err
}
_ = os.RemoveAll(del.Records) _ = os.RemoveAll(del.Records)
} }
if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil {
return err
}
return nil return nil
} }
@@ -647,7 +644,7 @@ func (u *CronjobService) HandleStop(id uint) error {
if len(record.TaskID) == 0 { if len(record.TaskID) == 0 {
return nil return nil
} }
if cancel, ok := global.LoadTaskCancel(record.TaskID); ok { if cancel, ok := global.TaskCtxMap[record.TaskID]; ok {
cancel() cancel()
} }
return nil return nil
+5 -6
View File
@@ -60,7 +60,7 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -119,7 +119,7 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -163,7 +163,6 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
record.Name = dbInfo.Database record.Name = dbInfo.Database
record.DetailName = dbInfo.Name record.DetailName = dbInfo.Name
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
record.Args = encodeBackupArgs(dbInfo.Args)
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name)) backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
switch dbInfo.DBType { switch dbInfo.DBType {
@@ -217,7 +216,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -275,7 +274,7 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
src := path.Join(backupDir, fileName) src := path.Join(backupDir, fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
return err return err
} }
record.FileDir = path.Dir(dst) record.FileDir = path.Dir(dst)
@@ -311,7 +310,7 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
src := path.Join(path.Dir(backupDir), fileName) src := path.Join(path.Dir(backupDir), fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
return err return err
} }
record.FileDir = path.Dir(dst) record.FileDir = path.Dir(dst)
+11 -40
View File
@@ -184,30 +184,9 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent { func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
var currentInfo dto.DashboardCurrent var currentInfo dto.DashboardCurrent
shotTime := time.Now() hostInfo, _ := psutil.HOST.GetHostInfo(false)
hostInfo, err := psutil.HOST.GetHostInfo(false) currentInfo.Uptime = hostInfo.Uptime
if err != nil { currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
global.LOG.Errorf("load host info failed: %v", err)
currentInfo.ShotTime = shotTime
return &currentInfo
}
uptime := hostInfo.Uptime
var bootTime uint64
if now := shotTime.Unix(); now > 0 {
nowUnix := uint64(now)
if hostInfo.BootTime > 0 && hostInfo.BootTime <= nowUnix {
bootTime = hostInfo.BootTime
uptime = nowUnix - bootTime
} else if uptime <= nowUnix {
bootTime = nowUnix - uptime
}
}
currentInfo.Uptime = uptime
currentInfo.RunningTime = loadRunningTime(uptime)
if bootTime > 0 {
currentInfo.TimeSinceUptime = time.Unix(int64(bootTime), 0).Format(constant.DateTimeLayout)
}
currentInfo.Procs = hostInfo.Procs currentInfo.Procs = hostInfo.Procs
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
@@ -249,12 +228,13 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
if ioOption == "all" { if ioOption == "all" {
diskInfo, _ := disk.IOCounters() diskInfo, _ := disk.IOCounters()
state := sumDiskIOCounters(diskInfo) for _, state := range diskInfo {
currentInfo.IOReadBytes = state.ReadBytes currentInfo.IOReadBytes += state.ReadBytes
currentInfo.IOWriteBytes = state.WriteBytes currentInfo.IOWriteBytes += state.WriteBytes
currentInfo.IOCount = state.ReadCount + state.WriteCount currentInfo.IOCount += (state.ReadCount + state.WriteCount)
currentInfo.IOReadTime = state.ReadTime currentInfo.IOReadTime += state.ReadTime
currentInfo.IOWriteTime = state.WriteTime currentInfo.IOWriteTime += state.WriteTime
}
} else { } else {
diskInfo, _ := disk.IOCounters(ioOption) diskInfo, _ := disk.IOCounters(ioOption)
for _, state := range diskInfo { for _, state := range diskInfo {
@@ -283,19 +263,10 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
} }
} }
currentInfo.ShotTime = shotTime currentInfo.ShotTime = time.Now()
return &currentInfo return &currentInfo
} }
func loadRunningTime(uptime uint64) dto.RunningTime {
return dto.RunningTime{
Days: uptime / 86400,
Hours: (uptime % 86400) / 3600,
Minutes: (uptime % 3600) / 60,
Seconds: uptime % 60,
}
}
func (u *DashboardService) LoadTopCPU() []dto.Process { func (u *DashboardService) LoadTopCPU() []dto.Process {
return loadTopCPU() return loadTopCPU()
} }
File diff suppressed because it is too large Load Diff
+20 -69
View File
@@ -28,11 +28,10 @@ import (
) )
const ( const (
rollbackPath = "1panel/tmp" rollbackPath = "1panel/tmp"
communityRestorePath = "1panel/tmp/community-restore" upgradePath = "1panel/tmp/upgrade"
upgradePath = "1panel/tmp/upgrade" uploadPath = "1panel/uploads"
uploadPath = "1panel/uploads" downloadPath = "1panel/download"
downloadPath = "1panel/download"
) )
func (u *DeviceService) Scan() dto.CleanData { func (u *DeviceService) Scan() dto.CleanData {
@@ -59,7 +58,7 @@ func (u *DeviceService) Scan() dto.CleanData {
SystemClean.BackupClean = loadBackupTree(fileOp) SystemClean.BackupClean = loadBackupTree(fileOp)
rollBackTree := loadRollBackTree() rollBackTree := loadRollBackTree(fileOp)
rollbackSize := uint64(0) rollbackSize := uint64(0)
for _, rollback := range rollBackTree { for _, rollback := range rollBackTree {
rollbackSize += rollback.Size rollbackSize += rollback.Size
@@ -114,15 +113,12 @@ func (u *DeviceService) Clean(req []dto.Clean) {
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app")) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database")) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website")) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
case "rollback_app": case "rollback_app":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name)) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
case "rollback_database": case "rollback_database":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name)) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
case "rollback_website": case "rollback_website":
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name)) dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
case "rollback_community_restore":
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))
case "upload": case "upload":
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name)) dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
@@ -218,7 +214,6 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount) dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount) dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount) dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)
upgrades := path.Join(global.Dir.BaseDir, upgradePath) upgrades := path.Join(global.Dir.BaseDir, upgradePath)
oldUpgradeFiles, _ := os.ReadDir(upgrades) oldUpgradeFiles, _ := os.ReadDir(upgrades)
@@ -611,21 +606,20 @@ func isExactPathMatch(path string, excludePaths []string) bool {
return false return false
} }
func loadRollBackTree() []dto.CleanTree { func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
var treeData []dto.CleanTree var treeData []dto.CleanTree
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore")
return treeData return treeData
} }
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree { func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
var treeData []dto.CleanTree var treeData []dto.CleanTree
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", fileOp)
path5 := path.Join(global.Dir.BaseDir, uploadPath) path5 := path.Join(global.Dir.BaseDir, uploadPath)
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp) uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
@@ -636,9 +630,9 @@ func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree { func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
var treeData []dto.CleanTree var treeData []dto.CleanTree
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website", fileOp)
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database") treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", fileOp)
path5 := path.Join(global.Dir.BaseDir, downloadPath) path5 := path.Join(global.Dir.BaseDir, downloadPath)
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp) uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
@@ -820,59 +814,16 @@ func loadContainerTree() []dto.CleanTree {
return treeData return treeData
} }
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree { func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem) size, _ := fileOp.GetDirSize(pathItem)
if len(list) == 0 || size == 0 { if size == 0 {
return treeData return treeData
} }
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false}) list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
return treeData return treeData
} }
func loadTreeWithFileSize(isCheck bool, originalPath, treeType, pathItem string) ([]dto.CleanTree, uint64) {
var (
lists []dto.CleanTree
total uint64
)
entries, err := os.ReadDir(pathItem)
if err != nil {
return lists, total
}
for _, entry := range entries {
item := dto.CleanTree{
ID: uuid.NewString(),
Label: entry.Name(),
Type: treeType,
Name: strings.TrimPrefix(path.Join(pathItem, entry.Name()), originalPath+"/"),
IsCheck: isCheck,
IsRecommend: isCheck,
CanDelete: true,
}
entryPath := path.Join(pathItem, entry.Name())
if entry.IsDir() {
children, size := loadTreeWithFileSize(isCheck, originalPath, treeType, entryPath)
if len(children) == 0 {
continue
}
item.Children = children
item.Size = size
} else {
info, err := entry.Info()
if err != nil {
continue
}
item.Size = uint64(info.Size())
}
if item.Size == 0 {
continue
}
total += item.Size
lists = append(lists, item)
}
return lists, total
}
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree { func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
var lists []dto.CleanTree var lists []dto.CleanTree
files, err := os.ReadDir(pathItem) files, err := os.ReadDir(pathItem)
@@ -1,84 +0,0 @@
package service
import (
"fmt"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
)
func composePortEnvKeys(index int) (containerPort, hostPort, hostIP, protocol string) {
return fmt.Sprintf("CONTAINER_PORT_%d", index),
fmt.Sprintf("HOST_PORT_%d", index),
fmt.Sprintf("HOST_IP_%d", index),
fmt.Sprintf("PORT_PROTOCOL_%d", index)
}
func isComposePortEnvKey(key string) bool {
return strings.HasPrefix(key, "CONTAINER_PORT_") ||
strings.HasPrefix(key, "HOST_PORT_") ||
strings.HasPrefix(key, "HOST_IP_") ||
strings.HasPrefix(key, "PORT_PROTOCOL_")
}
func formatComposePortMapping(hostIP, hostPort, containerPort, protocol string) string {
return fmt.Sprintf("${%s}:${%s}:${%s}/%s", hostIP, hostPort, containerPort, normalizeComposeProtocol(protocol))
}
func normalizeComposeProtocol(protocol string) string {
switch strings.ToLower(strings.TrimSpace(protocol)) {
case "udp":
return "udp"
default:
return "tcp"
}
}
func formatComposeVolume(source, target, mode string) string {
return fmt.Sprintf("%s:%s:%s", source, target, normalizeComposeVolumeMode(mode))
}
func normalizeComposeVolumeMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "ro":
return "ro"
default:
return "rw"
}
}
func loadComposeExposedPortsFromEnv(envs map[string]string, defaultHostIP string, strict bool) ([]request.ExposedPort, error) {
var ports []request.ExposedPort
for key, value := range envs {
if !strings.HasPrefix(key, "CONTAINER_PORT_") {
continue
}
index := strings.TrimPrefix(key, "CONTAINER_PORT_")
containerPort, err := strconv.Atoi(value)
if err != nil {
if strict {
return nil, err
}
continue
}
hostPort, err := strconv.Atoi(envs["HOST_PORT_"+index])
if err != nil {
if strict {
return nil, err
}
continue
}
hostIP := envs["HOST_IP_"+index]
if hostIP == "" {
hostIP = defaultHostIP
}
ports = append(ports, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
Protocol: normalizeComposeProtocol(envs["PORT_PROTOCOL_"+index]),
})
}
return ports, nil
}
+10 -14
View File
@@ -19,12 +19,10 @@ var (
agentAccountRepo = repo.NewIAgentAccountRepo() agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo() agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo() mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo() postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo() mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo() databaseRepo = repo.NewIDatabaseRepo()
databaseUserRepo = repo.NewIDatabaseUserRepo()
databaseUserGrantRepo = repo.NewIDatabaseUserGrantRepo()
imageRepoRepo = repo.NewIImageRepoRepo() imageRepoRepo = repo.NewIImageRepoRepo()
composeRepo = repo.NewIComposeTemplateRepo() composeRepo = repo.NewIComposeTemplateRepo()
@@ -40,14 +38,12 @@ var (
settingRepo = repo.NewISettingRepo() settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo() backupRepo = repo.NewIBackupRepo()
websiteRepo = repo.NewIWebsiteRepo() websiteRepo = repo.NewIWebsiteRepo()
websiteDomainRepo = repo.NewIWebsiteDomainRepo() websiteDomainRepo = repo.NewIWebsiteDomainRepo()
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
websiteSSLRepo = repo.NewISSLRepo() websiteSSLRepo = repo.NewISSLRepo()
websiteAcmeRepo = repo.NewIAcmeAccountRepo() websiteAcmeRepo = repo.NewIAcmeAccountRepo()
websiteCARepo = repo.NewIWebsiteCARepo() websiteCARepo = repo.NewIWebsiteCARepo()
websiteTemplateRepo = repo.NewIWebsiteTemplateRepo()
websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo()
snapshotRepo = repo.NewISnapshotRepo() snapshotRepo = repo.NewISnapshotRepo()
+12 -66
View File
@@ -16,7 +16,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -467,7 +466,7 @@ func (f *FileService) Compress(c request.FileCompress) error {
func preflightCompressTool(compressType files.CompressType) error { func preflightCompressTool(compressType files.CompressType) error {
switch compressType { switch compressType {
case files.Tar, files.Gz, files.Bz2, files.TarBz2, files.Tgz, files.TarGz, files.Xz, files.TarXz, files.Rar, files.X7z: case files.TarGz, files.Rar, files.X7z:
_, err := files.NewShellArchiver(compressType) _, err := files.NewShellArchiver(compressType)
return err return err
default: default:
@@ -477,7 +476,7 @@ func preflightCompressTool(compressType files.CompressType) error {
func preflightDecompressTool(decompressType files.CompressType) error { func preflightDecompressTool(decompressType files.CompressType) error {
switch decompressType { switch decompressType {
case files.Rar: case files.Rar, files.X7z:
_, err := files.NewExtractShellArchiver(decompressType) _, err := files.NewExtractShellArchiver(decompressType)
return err return err
default: default:
@@ -486,7 +485,7 @@ func preflightDecompressTool(decompressType files.CompressType) error {
} }
func (f *FileService) StopCompress(taskID string) error { func (f *FileService) StopCompress(taskID string) error {
if cancel, ok := global.LoadTaskCancel(taskID); ok { if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel() cancel()
return nil return nil
} }
@@ -494,7 +493,7 @@ func (f *FileService) StopCompress(taskID string) error {
} }
func (f *FileService) StopDeCompress(taskID string) error { func (f *FileService) StopDeCompress(taskID string) error {
if cancel, ok := global.LoadTaskCancel(taskID); ok { if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel() cancel()
return nil return nil
} }
@@ -534,10 +533,7 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
_ = os.RemoveAll(c.Dst) _ = os.RemoveAll(c.Dst)
} }
}() }()
if err := fo.DecompressWithOptions(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret, files.DecompressOptions{ if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
PreserveOwner: true,
AllowCLIReextract: true,
}); err != nil {
return err return err
} }
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil { if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
@@ -555,42 +551,19 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
} }
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error { func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
entries, err := os.ReadDir(srcDir) entries, err := os.ReadDir(srcDir)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range entries {
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name()), &state); err != nil { if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
return err return err
} }
} }
return nil return nil
} }
type decompressFileIdentity struct {
device uint64
inode uint64
}
type decompressCopyState struct {
hardlinks map[decompressFileIdentity]string
}
func decompressHardlinkIdentity(info os.FileInfo) (decompressFileIdentity, bool) {
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || stat.Nlink < 2 {
return decompressFileIdentity{}, false
}
return decompressFileIdentity{device: uint64(stat.Dev), inode: uint64(stat.Ino)}, true
}
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) { func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
return copyDecompressEntryWithState(ctx, srcPath, dstPath, &state)
}
func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string, state *decompressCopyState) (retErr error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -632,16 +605,13 @@ func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string,
if err := applyDecompressOwnership(srcPath, dstPath); err != nil { if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err return err
} }
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
return err
}
} }
entries, err := os.ReadDir(srcPath) entries, err := os.ReadDir(srcPath)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range entries {
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name()), state); err != nil { if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
return err return err
} }
} }
@@ -651,28 +621,12 @@ func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string,
return os.Chtimes(dstPath, info.ModTime(), info.ModTime()) return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
} }
dstInfo, err := os.Lstat(dstPath) if err := os.RemoveAll(dstPath); err != nil {
keepExistingFile := err == nil && dstInfo.Mode().IsRegular()
if err != nil && !os.IsNotExist(err) {
return err return err
} }
if !keepExistingFile {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil { if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err return err
} }
identity, isHardlink := decompressHardlinkIdentity(info)
if !keepExistingFile && isHardlink {
if existingPath, ok := state.hardlinks[identity]; ok {
if err := os.Link(existingPath, dstPath); err != nil {
return err
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
}
srcFile, err := os.Open(srcPath) srcFile, err := os.Open(srcPath)
if err != nil { if err != nil {
@@ -693,16 +647,8 @@ func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string,
if _, err := io.Copy(dstFile, srcFile); err != nil { if _, err := io.Copy(dstFile, srcFile); err != nil {
return err return err
} }
if !keepExistingFile { if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
if err := applyDecompressOwnership(srcPath, dstPath); err != nil { return err
return err
}
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
return err
}
if isHardlink {
state.hardlinks[identity] = dstPath
}
} }
return os.Chtimes(dstPath, info.ModTime(), info.ModTime()) return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
} }
@@ -712,7 +658,7 @@ func applyDecompressOwnership(srcPath, dstPath string) error {
if err != nil { if err != nil {
return err return err
} }
stat, ok := info.Sys().(*syscall.Stat_t) stat, ok := info.Sys().(*unix.Stat_t)
if !ok { if !ok {
return nil return nil
} }
@@ -1170,7 +1116,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
} }
logFilePath = path.Join(global.Dir.LogDir, "ai", safeName) logFilePath = path.Join(global.Dir.LogDir, "ai", safeName)
default: default:
return nil, buserr.WithName("ErrNotSupportType", req.Type) return nil, buserr.New("ErrNotSupportType")
} }
file, err := os.Open(logFilePath) file, err := os.Open(logFilePath)
+99 -36
View File
@@ -3,12 +3,14 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
@@ -26,6 +28,7 @@ type IFirewallService interface {
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error) SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
OperateFirewall(req dto.FirewallOperation) error OperateFirewall(req dto.FirewallOperation) error
OperatePortRule(req dto.PortRuleOperate, reload bool) error OperatePortRule(req dto.PortRuleOperate, reload bool) error
OperateForwardRule(req dto.ForwardRuleOperate) error
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
UpdatePortRule(req dto.PortRuleUpdate) error UpdatePortRule(req dto.PortRuleUpdate) error
UpdateAddrRule(req dto.AddrRuleUpdate) error UpdateAddrRule(req dto.AddrRuleUpdate) error
@@ -81,6 +84,8 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
switch req.Type { switch req.Type {
case "port": case "port":
rules, err = client.ListPort() rules, err = client.ListPort()
case "forward":
rules, err = client.ListForward()
case "address": case "address":
rules, err = client.ListAddress() rules, err = client.ListAddress()
} }
@@ -161,12 +166,6 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
if err != nil { if err != nil {
return err return err
} }
fail2BanState := newFirewallFail2BanState()
if client.Name() == "firewalld" && req.Operation == "stop" {
if err := fail2BanState.rememberBeforeFirewallStop(); err != nil {
return err
}
}
needRestartDocker := false needRestartDocker := false
switch req.Operation { switch req.Operation {
case "start": case "start":
@@ -211,11 +210,6 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
return fmt.Errorf("failed to restart Docker: %v", err) return fmt.Errorf("failed to restart Docker: %v", err)
} }
} }
if client.Name() == "firewalld" && req.Operation == "start" {
if err := fail2BanState.restoreAfterFirewallStart(); err != nil {
return err
}
}
return nil return nil
} }
@@ -228,7 +222,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
req.Chain = iptables.Chain1PanelBasic req.Chain = iptables.Chain1PanelBasic
} }
protos := strings.Split(req.Protocol, "/") protos := strings.Split(req.Protocol, "/")
itemAddress := splitFirewallRuleAddresses(req.Address) itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",")
if client.Name() == "ufw" { if client.Name() == "ufw" {
if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") { if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") {
@@ -312,6 +306,96 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
return nil return nil
} }
func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
client, err := firewall.NewFirewallClient()
if err != nil {
return err
}
rules, _ := client.ListForward()
i := 0
for _, rule := range rules {
shouldKeep := true
for i := range req.Rules {
reqRule := &req.Rules[i]
if reqRule.TargetIP == "" {
reqRule.TargetIP = "127.0.0.1"
}
if reqRule.Operation == "remove" {
for _, proto := range strings.Split(reqRule.Protocol, "/") {
if reqRule.Port == rule.Port &&
reqRule.TargetPort == rule.TargetPort &&
reqRule.TargetIP == rule.TargetIP &&
proto == rule.Protocol &&
reqRule.Interface == rule.Interface {
shouldKeep = false
break
}
}
}
}
if shouldKeep {
rules[i] = rule
i++
}
}
rules = rules[:i]
for _, rule := range rules {
for _, reqRule := range req.Rules {
if reqRule.Operation == "remove" {
continue
}
for _, proto := range strings.Split(reqRule.Protocol, "/") {
if reqRule.Port == rule.Port &&
reqRule.TargetPort == rule.TargetPort &&
reqRule.TargetIP == rule.TargetIP &&
proto == rule.Protocol &&
reqRule.Interface == rule.Interface {
return buserr.New("ErrRecordExist")
}
}
}
}
sort.SliceStable(req.Rules, func(i, j int) bool {
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
return true
}
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
return false
}
n1, _ := strconv.Atoi(req.Rules[i].Num)
n2, _ := strconv.Atoi(req.Rules[j].Num)
return n1 > n2
})
for _, r := range req.Rules {
for _, p := range strings.Split(r.Protocol, "/") {
if r.TargetIP == "" {
r.TargetIP = "127.0.0.1"
}
if err = client.PortForward(fireClient.Forward{
Num: r.Num,
Protocol: p,
Port: r.Port,
TargetIP: r.TargetIP,
TargetPort: r.TargetPort,
Interface: r.Interface,
}, r.Operation); err != nil {
if req.ForceDelete {
global.LOG.Error(err)
continue
}
return err
}
}
}
return nil
}
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error { func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
client, err := firewall.NewFirewallClient() client, err := firewall.NewFirewallClient()
if err != nil { if err != nil {
@@ -426,12 +510,11 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
return client.Reload() return client.Reload()
} }
func (u *FirewallService) operatePort(client firewall.FilterClient, req dto.PortRuleOperate) error { func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error {
var fireInfo fireClient.FireInfo var fireInfo fireClient.FireInfo
if err := copier.Copy(&fireInfo, &req); err != nil { if err := copier.Copy(&fireInfo, &req); err != nil {
return err return err
} }
fireInfo.Address = normalizeFirewallRuleAddress(fireInfo.Address)
if client.Name() == "ufw" { if client.Name() == "ufw" {
if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") { if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") {
@@ -446,26 +529,6 @@ func (u *FirewallService) operatePort(client firewall.FilterClient, req dto.Port
return client.Port(fireInfo, req.Operation) return client.Port(fireInfo, req.Operation)
} }
func splitFirewallRuleAddresses(address string) []string {
parts := strings.Split(strings.TrimSuffix(address, ","), ",")
addresses := make([]string, 0, len(parts))
for _, part := range parts {
addresses = append(addresses, normalizeFirewallRuleAddress(part))
}
if len(addresses) == 0 {
return []string{""}
}
return addresses
}
func normalizeFirewallRuleAddress(address string) string {
address = strings.TrimSpace(address)
if strings.EqualFold(address, "Anywhere") {
return ""
}
return address
}
type portOfApp struct { type portOfApp struct {
AppName string AppName string
HttpPort string HttpPort string
@@ -494,7 +557,7 @@ func (u *FirewallService) loadPortByApp() []portOfApp {
return datas return datas
} }
func (u *FirewallService) cleanUnUsedData(client firewall.FilterClient) { func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
list, _ := client.ListPort() list, _ := client.ListPort()
addressList, _ := client.ListAddress() addressList, _ := client.ListAddress()
list = append(list, addressList...) list = append(list, addressList...)
@@ -518,7 +581,7 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FilterClient) {
} }
} }
func (u *FirewallService) addPortsBeforeStart(client firewall.FilterClient) error { func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error {
if client.Name() == "iptables" { if client.Name() == "iptables" {
isInit, _ := iptables.LoadInitStatus("iptables", "base") isInit, _ := iptables.LoadInitStatus("iptables", "base")
if !isInit { if !isInit {
@@ -1,78 +0,0 @@
package service
import (
"fmt"
"os"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type firewallFail2BanState struct {
markerPath string
isExist func(string) bool
isActive func(string) bool
start func(string) error
}
func newFirewallFail2BanState() *firewallFail2BanState {
return &firewallFail2BanState{
markerPath: fail2BanRestoreWithFirewallMarker,
isExist: func(serviceName string) bool {
exists, err := controller.CheckExist(serviceName)
if err != nil {
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
}
return exists
},
isActive: func(serviceName string) bool {
active, err := controller.CheckActive(serviceName)
if err != nil {
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
}
return active
},
start: controller.HandleStart,
}
}
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
if !s.isExist("fail2ban.service") {
return nil
}
if !s.isActive("fail2ban.service") {
return nil
}
return s.markForRestore()
}
func (s *firewallFail2BanState) markForRestore() error {
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
_, err := os.Stat(s.markerPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := s.start("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
return s.clearRestoreMarker()
}
func (s *firewallFail2BanState) clearRestoreMarker() error {
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}

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