mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab758d626 | ||
|
|
3a0a6537f0 | ||
|
|
4eda92f25c | ||
|
|
94f8c5d2f1 | ||
|
|
92598effc1 | ||
|
|
748cecaee8 | ||
|
|
b783034d0a | ||
|
|
94a4e39793 | ||
|
|
1864dee591 | ||
|
|
8c4ea85d02 | ||
|
|
a714d5a84e | ||
|
|
4f97a7890f | ||
|
|
f15f91e5fa | ||
|
|
02bd500917 | ||
|
|
810c6fd232 | ||
|
|
d50a8983bc | ||
|
|
89647304bb | ||
|
|
9ebad13bf8 | ||
|
|
cc0ac8cc43 | ||
|
|
9f5a1d842a | ||
|
|
beb33957be | ||
|
|
ffcbbb705b | ||
|
|
058bc71630 | ||
|
|
0a38979bd9 | ||
|
|
5fa4a15914 | ||
|
|
81c551a11b | ||
|
|
5681449353 | ||
|
|
88849e6355 | ||
|
|
d6bbcdc32f | ||
|
|
abd1581c1d | ||
|
|
cd30ec7632 | ||
|
|
646826769a | ||
|
|
074c0abb3b | ||
|
|
3a00e893bc | ||
|
|
2f37677650 | ||
|
|
12ae89923b | ||
|
|
031a203a74 | ||
|
|
646b99974a | ||
|
|
254ed954e9 | ||
|
|
a0050dca09 | ||
|
|
cd80b4e6c3 | ||
|
|
df5d3d782c | ||
|
|
3c5b3c3467 | ||
|
|
2bdba42d87 | ||
|
|
0b146f7ff3 | ||
|
|
c28a4f6d69 | ||
|
|
f93a34acc4 | ||
|
|
4e78841bf6 | ||
|
|
7f5bb58c72 | ||
|
|
6a28359ac2 | ||
|
|
59655695b5 | ||
|
|
786c5c1d7c | ||
|
|
08388124b3 | ||
|
|
34ccc68b3a | ||
|
|
97ef386f17 | ||
|
|
394339d7dc | ||
|
|
8362578355 | ||
|
|
6dc218cf80 | ||
|
|
89888cef73 | ||
|
|
4d06f444ac |
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directories:
|
||||
- "/agent"
|
||||
- "/core"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -6,7 +6,7 @@ permissions:
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: Add Labels to PR
|
||||
if: github.repository == '1Panel-dev/1Panel'
|
||||
if: github.repository == '1Panel-dev/1Panel' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
+1
-12
@@ -9,7 +9,6 @@ build/1panel-core
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
@@ -41,20 +40,10 @@ core/cmd/server/web/index.html
|
||||
frontend/auto-imports.d.ts
|
||||
frontend/components.d.ts
|
||||
frontend/src/xpack
|
||||
frontend/src/xpack-ee
|
||||
agent/xpack
|
||||
agent/router/entry_xpack.go
|
||||
agent/server/init_xpack.go
|
||||
agent/utils/xpack/xpack.go
|
||||
agent/utils/xpack/xpack_xpack.go
|
||||
core/xpack
|
||||
core/router/entry_xpack.go
|
||||
core/server/init_xpack.go
|
||||
core/utils/xpack/xpack.go
|
||||
core/utils/xpack/xpack_xpack.go
|
||||
core/xpack-ee
|
||||
core/router/entry_xpackee.go
|
||||
core/server/init_xpackee.go
|
||||
core/utils/xpack/xpack_xpackee.go
|
||||
|
||||
.history/
|
||||
dist/
|
||||
|
||||
+4
-2
@@ -10,10 +10,12 @@ PR are always welcome, even if they only contain small fixes like typos or a few
|
||||
|
||||
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot of features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
|
||||
|
||||
This [development guideline](https://docs.1panel.pro/dev_manual/dev_manual/) contains information about repository structure, how to set up development environment, how to run it, and more.
|
||||
|
||||
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
|
||||
|
||||
## Add a new translation
|
||||
|
||||
If you'd like to help translate 1Panel into a new language, please read the [Translation Contribution Guide](docs/TRANSLATION.md). It lists every file you need to create or modify, along with a reference PR you can use as a concrete example.
|
||||
|
||||
## Report issues
|
||||
It is a great way to contribute by reporting an issue. Well-written and complete bug reports are always welcome! Please open an issue and follow the template to fill in required information.
|
||||
|
||||
|
||||
@@ -152,6 +152,26 @@ func (b *BaseApi) BindAgentWebsite(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Unbind Agent website
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/website/unbind [post]
|
||||
func (b *BaseApi) UnbindAgentWebsite(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UnbindWebsite(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent model config
|
||||
// @Accept json
|
||||
@@ -214,6 +234,67 @@ func (b *BaseApi) GetAgentOverview(c *gin.Context) {
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Hermes chat sessions
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {array} dto.AgentHermesChatSessionItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions [post]
|
||||
func (b *BaseApi) GetHermesChatSessions(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := agentService.GetHermesChatSessions(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Rename Hermes chat session
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentHermesChatSessionRenameReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions/rename [post]
|
||||
func (b *BaseApi) RenameHermesChatSession(c *gin.Context) {
|
||||
var req dto.AgentHermesChatSessionRenameReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.RenameHermesChatSession(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Hermes chat session
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentHermesChatSessionDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions/delete [post]
|
||||
func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
|
||||
var req dto.AgentHermesChatSessionDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteHermesChatSession(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Providers
|
||||
// @Success 200 {array} dto.ProviderInfo
|
||||
@@ -783,6 +864,27 @@ func (b *BaseApi) UpdateAgentDingTalkConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Weixin channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {object} dto.AgentWeixinConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/weixin/get [post]
|
||||
func (b *BaseApi) GetAgentWeixinConfig(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetWeixinConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
@@ -824,6 +926,26 @@ func (b *BaseApi) UpdateAgentQQBotConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentChannelDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/delete [post]
|
||||
func (b *BaseApi) DeleteAgentChannelConfig(c *gin.Context) {
|
||||
var req dto.AgentChannelDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteChannelConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Install Agent plugin
|
||||
// @Accept json
|
||||
@@ -1110,6 +1232,26 @@ func (b *BaseApi) InstallAgentSkill(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Uninstall Agent skill
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentSkillUninstallReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/skills/uninstall [post]
|
||||
func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
||||
var req dto.AgentSkillUninstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UninstallSkill(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Login Agent Weixin channel
|
||||
// @Accept json
|
||||
|
||||
@@ -415,6 +415,11 @@ func (b *BaseApi) Backup(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -475,6 +480,11 @@ func (b *BaseApi) Recover(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -530,6 +540,11 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Create mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mongodb 数据库 [name]","formatEN":"create mongodb database [name]"}
|
||||
func (b *BaseApi) CreateMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBCreate
|
||||
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 := mongodbService.Create(context.Background(), req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Page mongodb databases
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/search [post]
|
||||
func (b *BaseApi) SearchMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := mongodbService.SearchWithPage(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Update mongodb database description
|
||||
// @Accept json
|
||||
// @Param request body dto.UpdateDescription true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/description [post]
|
||||
// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"mongodb 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mongodb database [name] is modified => [description]"}
|
||||
func (b *BaseApi) UpdateMongodbDescription(c *gin.Context) {
|
||||
var req dto.UpdateDescription
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.UpdateDescription(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb database from remote
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbLoadDB true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/load [post]
|
||||
func (b *BaseApi) LoadMongodbFromRemote(c *gin.Context) {
|
||||
var req dto.MongodbLoadDB
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.LoadFromRemote(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Bind mongodb database user info
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbBind true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/bind [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mongodb 数据库 [database] [name] 用户 [username]","formatEN":"bind mongodb database [database] [name] user [username]"}
|
||||
func (b *BaseApi) BindMongodbUser(c *gin.Context) {
|
||||
var req dto.MongodbBind
|
||||
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 := mongodbService.BindUser(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb database password
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPassword true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/password [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] [name] 密码","formatEN":"update mongodb database [database] [name] password"}
|
||||
func (b *BaseApi) ChangeMongodbPassword(c *gin.Context) {
|
||||
var req dto.MongodbPassword
|
||||
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 := mongodbService.ChangePassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb root password
|
||||
// @Accept json
|
||||
// @Param request body dto.ChangeDBInfo true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/root/password [post]
|
||||
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] root 密码","formatEN":"update mongodb database [database] root password"}
|
||||
func (b *BaseApi) ChangeMongodbRootPassword(c *gin.Context) {
|
||||
var req dto.ChangeDBInfo
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Value) != 0 {
|
||||
value, err := base64.StdEncoding.DecodeString(req.Value)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Value = string(value)
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangeRootPassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivilegesLoad true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges [post]
|
||||
func (b *BaseApi) LoadMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivilegesLoad
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := mongodbService.LoadPrivileges(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, permission)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivileges true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges/change [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] 用户 [username] 权限","formatEN":"update mongodb database [database] user [username] privileges"}
|
||||
func (b *BaseApi) ChangeMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivileges
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangePrivileges(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Check before delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDeleteCheck true "request"
|
||||
// @Success 200 {array} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del/check [post]
|
||||
func (b *BaseApi) DeleteCheckMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDeleteCheck
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apps, err := mongodbService.DeleteCheck(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, apps)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"删除 mongodb 数据库 [name]","formatEN":"delete mongodb database [name]"}
|
||||
func (b *BaseApi) DeleteMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tx, ctx := helper.GetTxAndContext()
|
||||
if err := mongodbService.Delete(ctx, req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -31,18 +31,20 @@ var (
|
||||
dbCommonService = service.NewIDBCommonService()
|
||||
mysqlService = service.NewIMysqlService()
|
||||
postgresqlService = service.NewIPostgresqlService()
|
||||
mongodbService = service.NewIMongodbService()
|
||||
databaseService = service.NewIDatabaseService()
|
||||
redisService = service.NewIRedisService()
|
||||
|
||||
cronjobService = service.NewICronjobService()
|
||||
|
||||
fileService = service.NewIFileService()
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
fileService = service.NewIFileService()
|
||||
fileHistoryService = service.NewIFileHistoryService()
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
|
||||
deviceService = service.NewIDeviceService()
|
||||
fail2banService = service.NewIFail2BanService()
|
||||
|
||||
@@ -250,6 +250,26 @@ func (b *BaseApi) CompressFile(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Stop compress task
|
||||
// @Accept json
|
||||
// @Param request body request.FileCompressStopReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/compress/stop [post]
|
||||
func (b *BaseApi) StopCompressFile(c *gin.Context) {
|
||||
var req request.FileCompressStopReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileService.StopCompress(req.TaskID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Decompress file
|
||||
// @Accept json
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags File
|
||||
// @Summary Load file history list
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistorySearchReq true "request"
|
||||
// @Success 200 {object} response.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/search [post]
|
||||
func (b *BaseApi) SearchFileHistory(c *gin.Context) {
|
||||
var req request.FileHistorySearchReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := fileHistoryService.Search(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Load file history content
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryContentReq true "request"
|
||||
// @Success 200 {object} response.FileHistoryInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/content [post]
|
||||
func (b *BaseApi) GetFileHistoryContent(c *gin.Context) {
|
||||
var req request.FileHistoryContentReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := fileHistoryService.GetContent(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Delete file history record
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/del [post]
|
||||
func (b *BaseApi) DeleteFileHistory(c *gin.Context) {
|
||||
var req request.FileHistoryDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileHistoryService.Delete(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Restore file history record
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryRestoreReq true "request"
|
||||
// @Success 200 {object} response.FileInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/restore [post]
|
||||
func (b *BaseApi) RestoreFileHistory(c *gin.Context) {
|
||||
var req request.FileHistoryRestoreReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
info, err := fileHistoryService.Restore(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||
@@ -102,6 +103,41 @@ func (b *BaseApi) UpdateFileManageAISetting(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load file history setting info
|
||||
// @Success 200 {object} response.FileHistorySettingInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/file-history/search [post]
|
||||
func (b *BaseApi) GetFileHistorySettingInfo(c *gin.Context) {
|
||||
setting, err := settingService.GetFileHistorySettingInfo()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, setting)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Update file history setting
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistorySettingUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/file-history/update [post]
|
||||
func (b *BaseApi) UpdateFileHistorySetting(c *gin.Context) {
|
||||
var req request.FileHistorySettingUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := settingService.UpdateFileHistorySetting(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load local backup dir
|
||||
// @Success 200 {string} path
|
||||
|
||||
@@ -213,19 +213,29 @@ func loadContainerInitCmd(c *gin.Context) ([]string, error) {
|
||||
func loadDatabaseInitCmd(c *gin.Context) ([]string, error) {
|
||||
database := c.Query("database")
|
||||
databaseType := c.Query("databaseType")
|
||||
if len(database) == 0 || len(databaseType) == 0 {
|
||||
if len(databaseType) == 0 {
|
||||
return nil, fmt.Errorf("error param of database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
databaseConn, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Type: databaseType, Name: database})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
if len(databaseConn.ContainerName) == 0 {
|
||||
return nil, fmt.Errorf("no such database container for database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
commands := []string{"exec", "-it", databaseConn.ContainerName}
|
||||
switch databaseType {
|
||||
case "mysql", "mysql-cluster":
|
||||
commands = append(commands, []string{"mysql", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mariadb":
|
||||
commands = append(commands, []string{"mariadb", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mongodb":
|
||||
commands = append(commands, []string{
|
||||
"mongosh",
|
||||
"--username", databaseConn.Username,
|
||||
"--password", databaseConn.Password,
|
||||
"--authenticationDatabase", "admin",
|
||||
}...)
|
||||
case "postgresql", "postgresql-cluster":
|
||||
commands = []string{"exec", "-e", fmt.Sprintf("PGPASSWORD=%s", databaseConn.Password), "-it", databaseConn.ContainerName, "psql", "-t", "-U", databaseConn.Username}
|
||||
}
|
||||
|
||||
+77
-17
@@ -6,10 +6,10 @@ type AgentCreateReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Remark string `json:"remark"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
||||
Model string `json:"model"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -46,6 +46,7 @@ type AgentItem struct {
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
WebsitePrimaryDomain string `json:"websitePrimaryDomain"`
|
||||
WebsiteType string `json:"websiteType"`
|
||||
WebsiteProtocol string `json:"websiteProtocol"`
|
||||
AccountID uint `json:"accountId"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
@@ -91,6 +92,15 @@ type AgentModelConfig struct {
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Model string `json:"model"`
|
||||
MessageCount int64 `json:"messageCount"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
LastActive string `json:"lastActive"`
|
||||
}
|
||||
|
||||
type AgentOverviewReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
@@ -99,6 +109,17 @@ type AgentIDReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionRenameReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
Title string `json:"title" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionDeleteReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentOverview struct {
|
||||
Snapshot AgentOverviewSnapshot `json:"snapshot"`
|
||||
}
|
||||
@@ -298,6 +319,8 @@ type AgentFeishuConfigUpdateReq struct {
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
@@ -314,6 +337,8 @@ type AgentFeishuConfig struct {
|
||||
RequireMention string `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
@@ -327,6 +352,7 @@ type AgentTelegramConfigUpdateReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open allowlist disabled"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Proxy string `json:"proxy"`
|
||||
@@ -339,6 +365,7 @@ type AgentTelegramConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Proxy string `json:"proxy"`
|
||||
@@ -349,11 +376,16 @@ type AgentTelegramConfig struct {
|
||||
|
||||
type AgentChannelPairingApproveReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk"`
|
||||
PairingCode string `json:"pairingCode" validate:"required"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type AgentChannelDeleteReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk weixin"`
|
||||
}
|
||||
|
||||
type AgentWecomConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -379,7 +411,7 @@ type AgentWecomConfig struct {
|
||||
type AgentDingTalkConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=allowlist open disabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing allowlist open disabled"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
@@ -411,16 +443,28 @@ type AgentWeixinLoginReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWeixinConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Bots []AgentQQBotBot `json:"bots" validate:"required,min=1"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Bots []AgentQQBotBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Bots []AgentQQBotBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Bots []AgentQQBotBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentPluginInstallReq struct {
|
||||
@@ -458,6 +502,8 @@ type AgentDiscordConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
Proxy string `json:"proxy"`
|
||||
DefaultAccount string `json:"defaultAccount" validate:"required"`
|
||||
@@ -467,6 +513,8 @@ type AgentDiscordConfigUpdateReq struct {
|
||||
type AgentDiscordConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
Proxy string `json:"proxy"`
|
||||
DefaultAccount string `json:"defaultAccount"`
|
||||
@@ -556,25 +604,32 @@ type AgentConfigFile struct {
|
||||
|
||||
type AgentSkillSearchReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
|
||||
Keyword string `json:"keyword" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentSkillItem struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Bundled bool `json:"bundled"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
Trust string `json:"trust"`
|
||||
Identifier string `json:"identifier"`
|
||||
Bundled bool `json:"bundled"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Uninstallable bool `json:"uninstallable"`
|
||||
}
|
||||
|
||||
type AgentSkillSearchItem struct {
|
||||
Slug string `json:"slug"`
|
||||
Identifier string `json:"identifier"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Summary string `json:"summary"`
|
||||
Version string `json:"version"`
|
||||
Source string `json:"source"`
|
||||
Trust string `json:"trust"`
|
||||
Score string `json:"score"`
|
||||
}
|
||||
|
||||
@@ -586,7 +641,12 @@ type AgentSkillUpdateReq struct {
|
||||
|
||||
type AgentSkillInstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
|
||||
Slug string `json:"slug" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentSkillUninstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
+11
-10
@@ -65,7 +65,7 @@ type UploadForRecover struct {
|
||||
}
|
||||
|
||||
type CommonBackup struct {
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql 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"`
|
||||
DetailName string `json:"detailName"`
|
||||
Secret string `json:"secret"`
|
||||
@@ -77,15 +77,16 @@ type CommonBackup struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
type CommonRecover struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
Secret string `json:"secret"`
|
||||
TaskID string `json:"taskID"`
|
||||
BackupRecordID uint `json:"backupRecordID"`
|
||||
Timeout int `json:"timeout"`
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
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"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
Secret string `json:"secret"`
|
||||
DropAllCollections bool `json:"dropAllCollections"`
|
||||
TaskID string `json:"taskID"`
|
||||
BackupRecordID uint `json:"backupRecordID"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
type RecordSearch struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ type DBConfUpdateByFile struct {
|
||||
type ChangeDBInfo struct {
|
||||
ID uint `json:"id"`
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis mongodb mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
Value string `json:"value" validate:"required"`
|
||||
}
|
||||
@@ -137,6 +137,83 @@ type MysqlStatus struct {
|
||||
Position string `json:"Position"`
|
||||
}
|
||||
|
||||
// mongodb
|
||||
type MongodbDBSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
type MongodbDBInfo struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
MongodbName string `json:"mongodbName"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbDBCreate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbLoadDB struct {
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDeleteCheck struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
DeleteBackup bool `json:"deleteBackup"`
|
||||
}
|
||||
|
||||
type MongodbBind struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPassword struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPrivileges struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
}
|
||||
|
||||
type MongodbPrivilegesLoad struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type MysqlVariables struct {
|
||||
BinlogCacheSize string `json:"binlog_cache_size"`
|
||||
InnodbBufferPoolSize string `json:"innodb_buffer_pool_size"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
type SearchTaskLogReq struct {
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
TaskID string `json:"taskID"`
|
||||
PageInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ type FileCompress struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Replace bool `json:"replace"`
|
||||
Secret string `json:"secret"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileCompressStopReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type FileDeCompress struct {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
)
|
||||
|
||||
type FileHistorySearchReq struct {
|
||||
dto.PageInfo
|
||||
Path string `json:"path"`
|
||||
Scope string `json:"scope" validate:"required,oneof=current all"`
|
||||
Operation string `json:"operation"`
|
||||
}
|
||||
|
||||
type FileHistoryContentReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistoryIDs []uint
|
||||
|
||||
func (ids *FileHistoryIDs) UnmarshalJSON(data []byte) error {
|
||||
type alias []uint
|
||||
var direct alias
|
||||
if err := json.Unmarshal(data, &direct); err == nil {
|
||||
*ids = FileHistoryIDs(direct)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wrapped struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wrapped); err != nil {
|
||||
return err
|
||||
}
|
||||
*ids = FileHistoryIDs(wrapped.IDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
type FileHistoryDeleteReq struct {
|
||||
IDs FileHistoryIDs `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistoryRestoreReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistorySettingUpdate struct {
|
||||
Enable string `json:"enable" validate:"required,oneof=Enable Disable"`
|
||||
MaxPerPath int `json:"maxPerPath" validate:"min=0,max=1000"`
|
||||
DiskQuotaMB int `json:"diskQuotaMB" validate:"min=0,max=1048576"`
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package request
|
||||
@@ -0,0 +1,31 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type FileHistoryInfo struct {
|
||||
ID uint `json:"id"`
|
||||
FileID string `json:"fileId"`
|
||||
Path string `json:"path"`
|
||||
CurrentPath string `json:"currentPath"`
|
||||
PreviousID uint `json:"previousId"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
TargetPath string `json:"targetPath"`
|
||||
FileName string `json:"fileName"`
|
||||
Extension string `json:"extension"`
|
||||
FileMode string `json:"fileMode"`
|
||||
Operation string `json:"operation"`
|
||||
Deleted bool `json:"deleted"`
|
||||
ContentSize int64 `json:"contentSize"`
|
||||
ContentSHA string `json:"contentSHA"`
|
||||
StoragePath string `json:"storagePath,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
CurrentContent string `json:"currentContent,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type FileHistorySettingInfo struct {
|
||||
Enable string `json:"enable"`
|
||||
MaxPerPath int `json:"maxPerPath"`
|
||||
DiskQuotaMB int `json:"diskQuotaMB"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type DatabaseMongodb struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
From string `json:"from" gorm:"not null;default:local"`
|
||||
MongodbName string `json:"mongodbName" gorm:"not null"`
|
||||
Username string `json:"username" gorm:"not null"`
|
||||
Password string `json:"password" gorm:"not null"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
type FileHistory struct {
|
||||
BaseModel
|
||||
FileID string `json:"fileId" gorm:"not null;index:idx_file_history_file_id_created_at,priority:1;index:idx_file_history_file_id_deleted_created_at,priority:1"`
|
||||
Path string `json:"path" gorm:"not null;index:idx_file_history_path_created_at,priority:1;index:idx_file_history_path_hash_created_at,priority:1;index:idx_file_history_related_path,priority:1"`
|
||||
PathHash string `json:"pathHash" gorm:"not null;index:idx_file_history_path_created_at,priority:2;index:idx_file_history_path_hash_created_at,priority:2"`
|
||||
PreviousID uint `json:"previousId" gorm:"index"`
|
||||
SourcePath string `json:"sourcePath" gorm:"index:idx_file_history_related_path,priority:2"`
|
||||
TargetPath string `json:"targetPath" gorm:"index:idx_file_history_related_path,priority:3"`
|
||||
FileName string `json:"fileName" gorm:"not null"`
|
||||
Extension string `json:"extension"`
|
||||
FileMode string `json:"fileMode"`
|
||||
Operation string `json:"operation" gorm:"not null;index:idx_file_history_operation_created_at,priority:1"`
|
||||
Deleted bool `json:"deleted" gorm:"not null;default:false;index:idx_file_history_deleted_created_at,priority:1;index:idx_file_history_file_id_deleted_created_at,priority:2"`
|
||||
ContentSize int64 `json:"contentSize" gorm:"not null"`
|
||||
ContentSHA string `json:"contentSHA" gorm:"not null;index"`
|
||||
StoragePath string `json:"storagePath" gorm:"not null;uniqueIndex"`
|
||||
}
|
||||
+17
-107
@@ -5,11 +5,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenClawPatch struct {
|
||||
PrimaryModel string
|
||||
Models map[string]interface{}
|
||||
}
|
||||
|
||||
type OpenClawProviderPatch struct {
|
||||
PrimaryModel string
|
||||
ProviderKey string
|
||||
@@ -20,24 +15,7 @@ type OpenClawProviderPatch struct {
|
||||
AuthHeader bool
|
||||
}
|
||||
|
||||
func BuildOpenClawPatch(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
|
||||
providerPatch, err := BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, maxTokens, contextWindow = ResolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
|
||||
return newOpenClawPatch(
|
||||
providerPatch,
|
||||
resolveOpenClawModelName(provider, providerPatch.ModelID),
|
||||
resolveOpenClawModelInput(provider, providerPatch.ModelID),
|
||||
reasoning,
|
||||
contextWindow,
|
||||
maxTokens,
|
||||
), nil
|
||||
}
|
||||
|
||||
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
if modelName == "" {
|
||||
return nil, fmt.Errorf("model is required")
|
||||
}
|
||||
@@ -45,29 +23,29 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey st
|
||||
modelID := resolveOpenClawModelID(provider, modelName)
|
||||
switch provider {
|
||||
case "deepseek":
|
||||
return newOpenClawProviderPatch(modelName, "deepseek", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
|
||||
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "gemini":
|
||||
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, strings.TrimSpace(apiKey), baseURL, resolvedAPIType, false), nil
|
||||
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
case "moonshot", "kimi":
|
||||
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
|
||||
case "bailian-coding-plan":
|
||||
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
|
||||
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, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
|
||||
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "minimax":
|
||||
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, strings.TrimSpace(apiKey), baseURL, "anthropic-messages", true), nil
|
||||
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
|
||||
case "xiaomi":
|
||||
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
|
||||
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "custom", "vllm":
|
||||
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), resolvedAPIType, false), nil
|
||||
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
case "ollama":
|
||||
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", strings.TrimSpace(baseURL), resolvedAPIType, false), nil
|
||||
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
|
||||
case "kimi-coding":
|
||||
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, strings.TrimSpace(apiKey), baseURL, "anthropic-messages", false), nil
|
||||
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
|
||||
case "zai":
|
||||
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
|
||||
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
default:
|
||||
return newOpenClawProviderPatch(modelName, provider, modelID, strings.TrimSpace(apiKey), baseURL, resolvedAPIType, false), nil
|
||||
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +56,7 @@ func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey st
|
||||
providerKey = "moonshot"
|
||||
primaryModel = "moonshot/" + modelID
|
||||
}
|
||||
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false)
|
||||
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
|
||||
}
|
||||
|
||||
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
|
||||
@@ -93,84 +71,16 @@ func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseUR
|
||||
}
|
||||
}
|
||||
|
||||
func newOpenClawPatch(providerPatch *OpenClawProviderPatch, modelName string, input []string, reasoning bool, contextWindow, maxTokens int) *OpenClawPatch {
|
||||
model := map[string]interface{}{
|
||||
"id": providerPatch.ModelID,
|
||||
"name": modelName,
|
||||
"reasoning": reasoning,
|
||||
"input": input,
|
||||
"contextWindow": contextWindow,
|
||||
"maxTokens": maxTokens,
|
||||
"cost": map[string]interface{}{},
|
||||
}
|
||||
providerConfig := map[string]interface{}{
|
||||
"apiKey": providerPatch.APIKey,
|
||||
"baseUrl": providerPatch.BaseURL,
|
||||
"api": providerPatch.APIType,
|
||||
"models": []map[string]interface{}{model},
|
||||
}
|
||||
if providerPatch.AuthHeader {
|
||||
providerConfig["authHeader"] = true
|
||||
}
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: providerPatch.PrimaryModel,
|
||||
Models: map[string]interface{}{
|
||||
"mode": "merge",
|
||||
"providers": map[string]interface{}{
|
||||
providerPatch.ProviderKey: providerConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOpenClawModelID(provider, modelName string) string {
|
||||
if provider == "custom" || provider == "vllm" {
|
||||
return normalizeCustomModel(modelName)
|
||||
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
|
||||
}
|
||||
|
||||
func resolveOpenClawModelName(provider, modelID string) string {
|
||||
if item, ok := resolveOpenClawCatalogModel(provider, modelID); ok {
|
||||
return item.Name
|
||||
}
|
||||
if provider == "kimi-coding" {
|
||||
return "Kimi for Coding"
|
||||
}
|
||||
return modelID
|
||||
}
|
||||
|
||||
func resolveOpenClawModelInput(provider, modelID string) []string {
|
||||
if item, ok := resolveOpenClawCatalogModel(provider, modelID); ok && len(item.Input) > 0 {
|
||||
return item.Input
|
||||
}
|
||||
if provider == "kimi-coding" {
|
||||
return []string{"text", "image"}
|
||||
}
|
||||
return []string{"text"}
|
||||
}
|
||||
|
||||
func resolveOpenClawCatalogModel(provider, modelID string) (Model, bool) {
|
||||
candidates := []string{provider + "/" + modelID}
|
||||
if provider == "gemini" {
|
||||
candidates = append(candidates, "google/"+modelID)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if item, ok := FindModel(provider, candidate); ok {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return Model{}, false
|
||||
}
|
||||
|
||||
func normalizeCustomModel(modelName string) string {
|
||||
trim := strings.TrimSpace(modelName)
|
||||
trim = strings.TrimLeft(trim, "/")
|
||||
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 && strings.EqualFold(parts[0], "custom") {
|
||||
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
|
||||
}
|
||||
return trim
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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 MongodbRepo struct{}
|
||||
|
||||
type IMongodbRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseMongodb, error)
|
||||
WithByMongodbName(mongodbName string) DBOption
|
||||
List(opts ...DBOption) ([]model.DatabaseMongodb, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMongodb, error)
|
||||
Create(ctx context.Context, mongodb *model.DatabaseMongodb) error
|
||||
Delete(ctx context.Context, opts ...DBOption) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
DeleteLocal(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIMongodbRepo() IMongodbRepo {
|
||||
return &MongodbRepo{}
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Get(opts ...DBOption) (model.DatabaseMongodb, error) {
|
||||
var mongodb model.DatabaseMongodb
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.First(&mongodb).Error; err != nil {
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
pass, err := encrypt.StringDecrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) List(opts ...DBOption) ([]model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.Find(&mongodbs).Error; err != nil {
|
||||
return mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
if err := db.Limit(size).Offset(size * (page - 1)).Find(&mongodbs).Error; err != nil {
|
||||
return count, mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return count, mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Create(ctx context.Context, mongodb *model.DatabaseMongodb) error {
|
||||
pass, err := encrypt.StringEncrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return getTx(ctx).Create(mongodb).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
||||
return getTx(ctx, opts...).Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) DeleteLocal(ctx context.Context) error {
|
||||
return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Update(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.DatabaseMongodb{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) WithByMongodbName(mongodbName string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("mongodb_name = ?", mongodbName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileHistoryRepo struct{}
|
||||
|
||||
type IFileHistoryRepo interface {
|
||||
Get(opts ...DBOption) (model.FileHistory, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.FileHistory, error)
|
||||
Create(history *model.FileHistory) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteByIDs(ids []uint) error
|
||||
WithByID(id uint) DBOption
|
||||
WithByFileID(fileID string) DBOption
|
||||
WithByPath(path string) DBOption
|
||||
WithByPathHash(pathHash string) DBOption
|
||||
WithByPathLike(path string) DBOption
|
||||
WithByOperation(operation string) DBOption
|
||||
WithNotOperation(operation string) DBOption
|
||||
WithByRelatedPath(path string) DBOption
|
||||
}
|
||||
|
||||
func NewIFileHistoryRepo() IFileHistoryRepo {
|
||||
return &FileHistoryRepo{}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Get(opts ...DBOption) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return item, db.First(&item).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Page(limit, offset int, opts ...DBOption) (int64, []model.FileHistory, error) {
|
||||
var total int64
|
||||
var items []model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("created_at desc").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Create(history *model.FileHistory) error {
|
||||
return global.DB.Create(history).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Delete(opts ...DBOption) error {
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.FileHistory{}).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) DeleteByIDs(ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.Where("id in ?", ids).Delete(&model.FileHistory{}).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByID(id uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByFileID(fileID string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("file_id = ?", fileID)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPath(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path = ?", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPathHash(pathHash string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path_hash = ?", pathHash)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPathLike(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path like ?", "%"+path+"%")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByOperation(operation string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("operation = ?", operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithNotOperation(operation string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("operation <> ?", operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByRelatedPath(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path = ? OR source_path = ? OR target_path = ?", path, path, path)
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,9 @@ func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
|
||||
firewall.Strategy,
|
||||
).First(&data).Error
|
||||
}
|
||||
if data.ID != 0 {
|
||||
firewall.ID = data.ID
|
||||
}
|
||||
return global.DB.Save(firewall).Error
|
||||
}
|
||||
|
||||
|
||||
+161
-46
@@ -24,6 +24,7 @@ import (
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -35,8 +36,12 @@ type IAgentService interface {
|
||||
ResetToken(req dto.AgentTokenResetReq) error
|
||||
UpdateRemark(req dto.AgentRemarkUpdateReq) error
|
||||
BindWebsite(req dto.AgentWebsiteBindReq) error
|
||||
UnbindWebsite(req dto.AgentIDReq) error
|
||||
GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
|
||||
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
|
||||
RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
|
||||
DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error
|
||||
GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
|
||||
GetProviders() ([]dto.ProviderInfo, error)
|
||||
GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)
|
||||
@@ -49,6 +54,7 @@ type IAgentService interface {
|
||||
SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error)
|
||||
UpdateSkill(req dto.AgentSkillUpdateReq) error
|
||||
InstallSkill(req dto.AgentSkillInstallReq) error
|
||||
UninstallSkill(req dto.AgentSkillUninstallReq) error
|
||||
|
||||
CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
|
||||
DeleteRole(req dto.AgentRoleDeleteReq) error
|
||||
@@ -80,9 +86,11 @@ type IAgentService interface {
|
||||
UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
|
||||
GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error)
|
||||
UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error
|
||||
GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error)
|
||||
LoginWeixinChannel(req dto.AgentWeixinLoginReq) error
|
||||
GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error)
|
||||
UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
|
||||
DeleteChannelConfig(req dto.AgentChannelDeleteReq) error
|
||||
InstallPlugin(req dto.AgentPluginInstallReq) error
|
||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||
@@ -91,7 +99,7 @@ type IAgentService interface {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
|
||||
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser"
|
||||
defaultBrowserProfile = "openclaw"
|
||||
defaultUserTimezone = "Asia/Shanghai"
|
||||
defaultToolsProfile = "full"
|
||||
@@ -152,15 +160,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
var account *model.AgentAccount
|
||||
var installHooks *appInstallHooks
|
||||
|
||||
if agentType == constant.AppOpenclaw {
|
||||
var err error
|
||||
allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return nil, fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||
if req.AccountID == 0 {
|
||||
return nil, buserr.New("ErrAgentAccountRequired")
|
||||
}
|
||||
@@ -184,6 +184,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
runtimeModel = resolvedRuntime.PrimaryModel
|
||||
apiKey = account.APIKey
|
||||
accountID = account.ID
|
||||
}
|
||||
|
||||
if agentType == constant.AppOpenclaw {
|
||||
var err error
|
||||
allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return nil, fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
token = strings.TrimSpace(req.Token)
|
||||
if token == "" {
|
||||
token = generateToken()
|
||||
@@ -193,6 +204,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppHermesAgent {
|
||||
installHooks = &appInstallHooks{
|
||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||
return prepareHermesInstallFiles(appInstall, account, storedModel)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -200,12 +217,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.MemoryLimit: "0",
|
||||
constant.HostIP: "",
|
||||
}
|
||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||
if agentType == constant.AppOpenclaw {
|
||||
if isOpenclawHTTPSWindowVersion(detail.Version) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = req.WebUIPort
|
||||
} else {
|
||||
params["PANEL_APP_PORT_HTTP"] = req.WebUIPort
|
||||
}
|
||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||
}
|
||||
@@ -217,8 +230,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
params["BASE_URL"] = baseURL
|
||||
params["API_KEY"] = apiKey
|
||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||
} else {
|
||||
params["PANEL_APP_PORT_HTTP"] = req.WebUIPort
|
||||
}
|
||||
|
||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||
@@ -250,6 +261,9 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
if agentType == constant.AppOpenclaw {
|
||||
configPath = path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
||||
}
|
||||
agent := &model.Agent{
|
||||
Name: req.Name,
|
||||
Remark: req.Remark,
|
||||
@@ -276,6 +290,14 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
params["PANEL_APP_PORT_HTTP"] = webUIPort
|
||||
}
|
||||
|
||||
func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) {
|
||||
var opts []repo.DBOption
|
||||
if strings.TrimSpace(req.Info) != "" {
|
||||
@@ -420,6 +442,33 @@ func (a AgentService) UpdateRemark(req dto.AgentRemarkUpdateReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, err := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentModelConfig{
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: []string{},
|
||||
}, nil
|
||||
}
|
||||
agent, _, conf, err := a.loadOpenclawAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -444,7 +493,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error {
|
||||
agent, err := loadOpenclawAgentByID(req.AgentID)
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -459,8 +508,21 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
modelName := resolvedRuntime.StoredModel
|
||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
return err
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
agent.Provider = account.Provider
|
||||
agent.Model = modelName
|
||||
@@ -813,6 +875,17 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentOtherConfig{
|
||||
UserTimezone: cfg.Timezone,
|
||||
BrowserEnabled: true,
|
||||
NPMRegistry: "https://registry.npmjs.org/",
|
||||
}, nil
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -830,6 +903,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||
return err
|
||||
}
|
||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||
InstallId: install.ID,
|
||||
Operate: constant.Restart,
|
||||
})
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -844,11 +930,14 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
return setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry)
|
||||
if err := setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) {
|
||||
agent, _, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -860,12 +949,11 @@ func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfi
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var payload interface{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &payload); err != nil {
|
||||
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(agent.ConfigPath)
|
||||
@@ -875,12 +963,32 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
if err := os.WriteFile(agent.ConfigPath, []byte(req.Content), info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Model.Default != "" {
|
||||
agent.Model = cfg.Model.Default
|
||||
if err := agentRepo.Save(agent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||
InstallId: install.ID,
|
||||
Operate: constant.Restart,
|
||||
})
|
||||
}
|
||||
|
||||
func validateAgentConfigFileContent(agentType, content string) error {
|
||||
var payload interface{}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
return yaml.Unmarshal([]byte(content), &payload)
|
||||
}
|
||||
return json.Unmarshal([]byte(content), &payload)
|
||||
}
|
||||
|
||||
func getOpenclawNPMRegistry(containerName string) (string, error) {
|
||||
registry, err := runDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry")
|
||||
if err != nil {
|
||||
@@ -917,8 +1025,8 @@ func (a AgentService) loadOpenclawAgentAndInstall(agentID uint) (*model.Agent, *
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, install, nil
|
||||
}
|
||||
@@ -940,8 +1048,8 @@ func (a AgentService) loadOpenclawAgentConfig(agentID uint) (*model.Agent, *mode
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, nil, nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, install, conf, nil
|
||||
}
|
||||
@@ -970,30 +1078,37 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
return nil
|
||||
}
|
||||
for _, agent := range agents {
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
modelName := strings.TrimSpace(agent.Model)
|
||||
var selectedAccountModel dto.AgentAccountModel
|
||||
if modelName != "" {
|
||||
selectedAccountModel, err = requireAgentAccountModelForProvider(account.Provider, accountModels, modelName)
|
||||
if err != nil {
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
} else {
|
||||
selectedAccountModel = accountModels[0]
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, agent.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modelName = resolvedRuntime.StoredModel
|
||||
modelName := resolvedRuntime.StoredModel
|
||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
return err
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
switch agent.AgentType {
|
||||
case constant.AppOpenclaw:
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
agent.BaseURL = account.BaseURL
|
||||
agent.APIKey = account.APIKey
|
||||
|
||||
@@ -13,17 +13,34 @@ import (
|
||||
"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/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
)
|
||||
|
||||
type openclawPluginPackage struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func updateHermesChannelConfig(agent *model.Agent, install *model.AppInstall, write func(confDir string) error) error {
|
||||
if err := write(path.Dir(agent.ConfigPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := compose.Restart(install.GetComposePath())
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) {
|
||||
_, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesFeishuChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,6 +51,26 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesFeishuChannelConfig(confDir, dto.AgentFeishuConfig{
|
||||
Enabled: req.Enabled,
|
||||
ThreadSession: req.ThreadSession,
|
||||
ReplyMode: req.ReplyMode,
|
||||
Streaming: req.Streaming,
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Domain: "feishu",
|
||||
ConnectionMode: "websocket",
|
||||
Bots: req.Bots,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
config := dto.AgentFeishuConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -43,6 +80,8 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Domain: req.Domain,
|
||||
ConnectionMode: req.ConnectionMode,
|
||||
Bots: req.Bots,
|
||||
}
|
||||
setFeishuConfig(conf, config)
|
||||
@@ -52,7 +91,14 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
|
||||
}
|
||||
|
||||
func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) {
|
||||
_, _, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesTelegramChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,11 +107,32 @@ func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.Ag
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesTelegramChannelConfig(confDir, dto.AgentTelegramConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Proxy: req.Proxy,
|
||||
Streaming: req.Streaming,
|
||||
DefaultAccount: req.DefaultAccount,
|
||||
Bots: req.Bots,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
setTelegramConfig(conf, dto.AgentTelegramConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Proxy: req.Proxy,
|
||||
@@ -78,7 +145,14 @@ func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq)
|
||||
}
|
||||
|
||||
func (a AgentService) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) {
|
||||
_, _, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesDiscordChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,10 +161,30 @@ func (a AgentService) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordCon
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesDiscordChannelConfig(confDir, dto.AgentDiscordConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
Proxy: req.Proxy,
|
||||
DefaultAccount: req.DefaultAccount,
|
||||
Bots: req.Bots,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
setDiscordConfig(conf, dto.AgentDiscordConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
RequireMention: req.RequireMention,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
Proxy: req.Proxy,
|
||||
DefaultAccount: req.DefaultAccount,
|
||||
@@ -101,7 +195,14 @@ func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) e
|
||||
}
|
||||
|
||||
func (a AgentService) GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error) {
|
||||
_, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesQQBotChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,6 +213,22 @@ func (a AgentService) GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig,
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesQQBotChannelConfig(confDir, dto.AgentQQBotConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Bots: req.Bots,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
setQQBotConfig(conf, dto.AgentQQBotConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -122,7 +239,14 @@ func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
|
||||
}
|
||||
|
||||
func (a AgentService) GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error) {
|
||||
_, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesWecomChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,6 +257,23 @@ func (a AgentService) GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig,
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesWecomChannelConfig(confDir, dto.AgentWecomConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
BotID: req.BotID,
|
||||
Secret: req.Secret,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
setWecomConfig(conf, dto.AgentWecomConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -148,7 +289,14 @@ func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
|
||||
}
|
||||
|
||||
func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error) {
|
||||
_, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesDingTalkChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -158,7 +306,34 @@ func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkC
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error) {
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return readHermesWeixinChannelConfig(path.Dir(agent.ConfigPath))
|
||||
}
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
return writeHermesDingTalkChannelConfig(confDir, dto.AgentDingTalkConfig{
|
||||
Enabled: req.Enabled,
|
||||
DmPolicy: req.DmPolicy,
|
||||
AllowFrom: req.AllowFrom,
|
||||
GroupPolicy: req.GroupPolicy,
|
||||
GroupAllowFrom: req.GroupAllowFrom,
|
||||
Bots: req.Bots,
|
||||
})
|
||||
})
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
setDingTalkConfig(conf, dto.AgentDingTalkConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -299,7 +474,7 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -307,10 +482,22 @@ func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loginTask.AddSubTask("Login OpenClaw Weixin channel", func(t *task.Task) error {
|
||||
loginTask.AddSubTask("Login Weixin channel", func(t *task.Task) error {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(30*time.Minute))
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return mgr.Run("docker", buildHermesWeixinLoginArgs(install.ContainerName)...)
|
||||
}
|
||||
return mgr.RunBashCf("docker exec %s openclaw channels login --channel openclaw-weixin", install.ContainerName)
|
||||
}, nil)
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
loginTask.AddSubTask("Restart Hermes-Agent container", func(t *task.Task) error {
|
||||
output, err := compose.Restart(install.GetComposePath())
|
||||
if output != "" {
|
||||
t.Log(output)
|
||||
}
|
||||
return err
|
||||
}, nil)
|
||||
}
|
||||
go func() {
|
||||
if err := loginTask.Execute(); err != nil {
|
||||
global.LOG.Errorf("login openclaw weixin channel failed: %v", err)
|
||||
@@ -319,6 +506,36 @@ func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return updateHermesChannelConfig(agent, install, func(confDir string) error {
|
||||
switch req.Type {
|
||||
case "telegram":
|
||||
return deleteHermesTelegramChannelConfig(confDir)
|
||||
case "discord":
|
||||
return deleteHermesDiscordChannelConfig(confDir)
|
||||
case "qqbot":
|
||||
return deleteHermesQQBotChannelConfig(confDir)
|
||||
case "wecom":
|
||||
return deleteHermesWecomChannelConfig(confDir)
|
||||
case "dingtalk":
|
||||
return deleteHermesDingTalkChannelConfig(confDir)
|
||||
case "feishu":
|
||||
return deleteHermesFeishuChannelConfig(confDir)
|
||||
case "weixin":
|
||||
return deleteHermesWeixinChannelConfig(confDir)
|
||||
default:
|
||||
return fmt.Errorf("unsupported channel type: %s", req.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
@@ -354,10 +571,18 @@ func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPlugin
|
||||
}
|
||||
|
||||
func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error {
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
||||
output, err := mgr.RunWithStdout(
|
||||
"docker",
|
||||
buildHermesDockerExecArgs(install.ContainerName, "pairing", "approve", req.Type, req.PairingCode)...,
|
||||
)
|
||||
return validateHermesPairingApproveResult(output, err)
|
||||
}
|
||||
if req.AccountID != "" {
|
||||
return cmd.RunDefaultBashCf(
|
||||
"docker exec %s openclaw pairing approve %s %q --account %q",
|
||||
@@ -384,7 +609,7 @@ func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||
RequireMention: "true",
|
||||
GroupPolicy: "open",
|
||||
GroupAllowFrom: []string{},
|
||||
Bots: []dto.AgentFeishuBot{defaultFeishuBot()},
|
||||
Bots: []dto.AgentFeishuBot{},
|
||||
}
|
||||
feishu := getChannelConfig(conf, "feishu")
|
||||
if len(feishu) == 0 {
|
||||
@@ -434,7 +659,10 @@ func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||
baseEnabled := defaultBot.Enabled
|
||||
baseDmPolicy := defaultBot.DmPolicy
|
||||
baseAllowFrom := append([]string(nil), defaultBot.AllowFrom...)
|
||||
bots := []dto.AgentFeishuBot{defaultBot}
|
||||
bots := make([]dto.AgentFeishuBot, 0, len(accounts)+1)
|
||||
if defaultBot.AppID != "" || defaultBot.AppSecret != "" {
|
||||
bots = append(bots, defaultBot)
|
||||
}
|
||||
for _, accountID := range sortedChildKeys(accounts) {
|
||||
if accountID == "default" {
|
||||
continue
|
||||
@@ -552,6 +780,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
|
||||
Enabled: true,
|
||||
DmPolicy: "pairing",
|
||||
AllowFrom: []string{},
|
||||
RequireMention: false,
|
||||
GroupPolicy: "open",
|
||||
GroupAllowFrom: []string{},
|
||||
Streaming: "partial",
|
||||
@@ -570,6 +799,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
|
||||
if groupPolicy := extractStringValue(telegram["groupPolicy"]); groupPolicy != "" {
|
||||
result.GroupPolicy = groupPolicy
|
||||
}
|
||||
result.RequireMention = result.GroupPolicy == "allowlist"
|
||||
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
|
||||
result.Proxy = extractStringValue(telegram["proxy"])
|
||||
if streaming := extractStringValue(telegram["streaming"]); streaming != "" {
|
||||
@@ -618,6 +848,11 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
|
||||
telegram["dmPolicy"] = config.DmPolicy
|
||||
telegram["groupPolicy"] = config.GroupPolicy
|
||||
telegram["defaultAccount"] = defaultAccount
|
||||
if config.RequireMention {
|
||||
telegram["groupPolicy"] = "allowlist"
|
||||
} else if config.GroupPolicy == "allowlist" {
|
||||
telegram["groupPolicy"] = "allowlist"
|
||||
}
|
||||
if config.DmPolicy == "open" {
|
||||
telegram["allowFrom"] = []string{"*"}
|
||||
} else if config.DmPolicy == "allowlist" {
|
||||
@@ -656,7 +891,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
|
||||
}
|
||||
|
||||
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
|
||||
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"}
|
||||
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
|
||||
discord := getChannelConfig(conf, "discord")
|
||||
if len(discord) == 0 {
|
||||
return result
|
||||
@@ -674,6 +909,8 @@ func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
|
||||
if groupPolicy := extractStringValue(discord["groupPolicy"]); groupPolicy != "" {
|
||||
result.GroupPolicy = groupPolicy
|
||||
}
|
||||
result.RequireMention = result.GroupPolicy == "allowlist"
|
||||
result.AllowFrom = extractStringList(discord["allowFrom"])
|
||||
result.Proxy = extractStringValue(discord["proxy"])
|
||||
accounts := childMap(discord, "accounts")
|
||||
if len(accounts) == 0 {
|
||||
@@ -712,8 +949,15 @@ func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig
|
||||
discord["dmPolicy"] = config.DmPolicy
|
||||
discord["groupPolicy"] = config.GroupPolicy
|
||||
discord["defaultAccount"] = defaultAccount
|
||||
if config.RequireMention {
|
||||
discord["groupPolicy"] = "allowlist"
|
||||
} else if config.GroupPolicy == "allowlist" {
|
||||
discord["groupPolicy"] = "allowlist"
|
||||
}
|
||||
if config.DmPolicy == "open" {
|
||||
discord["allowFrom"] = []string{"*"}
|
||||
} else if config.DmPolicy == "allowlist" {
|
||||
discord["allowFrom"] = append([]string(nil), config.AllowFrom...)
|
||||
} else {
|
||||
delete(discord, "allowFrom")
|
||||
}
|
||||
@@ -740,31 +984,34 @@ func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
|
||||
result := dto.AgentQQBotConfig{Enabled: true}
|
||||
qqbot := getChannelConfig(conf, "qqbot")
|
||||
if len(qqbot) == 0 {
|
||||
result.Bots = []dto.AgentQQBotBot{defaultQQBot()}
|
||||
result.Bots = []dto.AgentQQBotBot{}
|
||||
return result
|
||||
}
|
||||
if enabled, ok := qqbot["enabled"].(bool); ok {
|
||||
result.Enabled = enabled
|
||||
}
|
||||
bots := []dto.AgentQQBotBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: extractStringValue(qqbot["name"]),
|
||||
Enabled: extractBoolValue(qqbot["enabled"], true),
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: extractStringValue(qqbot["appId"]),
|
||||
ClientSecret: extractStringValue(qqbot["clientSecret"]),
|
||||
AllowFrom: extractStringList(qqbot["allowFrom"]),
|
||||
SystemPrompt: extractStringValue(qqbot["systemPrompt"]),
|
||||
bots := make([]dto.AgentQQBotBot, 0, len(childMap(qqbot, "accounts"))+1)
|
||||
defaultBot := dto.AgentQQBotBot{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: extractStringValue(qqbot["name"]),
|
||||
Enabled: extractBoolValue(qqbot["enabled"], true),
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: extractStringValue(qqbot["appId"]),
|
||||
ClientSecret: extractStringValue(qqbot["clientSecret"]),
|
||||
AllowFrom: extractStringList(qqbot["allowFrom"]),
|
||||
SystemPrompt: extractStringValue(qqbot["systemPrompt"]),
|
||||
}
|
||||
if bots[0].Name == "" {
|
||||
bots[0].Name = "Default"
|
||||
if defaultBot.Name == "" {
|
||||
defaultBot.Name = "Default"
|
||||
}
|
||||
for _, accountID := range sortedChildKeys(childMap(qqbot, "accounts")) {
|
||||
account := childMap(childMap(qqbot, "accounts"), accountID)
|
||||
if defaultBot.AppID != "" || defaultBot.ClientSecret != "" {
|
||||
bots = append(bots, defaultBot)
|
||||
}
|
||||
accounts := childMap(qqbot, "accounts")
|
||||
for _, accountID := range sortedChildKeys(accounts) {
|
||||
account := childMap(accounts, accountID)
|
||||
bots = append(bots, dto.AgentQQBotBot{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: accountID,
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"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/joho/godotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const hermesWorkspaceDir = "/opt/data/workspace"
|
||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||
|
||||
type hermesConfig struct {
|
||||
Model hermesModelConfig `yaml:"model"`
|
||||
Terminal hermesTerminalConfig `yaml:"terminal"`
|
||||
Timezone string `yaml:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
type hermesModelConfig struct {
|
||||
Default string `yaml:"default"`
|
||||
Provider string `yaml:"provider"`
|
||||
BaseURL string `yaml:"base_url,omitempty"`
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
type hermesTerminalConfig struct {
|
||||
Backend string `yaml:"backend"`
|
||||
Cwd string `yaml:"cwd"`
|
||||
}
|
||||
|
||||
type hermesEnvEntry struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func buildHermesDockerExecCommandArgs(containerName, command string, commandArgs ...string) []string {
|
||||
args := []string{"exec", "-u", "hermes", containerName, command}
|
||||
return append(args, commandArgs...)
|
||||
}
|
||||
|
||||
func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []string {
|
||||
return buildHermesDockerExecCommandArgs(containerName, "hermes", hermesArgs...)
|
||||
}
|
||||
|
||||
func writeHermesConfig(confDir string, account *model.AgentAccount, modelName string, timezone string) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
if account == nil {
|
||||
return fmt.Errorf("account is required")
|
||||
}
|
||||
if strings.TrimSpace(modelName) == "" {
|
||||
return fmt.Errorf("model is required")
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(confDir) {
|
||||
if err := fileOp.CreateDir(confDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
provider := resolveHermesProvider(account.Provider)
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg["model"] = map[string]interface{}{
|
||||
"default": resolveHermesModel(account.Provider, provider, modelName),
|
||||
"provider": provider,
|
||||
"base_url": account.BaseURL,
|
||||
}
|
||||
cfg["terminal"] = map[string]interface{}{
|
||||
"backend": "local",
|
||||
"cwd": hermesWorkspaceDir,
|
||||
}
|
||||
if timezone != "" {
|
||||
cfg["timezone"] = timezone
|
||||
} else {
|
||||
delete(cfg, "timezone")
|
||||
}
|
||||
if err := writeHermesConfigMap(configPath, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeHermesEnv(path.Join(confDir, ".env"), resolveHermesEnvEntries(account))
|
||||
}
|
||||
|
||||
func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName string) error {
|
||||
if appInstall == nil {
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
if err := writeHermesConfig(dataDir, account, modelName, normalizeHermesTimezone(common.LoadTimeZoneByCmd())); err != nil {
|
||||
return err
|
||||
}
|
||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||
}
|
||||
|
||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||
content, err := files.NewFileOp().GetContent(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg hermesConfig
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func readHermesConfigMap(configPath string) (map[string]interface{}, error) {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(configPath) {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
content, err := fileOp.GetContent(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := map[string]interface{}{}
|
||||
if strings.TrimSpace(string(content)) == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
|
||||
payload, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return files.NewFileOp().SaveFile(configPath, string(payload), 0600)
|
||||
}
|
||||
|
||||
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowFrom := splitHermesEnvList(envMap["TELEGRAM_ALLOWED_USERS"])
|
||||
requireMention := extractBoolValue(childMap(cfg, "telegram")["require_mention"], false)
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "TELEGRAM_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if envMap["TELEGRAM_BOT_TOKEN"] != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := ""
|
||||
if requireMention {
|
||||
groupPolicy = "allowlist"
|
||||
} else if envMap["TELEGRAM_BOT_TOKEN"] != "" {
|
||||
groupPolicy = "open"
|
||||
}
|
||||
token := envMap["TELEGRAM_BOT_TOKEN"]
|
||||
result := &dto.AgentTelegramConfig{
|
||||
Enabled: token != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
RequireMention: requireMention,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: []string{},
|
||||
Streaming: "",
|
||||
DefaultAccount: "",
|
||||
}
|
||||
if token != "" {
|
||||
result.Bots = []dto.AgentTelegramBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
BotToken: token,
|
||||
DmPolicy: dmPolicy,
|
||||
GroupPolicy: groupPolicy,
|
||||
Streaming: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token := firstHermesTelegramBotToken(config.Bots, config.DefaultAccount)
|
||||
if token != "" {
|
||||
envMap["TELEGRAM_BOT_TOKEN"] = token
|
||||
} else {
|
||||
delete(envMap, "TELEGRAM_BOT_TOKEN")
|
||||
}
|
||||
delete(envMap, "TELEGRAM_ALLOWED_USERS")
|
||||
delete(envMap, "TELEGRAM_ALLOW_ALL_USERS")
|
||||
switch config.DmPolicy {
|
||||
case "open":
|
||||
envMap["TELEGRAM_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["TELEGRAM_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"TELEGRAM_ALLOWED_USERS",
|
||||
"TELEGRAM_ALLOW_ALL_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
telegram := ensureChildMap(cfg, "telegram")
|
||||
telegram["require_mention"] = config.RequireMention
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowFrom := splitHermesEnvList(envMap["DISCORD_ALLOWED_USERS"])
|
||||
requireMention := extractBoolValue(childMap(cfg, "discord")["require_mention"], false)
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "DISCORD_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if envMap["DISCORD_BOT_TOKEN"] != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := ""
|
||||
if requireMention {
|
||||
groupPolicy = "allowlist"
|
||||
} else if envMap["DISCORD_BOT_TOKEN"] != "" {
|
||||
groupPolicy = "open"
|
||||
}
|
||||
token := envMap["DISCORD_BOT_TOKEN"]
|
||||
result := &dto.AgentDiscordConfig{
|
||||
Enabled: token != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
RequireMention: requireMention,
|
||||
GroupPolicy: groupPolicy,
|
||||
DefaultAccount: "",
|
||||
}
|
||||
if token != "" {
|
||||
result.Bots = []dto.AgentDiscordBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token := firstHermesDiscordBotToken(config.Bots, config.DefaultAccount)
|
||||
if token != "" {
|
||||
envMap["DISCORD_BOT_TOKEN"] = token
|
||||
} else {
|
||||
delete(envMap, "DISCORD_BOT_TOKEN")
|
||||
}
|
||||
delete(envMap, "DISCORD_ALLOWED_USERS")
|
||||
delete(envMap, "DISCORD_ALLOW_ALL_USERS")
|
||||
switch config.DmPolicy {
|
||||
case "open":
|
||||
envMap["DISCORD_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["DISCORD_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"DISCORD_ALLOW_ALL_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discord := ensureChildMap(cfg, "discord")
|
||||
discord["require_mention"] = config.RequireMention
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesEnvKeys(confDir string, keys ...string) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range keys {
|
||||
delete(envMap, key)
|
||||
}
|
||||
return writeHermesEnvMap(envPath, envMap, keys)
|
||||
}
|
||||
|
||||
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range topLevelKeys {
|
||||
delete(cfg, key)
|
||||
}
|
||||
if len(platformKeys) > 0 {
|
||||
if platforms, ok := cfg["platforms"].(map[string]interface{}); ok {
|
||||
for _, key := range platformKeys {
|
||||
delete(platforms, key)
|
||||
}
|
||||
if len(platforms) == 0 {
|
||||
delete(cfg, "platforms")
|
||||
}
|
||||
}
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesTelegramChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"TELEGRAM_ALLOWED_USERS",
|
||||
"TELEGRAM_ALLOW_ALL_USERS",
|
||||
"TELEGRAM_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, []string{"telegram"}, []string{"telegram"})
|
||||
}
|
||||
|
||||
func deleteHermesDiscordChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"DISCORD_ALLOW_ALL_USERS",
|
||||
"DISCORD_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, []string{"discord"}, []string{"discord"})
|
||||
}
|
||||
|
||||
func normalizeHermesTimezone(timezone string) string {
|
||||
timezone = strings.TrimSpace(timezone)
|
||||
if timezone == "" {
|
||||
return ""
|
||||
}
|
||||
if _, err := time.LoadLocation(timezone); err != nil {
|
||||
return ""
|
||||
}
|
||||
return timezone
|
||||
}
|
||||
|
||||
func resolveHermesProvider(provider string) string {
|
||||
switch provider {
|
||||
case "":
|
||||
return "custom"
|
||||
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
|
||||
return provider
|
||||
case "minimax":
|
||||
return "minimax-cn"
|
||||
default:
|
||||
return "custom"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHermesModel(sourceProvider, targetProvider, modelName string) string {
|
||||
target := strings.TrimSpace(modelName)
|
||||
if target == "" {
|
||||
return ""
|
||||
}
|
||||
if targetProvider != "custom" {
|
||||
return 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) {
|
||||
if account == nil {
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configuredModel = strings.TrimSpace(configuredModel)
|
||||
if configuredModel == "" {
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
provider := resolveHermesProvider(account.Provider)
|
||||
for _, item := range accountModels {
|
||||
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
||||
return item.ID, nil
|
||||
}
|
||||
}
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
|
||||
func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
apiKey := account.APIKey
|
||||
baseURL := account.BaseURL
|
||||
entries := make([]hermesEnvEntry, 0, 4)
|
||||
|
||||
appendEntry := func(key, value string) {
|
||||
if key == "" || value == "" {
|
||||
return
|
||||
}
|
||||
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
|
||||
}
|
||||
|
||||
switch account.Provider {
|
||||
case "openrouter":
|
||||
appendEntry("OPENROUTER_API_KEY", apiKey)
|
||||
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
||||
case "anthropic":
|
||||
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
||||
case "gemini":
|
||||
appendEntry("GOOGLE_API_KEY", apiKey)
|
||||
appendEntry("GEMINI_API_KEY", apiKey)
|
||||
appendEntry("GEMINI_BASE_URL", baseURL)
|
||||
case "zai":
|
||||
appendEntry("GLM_API_KEY", apiKey)
|
||||
appendEntry("ZAI_API_KEY", apiKey)
|
||||
appendEntry("Z_AI_API_KEY", apiKey)
|
||||
appendEntry("GLM_BASE_URL", baseURL)
|
||||
case "kimi-coding", "moonshot":
|
||||
appendEntry("KIMI_API_KEY", apiKey)
|
||||
appendEntry("KIMI_BASE_URL", baseURL)
|
||||
case "kimi":
|
||||
appendEntry("KIMI_CN_API_KEY", apiKey)
|
||||
appendEntry("KIMI_BASE_URL", baseURL)
|
||||
case "minimax":
|
||||
appendEntry("MINIMAX_CN_API_KEY", apiKey)
|
||||
appendEntry("MINIMAX_CN_BASE_URL", baseURL)
|
||||
case "xiaomi":
|
||||
appendEntry("XIAOMI_API_KEY", apiKey)
|
||||
appendEntry("XIAOMI_BASE_URL", baseURL)
|
||||
case "deepseek":
|
||||
appendEntry("DEEPSEEK_API_KEY", apiKey)
|
||||
appendEntry("DEEPSEEK_BASE_URL", baseURL)
|
||||
case "bailian-coding-plan":
|
||||
appendEntry("DASHSCOPE_API_KEY", apiKey)
|
||||
appendEntry("DASHSCOPE_BASE_URL", baseURL)
|
||||
case "openai":
|
||||
appendEntry("OPENAI_API_KEY", apiKey)
|
||||
appendEntry("OPENAI_BASE_URL", baseURL)
|
||||
case "custom", "vllm":
|
||||
if account.APIType == "openai-completions" || account.APIType == "openai-responses" {
|
||||
appendEntry("OPENAI_API_KEY", apiKey)
|
||||
appendEntry("OPENAI_BASE_URL", baseURL)
|
||||
}
|
||||
default:
|
||||
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" {
|
||||
appendEntry(envKey, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
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 {
|
||||
if value == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
seen := make(map[string]struct{}, len(parts))
|
||||
for _, part := range parts {
|
||||
item := strings.TrimSpace(part)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func joinHermesEnvList(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
item := strings.TrimSpace(value)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func firstHermesTelegramBotToken(bots []dto.AgentTelegramBot, defaultAccount string) string {
|
||||
for _, bot := range bots {
|
||||
if bot.AccountID == defaultAccount || bot.IsDefault {
|
||||
return bot.BotToken
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return ""
|
||||
}
|
||||
return bots[0].BotToken
|
||||
}
|
||||
|
||||
func firstHermesDiscordBotToken(bots []dto.AgentDiscordBot, defaultAccount string) string {
|
||||
for _, bot := range bots {
|
||||
if bot.AccountID == defaultAccount || bot.IsDefault {
|
||||
return bot.Token
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return ""
|
||||
}
|
||||
return bots[0].Token
|
||||
}
|
||||
|
||||
func extractHermesEnvBool(envMap map[string]string, key string, defaultValue bool) bool {
|
||||
value, ok := envMap[key]
|
||||
if !ok || value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return strings.EqualFold(value, "true")
|
||||
}
|
||||
|
||||
func validateHermesPairingApproveResult(output string, err error) error {
|
||||
if strings.Contains(output, "Approved!") {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(output, "not found or expired for platform") {
|
||||
return buserr.New("ErrHermesPairingCodeUnavailable")
|
||||
}
|
||||
if err == nil {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
return fmt.Errorf("unexpected hermes pairing approve result")
|
||||
}
|
||||
return errors.New(strings.TrimSpace(output))
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found or expired for platform") {
|
||||
return buserr.New("ErrHermesPairingCodeUnavailable")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
const hermesWeixinLoginScript = `import asyncio
|
||||
from gateway.platforms.weixin import qr_login
|
||||
from hermes_cli.config import save_env_value
|
||||
|
||||
creds = asyncio.run(qr_login('/opt/data'))
|
||||
print('RESULT =', creds)
|
||||
|
||||
if not creds:
|
||||
raise SystemExit(1)
|
||||
|
||||
save_env_value('WEIXIN_ACCOUNT_ID', creds.get('account_id', ''))
|
||||
save_env_value('WEIXIN_TOKEN', creds.get('token', ''))
|
||||
if creds.get('base_url'):
|
||||
save_env_value('WEIXIN_BASE_URL', creds['base_url'])
|
||||
save_env_value('WEIXIN_CDN_BASE_URL', 'https://novac2c.cdn.weixin.qq.com/c2c')
|
||||
save_env_value('WEIXIN_DM_POLICY', 'open')
|
||||
save_env_value('WEIXIN_ALLOW_ALL_USERS', 'true')
|
||||
save_env_value('WEIXIN_ALLOWED_USERS', '')
|
||||
save_env_value('WEIXIN_GROUP_POLICY', 'disabled')
|
||||
save_env_value('WEIXIN_GROUP_ALLOWED_USERS', '')
|
||||
save_env_value('WEIXIN_HOME_CHANNEL', creds.get('user_id', ''))
|
||||
|
||||
print('WEIXIN ENV WRITTEN')
|
||||
print('Restart the Hermes-Agent container to apply the new Weixin settings.')
|
||||
`
|
||||
|
||||
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "qq")
|
||||
extra := childMap(platform, "extra")
|
||||
appID := envMap["QQ_APP_ID"]
|
||||
clientSecret := envMap["QQ_CLIENT_SECRET"]
|
||||
dmPolicy := extractStringValue(extra["dm_policy"])
|
||||
allowFrom := splitHermesEnvList(envMap["QQ_ALLOWED_USERS"])
|
||||
groupPolicy := extractStringValue(extra["group_policy"])
|
||||
groupAllowFrom := splitHermesEnvList(envMap["QQ_GROUP_ALLOWED_USERS"])
|
||||
|
||||
result := &dto.AgentQQBotConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], false) && appID != "" && clientSecret != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: groupAllowFrom,
|
||||
}
|
||||
if appID != "" || clientSecret != "" {
|
||||
result.Bots = []dto.AgentQQBotBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: appID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultBot := getDefaultQQBot(config.Bots)
|
||||
envMap["QQ_APP_ID"] = defaultBot.AppID
|
||||
envMap["QQ_CLIENT_SECRET"] = defaultBot.ClientSecret
|
||||
delete(envMap, "QQ_ALLOWED_USERS")
|
||||
delete(envMap, "QQ_GROUP_ALLOWED_USERS")
|
||||
delete(envMap, "QQ_ALLOW_ALL_USERS")
|
||||
delete(envMap, "QQ_MARKDOWN_SUPPORT")
|
||||
if config.DmPolicy == "open" {
|
||||
envMap["QQ_ALLOW_ALL_USERS"] = "true"
|
||||
} else if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["QQ_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.GroupAllowFrom); allow != "" {
|
||||
envMap["QQ_GROUP_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"QQ_APP_ID",
|
||||
"QQ_CLIENT_SECRET",
|
||||
"QQ_ALLOW_ALL_USERS",
|
||||
"QQ_ALLOWED_USERS",
|
||||
"QQ_GROUP_ALLOWED_USERS",
|
||||
"QQ_HOME_CHANNEL",
|
||||
"QQ_HOME_CHANNEL_NAME",
|
||||
"QQ_STT_API_KEY",
|
||||
"QQ_STT_BASE_URL",
|
||||
"QQ_STT_MODEL",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "qq")
|
||||
platform["enabled"] = config.Enabled && defaultBot.AppID != "" && defaultBot.ClientSecret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
extra["markdown_support"] = true
|
||||
extra["dm_policy"] = config.DmPolicy
|
||||
extra["group_policy"] = config.GroupPolicy
|
||||
delete(extra, "app_id")
|
||||
delete(extra, "client_secret")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
extra["allow_from"] = append([]string(nil), config.AllowFrom...)
|
||||
} else {
|
||||
delete(extra, "allow_from")
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
extra["group_allow_from"] = append([]string(nil), config.GroupAllowFrom...)
|
||||
} else {
|
||||
delete(extra, "group_allow_from")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesQQBotChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"QQ_APP_ID",
|
||||
"QQ_CLIENT_SECRET",
|
||||
"QQ_ALLOW_ALL_USERS",
|
||||
"QQ_ALLOWED_USERS",
|
||||
"QQ_GROUP_ALLOWED_USERS",
|
||||
"QQ_HOME_CHANNEL",
|
||||
"QQ_HOME_CHANNEL_NAME",
|
||||
"QQ_STT_API_KEY",
|
||||
"QQ_STT_BASE_URL",
|
||||
"QQ_STT_MODEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"qq"})
|
||||
}
|
||||
|
||||
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "wecom")
|
||||
allowFrom := splitHermesEnvList(envMap["WECOM_ALLOWED_USERS"])
|
||||
groupAllowFrom := splitHermesEnvList(envMap["WECOM_GROUP_ALLOWED_USERS"])
|
||||
dmPolicy := envMap["WECOM_DM_POLICY"]
|
||||
groupPolicy := envMap["WECOM_GROUP_POLICY"]
|
||||
|
||||
botID := envMap["WECOM_BOT_ID"]
|
||||
secret := envMap["WECOM_SECRET"]
|
||||
|
||||
return &dto.AgentWecomConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], false) && botID != "" && secret != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: groupAllowFrom,
|
||||
BotID: botID,
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.BotID != "" {
|
||||
envMap["WECOM_BOT_ID"] = config.BotID
|
||||
} else {
|
||||
delete(envMap, "WECOM_BOT_ID")
|
||||
}
|
||||
if config.Secret != "" {
|
||||
envMap["WECOM_SECRET"] = config.Secret
|
||||
} else {
|
||||
delete(envMap, "WECOM_SECRET")
|
||||
}
|
||||
delete(envMap, "WECOM_ALLOWED_USERS")
|
||||
delete(envMap, "WECOM_GROUP_ALLOWED_USERS")
|
||||
delete(envMap, "WECOM_ALLOW_ALL_USERS")
|
||||
delete(envMap, "WECOM_DM_POLICY")
|
||||
delete(envMap, "WECOM_GROUP_POLICY")
|
||||
if config.DmPolicy != "" {
|
||||
envMap["WECOM_DM_POLICY"] = config.DmPolicy
|
||||
}
|
||||
if config.GroupPolicy != "" {
|
||||
envMap["WECOM_GROUP_POLICY"] = config.GroupPolicy
|
||||
}
|
||||
if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["WECOM_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.GroupAllowFrom); allow != "" {
|
||||
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"WECOM_BOT_ID",
|
||||
"WECOM_SECRET",
|
||||
"WECOM_DM_POLICY",
|
||||
"WECOM_ALLOWED_USERS",
|
||||
"WECOM_GROUP_POLICY",
|
||||
"WECOM_GROUP_ALLOWED_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "wecom")
|
||||
platform["enabled"] = config.Enabled && config.BotID != "" && config.Secret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
extra["dm_policy"] = config.DmPolicy
|
||||
extra["group_policy"] = config.GroupPolicy
|
||||
delete(extra, "bot_id")
|
||||
delete(extra, "secret")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
extra["allow_from"] = append([]string(nil), config.AllowFrom...)
|
||||
} else {
|
||||
delete(extra, "allow_from")
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
extra["group_allow_from"] = append([]string(nil), config.GroupAllowFrom...)
|
||||
} else {
|
||||
delete(extra, "group_allow_from")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesWecomChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"WECOM_BOT_ID",
|
||||
"WECOM_SECRET",
|
||||
"WECOM_ALLOW_ALL_USERS",
|
||||
"WECOM_DM_POLICY",
|
||||
"WECOM_ALLOWED_USERS",
|
||||
"WECOM_GROUP_POLICY",
|
||||
"WECOM_GROUP_ALLOWED_USERS",
|
||||
"WECOM_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"wecom"})
|
||||
}
|
||||
|
||||
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "dingtalk")
|
||||
extra := childMap(platform, "extra")
|
||||
clientID := envMap["DINGTALK_CLIENT_ID"]
|
||||
clientSecret := envMap["DINGTALK_CLIENT_SECRET"]
|
||||
allowFrom := splitHermesEnvList(envMap["DINGTALK_ALLOWED_USERS"])
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "DINGTALK_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if extractStringValue(extra["unauthorized_dm_behavior"]) == "ignore" {
|
||||
dmPolicy = "disabled"
|
||||
} else if clientID != "" || clientSecret != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
result := &dto.AgentDingTalkConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], clientID != "" && clientSecret != ""),
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: "",
|
||||
GroupAllowFrom: []string{},
|
||||
}
|
||||
if clientID != "" || clientSecret != "" {
|
||||
result.Bots = []dto.AgentDingTalkBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientID, clientSecret := firstHermesDingTalkBotCredentials(config.Bots)
|
||||
if config.Enabled && clientID != "" {
|
||||
envMap["DINGTALK_CLIENT_ID"] = clientID
|
||||
} else {
|
||||
delete(envMap, "DINGTALK_CLIENT_ID")
|
||||
}
|
||||
if config.Enabled && clientSecret != "" {
|
||||
envMap["DINGTALK_CLIENT_SECRET"] = clientSecret
|
||||
} else {
|
||||
delete(envMap, "DINGTALK_CLIENT_SECRET")
|
||||
}
|
||||
delete(envMap, "DINGTALK_ALLOWED_USERS")
|
||||
delete(envMap, "DINGTALK_ALLOW_ALL_USERS")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["DINGTALK_ALLOWED_USERS"] = allow
|
||||
}
|
||||
} else if config.DmPolicy == "open" {
|
||||
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"DINGTALK_CLIENT_ID",
|
||||
"DINGTALK_CLIENT_SECRET",
|
||||
"DINGTALK_ALLOW_ALL_USERS",
|
||||
"DINGTALK_ALLOWED_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "dingtalk")
|
||||
platform["enabled"] = config.Enabled && clientID != "" && clientSecret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
switch config.DmPolicy {
|
||||
case "pairing":
|
||||
extra["unauthorized_dm_behavior"] = "pair"
|
||||
case "disabled":
|
||||
extra["unauthorized_dm_behavior"] = "ignore"
|
||||
default:
|
||||
delete(extra, "unauthorized_dm_behavior")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesDingTalkChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"DINGTALK_CLIENT_ID",
|
||||
"DINGTALK_CLIENT_SECRET",
|
||||
"DINGTALK_ALLOW_ALL_USERS",
|
||||
"DINGTALK_ALLOWED_USERS",
|
||||
"DINGTALK_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"dingtalk"})
|
||||
}
|
||||
|
||||
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "feishu")
|
||||
appID := envMap["FEISHU_APP_ID"]
|
||||
appSecret := envMap["FEISHU_APP_SECRET"]
|
||||
allowFrom := splitHermesEnvList(envMap["FEISHU_ALLOWED_USERS"])
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "FEISHU_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if appID != "" || appSecret != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := envMap["FEISHU_GROUP_POLICY"]
|
||||
result := &dto.AgentFeishuConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], appID != "" && appSecret != ""),
|
||||
ThreadSession: false,
|
||||
ReplyMode: "",
|
||||
Streaming: false,
|
||||
RequireMention: "",
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: []string{},
|
||||
Domain: envMap["FEISHU_DOMAIN"],
|
||||
ConnectionMode: envMap["FEISHU_CONNECTION_MODE"],
|
||||
}
|
||||
if appID != "" || appSecret != "" {
|
||||
result.Bots = []dto.AgentFeishuBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bot := firstHermesFeishuBot(config.Bots)
|
||||
if config.GroupPolicy == "allowlist" && bot.DmPolicy == "pairing" {
|
||||
return buserr.New("ErrHermesFeishuGroupAllowlistRequiresAllowlist")
|
||||
}
|
||||
if bot.AppID != "" {
|
||||
envMap["FEISHU_APP_ID"] = bot.AppID
|
||||
} else {
|
||||
delete(envMap, "FEISHU_APP_ID")
|
||||
}
|
||||
if bot.AppSecret != "" {
|
||||
envMap["FEISHU_APP_SECRET"] = bot.AppSecret
|
||||
} else {
|
||||
delete(envMap, "FEISHU_APP_SECRET")
|
||||
}
|
||||
envMap["FEISHU_DOMAIN"] = "feishu"
|
||||
envMap["FEISHU_CONNECTION_MODE"] = "websocket"
|
||||
envMap["FEISHU_GROUP_POLICY"] = config.GroupPolicy
|
||||
delete(envMap, "FEISHU_ALLOW_ALL_USERS")
|
||||
delete(envMap, "FEISHU_ALLOWED_USERS")
|
||||
switch bot.DmPolicy {
|
||||
case "open":
|
||||
envMap["FEISHU_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(bot.AllowFrom); allow != "" {
|
||||
envMap["FEISHU_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(bot.AllowFrom); allow != "" {
|
||||
envMap["FEISHU_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_DOMAIN",
|
||||
"FEISHU_CONNECTION_MODE",
|
||||
"FEISHU_ALLOW_ALL_USERS",
|
||||
"FEISHU_ALLOWED_USERS",
|
||||
"FEISHU_GROUP_POLICY",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "feishu")
|
||||
platform["enabled"] = config.Enabled && bot.AppID != "" && bot.AppSecret != ""
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesFeishuChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_DOMAIN",
|
||||
"FEISHU_CONNECTION_MODE",
|
||||
"FEISHU_ALLOW_ALL_USERS",
|
||||
"FEISHU_ALLOWED_USERS",
|
||||
"FEISHU_GROUP_POLICY",
|
||||
"FEISHU_HOME_CHANNEL",
|
||||
"FEISHU_VERIFICATION_TOKEN",
|
||||
"FEISHU_ENCRYPT_KEY",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"feishu"})
|
||||
}
|
||||
|
||||
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentWeixinConfig{
|
||||
Enabled: envMap["WEIXIN_ACCOUNT_ID"] != "" || envMap["WEIXIN_TOKEN"] != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func deleteHermesWeixinChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"WEIXIN_ACCOUNT_ID",
|
||||
"WEIXIN_TOKEN",
|
||||
"WEIXIN_BASE_URL",
|
||||
"WEIXIN_CDN_BASE_URL",
|
||||
"WEIXIN_DM_POLICY",
|
||||
"WEIXIN_ALLOW_ALL_USERS",
|
||||
"WEIXIN_ALLOWED_USERS",
|
||||
"WEIXIN_GROUP_POLICY",
|
||||
"WEIXIN_GROUP_ALLOWED_USERS",
|
||||
"WEIXIN_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"weixin"})
|
||||
}
|
||||
|
||||
func firstHermesDingTalkBotCredentials(bots []dto.AgentDingTalkBot) (string, string) {
|
||||
for _, bot := range bots {
|
||||
if bot.IsDefault || bot.AccountID == "default" {
|
||||
return bot.ClientID, bot.ClientSecret
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return bots[0].ClientID, bots[0].ClientSecret
|
||||
}
|
||||
|
||||
func firstHermesFeishuBot(bots []dto.AgentFeishuBot) dto.AgentFeishuBot {
|
||||
for _, bot := range bots {
|
||||
if bot.IsDefault || bot.AccountID == "default" {
|
||||
return bot
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return dto.AgentFeishuBot{}
|
||||
}
|
||||
return bots[0]
|
||||
}
|
||||
|
||||
func buildHermesWeixinLoginArgs(containerName string) []string {
|
||||
return buildHermesDockerExecCommandArgs(
|
||||
containerName,
|
||||
"/opt/hermes/.venv/bin/python",
|
||||
"-u",
|
||||
"-c",
|
||||
hermesWeixinLoginScript,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
const hermesSessionListLimit = 100
|
||||
|
||||
func (a AgentService) GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return listHermesChatSessionsFromStateDB(filepath.Join(install.GetPath(), "data", "state.db"))
|
||||
}
|
||||
|
||||
func (a AgentService) RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
|
||||
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||
"docker",
|
||||
buildHermesDockerExecArgs(install.ContainerName, "sessions", "rename", req.ID, req.Title)...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
|
||||
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||
"docker",
|
||||
buildHermesDockerExecArgs(install.ContainerName, "sessions", "delete", req.ID, "--yes")...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func listHermesChatSessionsFromStateDB(stateDBPath string) ([]dto.AgentHermesChatSessionItem, error) {
|
||||
if !files.NewFileOp().Stat(stateDBPath) {
|
||||
return []dto.AgentHermesChatSessionItem{}, nil
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", stateDBPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
s.id,
|
||||
COALESCE(NULLIF(TRIM(s.title), ''), s.id) AS title,
|
||||
COALESCE(s.model, '') AS model,
|
||||
COALESCE(s.message_count, 0) AS message_count,
|
||||
s.started_at,
|
||||
COALESCE(MAX(m.timestamp), s.started_at) AS last_active
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
WHERE s.source = 'cli'
|
||||
GROUP BY s.id, title, model, s.message_count, s.started_at
|
||||
ORDER BY last_active DESC
|
||||
LIMIT ?
|
||||
`, hermesSessionListLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]dto.AgentHermesChatSessionItem, 0, 8)
|
||||
for rows.Next() {
|
||||
var item dto.AgentHermesChatSessionItem
|
||||
var title sql.NullString
|
||||
var model sql.NullString
|
||||
var startedAt sql.NullFloat64
|
||||
var lastActive sql.NullFloat64
|
||||
if err := rows.Scan(&item.ID, &title, &model, &item.MessageCount, &startedAt, &lastActive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Title = strings.TrimSpace(title.String)
|
||||
if item.Title == "" {
|
||||
item.Title = item.ID
|
||||
}
|
||||
item.Model = strings.TrimSpace(model.String)
|
||||
item.StartedAt = formatHermesSessionTimestamp(startedAt)
|
||||
item.LastActive = formatHermesSessionTimestamp(lastActive)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func formatHermesSessionTimestamp(value sql.NullFloat64) string {
|
||||
if !value.Valid || value.Float64 <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
seconds, fraction := math.Modf(value.Float64)
|
||||
return time.Unix(int64(seconds), int64(fraction*float64(time.Second))).UTC().Format(time.RFC3339)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
type hermesSkillsListEntry struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type hermesSkillsListPayload struct {
|
||||
Skills []hermesSkillsListEntry `json:"skills"`
|
||||
}
|
||||
|
||||
func listHermesSkills(containerName string) ([]dto.AgentSkillItem, error) {
|
||||
output, err := runHermesSkillsCommandWithStdout(2*time.Minute, containerName, "list", "--source", "all")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := parseHermesSkillsListOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata, err := readHermesSkillsListMetadata(containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mergeHermesSkillsWithMetadata(items, metadata), nil
|
||||
}
|
||||
|
||||
func searchHermesSkills(containerName, source, keyword string) ([]dto.AgentSkillSearchItem, error) {
|
||||
if source != "official" && source != "skills-sh" {
|
||||
return nil, fmt.Errorf("unsupported hermes skill source: %s", source)
|
||||
}
|
||||
output, err := runHermesSkillsCommandWithStdout(
|
||||
2*time.Minute,
|
||||
containerName,
|
||||
"search",
|
||||
keyword,
|
||||
"--source",
|
||||
source,
|
||||
"--limit",
|
||||
"20",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseHermesSkillSearchOutput(output)
|
||||
}
|
||||
|
||||
func runHermesSkillsCommandWithStdout(timeout time.Duration, containerName string, hermesArgs ...string) (string, error) {
|
||||
args := []string{"exec", "-e", "COLUMNS=240", "-u", "hermes", containerName, "hermes", "skills"}
|
||||
args = append(args, hermesArgs...)
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", args...)
|
||||
}
|
||||
|
||||
func readHermesSkillsListMetadata(containerName string) (map[string]hermesSkillsListEntry, error) {
|
||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).RunWithStdout(
|
||||
"docker",
|
||||
"exec",
|
||||
"-u",
|
||||
"hermes",
|
||||
containerName,
|
||||
"python",
|
||||
"-c",
|
||||
"import sys; sys.path.insert(0, '/opt/hermes'); from tools.skills_tool import skills_list; print(skills_list())",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseHermesSkillsListMetadataOutput(output)
|
||||
}
|
||||
|
||||
func parseHermesSkillsListOutput(output string) ([]dto.AgentSkillItem, error) {
|
||||
headers, rows, err := parseHermesTableOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.AgentSkillItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item := dto.AgentSkillItem{
|
||||
Name: row[headers["Name"]],
|
||||
Category: row[headers["Category"]],
|
||||
Source: row[headers["Source"]],
|
||||
Trust: row[headers["Trust"]],
|
||||
Uninstallable: row[headers["Source"]] != "builtin" && row[headers["Source"]] != "local",
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseHermesSkillsListMetadataOutput(output string) (map[string]hermesSkillsListEntry, error) {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
return map[string]hermesSkillsListEntry{}, nil
|
||||
}
|
||||
var payload hermesSkillsListPayload
|
||||
if err := json.Unmarshal([]byte(output), &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata := make(map[string]hermesSkillsListEntry, len(payload.Skills))
|
||||
for _, skill := range payload.Skills {
|
||||
if skill.Name == "" {
|
||||
continue
|
||||
}
|
||||
metadata[skill.Name] = skill
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func mergeHermesSkillsWithMetadata(items []dto.AgentSkillItem, metadata map[string]hermesSkillsListEntry) []dto.AgentSkillItem {
|
||||
for i := range items {
|
||||
entry, ok := metadata[items[i].Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items[i].Description = entry.Description
|
||||
if items[i].Category == "" && entry.Category != "" {
|
||||
items[i].Category = entry.Category
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func parseHermesSkillSearchOutput(output string) ([]dto.AgentSkillSearchItem, error) {
|
||||
if strings.Contains(output, "No skills found matching your query.") {
|
||||
return []dto.AgentSkillSearchItem{}, nil
|
||||
}
|
||||
headers, rows, err := parseHermesTableOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.AgentSkillSearchItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
identifier := row[headers["Identifier"]]
|
||||
items = append(items, dto.AgentSkillSearchItem{
|
||||
Slug: identifier,
|
||||
Identifier: identifier,
|
||||
Name: row[headers["Name"]],
|
||||
Description: row[headers["Description"]],
|
||||
Source: row[headers["Source"]],
|
||||
Trust: row[headers["Trust"]],
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseHermesTableOutput(output string) (map[string]int, [][]string, error) {
|
||||
lines := strings.Split(strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, "")), "\n")
|
||||
var headers []string
|
||||
rows := make([][]string, 0)
|
||||
var current []string
|
||||
|
||||
for _, rawLine := range lines {
|
||||
line := strings.TrimSpace(rawLine)
|
||||
if (!strings.HasPrefix(line, "│") || !strings.HasSuffix(line, "│")) &&
|
||||
(!strings.HasPrefix(line, "┃") || !strings.HasSuffix(line, "┃")) {
|
||||
continue
|
||||
}
|
||||
line = strings.ReplaceAll(line, "┃", "│")
|
||||
parts := strings.Split(line, "│")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
cols := make([]string, 0, len(parts)-2)
|
||||
for _, part := range parts[1 : len(parts)-1] {
|
||||
cols = append(cols, strings.TrimSpace(part))
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
headers = cols
|
||||
continue
|
||||
}
|
||||
if cols[0] != "" {
|
||||
if current != nil {
|
||||
rows = append(rows, current)
|
||||
}
|
||||
current = cols
|
||||
continue
|
||||
}
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
for i := range cols {
|
||||
if cols[i] == "" {
|
||||
continue
|
||||
}
|
||||
if current[i] == "" {
|
||||
current[i] = cols[i]
|
||||
continue
|
||||
}
|
||||
current[i] = current[i] + " " + cols[i]
|
||||
}
|
||||
}
|
||||
|
||||
if current != nil {
|
||||
rows = append(rows, current)
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil, nil, fmt.Errorf("hermes skills table not found")
|
||||
}
|
||||
|
||||
headerIndex := make(map[string]int, len(headers))
|
||||
for i, header := range headers {
|
||||
headerIndex[header] = i
|
||||
}
|
||||
return headerIndex, rows, nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
@@ -41,13 +42,19 @@ var clawhubSearchLinePattern = regexp.MustCompile(`^(\S+)\s+(.+?)\s+\(([\d.]+)\)
|
||||
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
||||
|
||||
func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, error) {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return listHermesSkills(install.ContainerName)
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
output, err := runDockerExecWithStdout(5*time.Minute, install.ContainerName, "sh", "-c", "openclaw skills list --json 2>&1")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -59,13 +66,19 @@ func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, erro
|
||||
}
|
||||
|
||||
func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return searchHermesSkills(install.ContainerName, req.Source, req.Keyword)
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
output, err := loadOpenclawSkillSearchOutput(install.ContainerName, req.Source, req.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -102,7 +115,7 @@ func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,6 +126,21 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
installTask.AddSubTask("Install Hermes skill", func(t *task.Task) error {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
|
||||
return mgr.Run("docker", buildHermesDockerExecArgs(install.ContainerName, "skills", "install", req.Slug, "--yes")...)
|
||||
}, nil)
|
||||
go func() {
|
||||
if err := installTask.Execute(); err != nil {
|
||||
global.LOG.Errorf("install hermes skill failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
|
||||
return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug))
|
||||
@@ -125,6 +153,30 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) UninstallSkill(req dto.AgentSkillUninstallReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).Run(
|
||||
"docker",
|
||||
buildHermesDockerExecCommandArgs(
|
||||
install.ContainerName,
|
||||
"sh",
|
||||
"-lc",
|
||||
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
|
||||
"sh",
|
||||
req.Name,
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
|
||||
payloadBytes, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
|
||||
@@ -43,8 +43,8 @@ func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, nil
|
||||
}
|
||||
@@ -386,6 +386,10 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
return item
|
||||
}
|
||||
|
||||
func isAgentAppKey(appKey string) bool {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
}
|
||||
|
||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||
return !common.CompareAppVersion(version, openclawHTTPSVersion)
|
||||
}
|
||||
@@ -1002,14 +1006,10 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if len(accountModels) == 0 {
|
||||
return "", nil, nil, fmt.Errorf("model is required")
|
||||
}
|
||||
selectedModel = strings.TrimSpace(selectedModel)
|
||||
if selectedModel == "" {
|
||||
selectedModel = strings.TrimSpace(accountModels[0].ID)
|
||||
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if selectedModel == "" {
|
||||
return "", nil, nil, fmt.Errorf("model is required")
|
||||
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, selectedModel)
|
||||
if err != nil {
|
||||
@@ -1460,9 +1460,6 @@ func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.Agent
|
||||
return err
|
||||
}
|
||||
for _, agent := range agents {
|
||||
if strings.TrimSpace(agent.Model) == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, agent.Model); !ok {
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,27 @@ func (a AgentService) BindWebsite(req dto.AgentWebsiteBindReq) error {
|
||||
return ensureOpenclawWebsiteAllowedOrigin(agent, &website)
|
||||
}
|
||||
|
||||
func (a AgentService) UnbindWebsite(req dto.AgentIDReq) error {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.WebsiteID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if website.Type == constant.Deployment {
|
||||
return buserr.New("ErrAgentWebsiteUnbindUnsupported")
|
||||
}
|
||||
|
||||
agent.WebsiteID = 0
|
||||
return agentRepo.Save(agent)
|
||||
}
|
||||
|
||||
func hydrateAgentWebsiteItems(items []dto.AgentItem) error {
|
||||
explicitWebsiteMap, err := loadAgentWebsiteMapByID(items)
|
||||
if err != nil {
|
||||
@@ -126,9 +147,11 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
|
||||
if !ok {
|
||||
items[index].WebsiteID = 0
|
||||
items[index].WebsitePrimaryDomain = ""
|
||||
items[index].WebsiteType = ""
|
||||
items[index].WebsiteProtocol = ""
|
||||
continue
|
||||
}
|
||||
items[index].WebsiteType = website.Type
|
||||
items[index].WebsiteProtocol = website.Protocol
|
||||
websiteDomains := websiteDomainMap[items[index].WebsiteID]
|
||||
if len(websiteDomains) == 0 {
|
||||
@@ -192,7 +215,7 @@ func bindDeploymentWebsiteToAgentByAppInstall(website *model.Website) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInstall.App.Key != constant.AppOpenclaw && appInstall.App.Key != constant.AppCopaw {
|
||||
if appInstall.App.Key != constant.AppOpenclaw && appInstall.App.Key != constant.AppCopaw && appInstall.App.Key != constant.AppHermesAgent {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -215,7 +238,7 @@ func bindDeploymentWebsiteToAgentByAppInstall(website *model.Website) error {
|
||||
}
|
||||
|
||||
func ensureOpenclawWebsiteAllowedOrigin(agent *model.Agent, website *model.Website) error {
|
||||
if agent == nil || website == nil || agent.AgentType == constant.AppCopaw {
|
||||
if agent == nil || website == nil || agent.AgentType != constant.AppOpenclaw {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -576,7 +577,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
}
|
||||
|
||||
installTask.AddSubTask(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus)
|
||||
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour)
|
||||
|
||||
go func() {
|
||||
if taskErr := installTask.Execute(); taskErr != nil {
|
||||
|
||||
@@ -874,7 +874,7 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
||||
envKey := ""
|
||||
switch param {
|
||||
case "password":
|
||||
if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" {
|
||||
if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" || appKey == "mongodb" {
|
||||
envKey = "PANEL_DB_ROOT_PASSWORD="
|
||||
} else {
|
||||
envKey = "PANEL_REDIS_ROOT_PASSWORD="
|
||||
@@ -915,7 +915,7 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
||||
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
|
||||
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
|
||||
}, repo.WithByID(appInstall.ID))
|
||||
if appKey == "mysql" || appKey == "postgresql" {
|
||||
if appKey == "mysql" || appKey == "postgresql" || appKey == "mongodb" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
return err
|
||||
}
|
||||
appKey := install.App.Key
|
||||
if appKey == constant.AppOpenclaw || appKey == constant.AppCopaw {
|
||||
if isAgentAppKey(appKey) {
|
||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||
}
|
||||
|
||||
@@ -425,6 +425,8 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
switch install.App.Key {
|
||||
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
|
||||
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
|
||||
case constant.AppMongodb:
|
||||
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
_ = postgresqlRepo.Delete(ctx, postgresqlRepo.WithByPostgresqlName(install.Name))
|
||||
}
|
||||
@@ -2246,10 +2248,39 @@ func isEditCompose(installed model.AppInstall) bool {
|
||||
if rawCompose == "" || err != nil {
|
||||
return false
|
||||
}
|
||||
if rawCompose != installed.DockerCompose {
|
||||
return true
|
||||
equal, err := composeEqualExceptImage(rawCompose, installed.DockerCompose)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !equal
|
||||
}
|
||||
|
||||
func composeEqualExceptImage(expected, current string) (bool, error) {
|
||||
expectedCompose := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(expected), &expectedCompose); err != nil {
|
||||
return false, err
|
||||
}
|
||||
currentCompose := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(current), ¤tCompose); err != nil {
|
||||
return false, err
|
||||
}
|
||||
removeComposeServiceImages(expectedCompose)
|
||||
removeComposeServiceImages(currentCompose)
|
||||
return reflect.DeepEqual(expectedCompose, currentCompose), nil
|
||||
}
|
||||
|
||||
func removeComposeServiceImages(composeMap map[string]interface{}) {
|
||||
services, ok := composeMap["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, service := range services {
|
||||
serviceMap, ok := service.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(serviceMap, "image")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAppVersions(key string, details []model.AppDetail) []string {
|
||||
|
||||
@@ -44,10 +44,13 @@ type IBackupService interface {
|
||||
|
||||
MysqlBackup(db dto.CommonBackup) error
|
||||
PostgresqlBackup(db dto.CommonBackup) error
|
||||
MongodbBackup(db dto.CommonBackup) error
|
||||
MysqlRecover(db dto.CommonRecover) error
|
||||
PostgresqlRecover(db dto.CommonRecover) error
|
||||
MongodbRecover(db dto.CommonRecover) error
|
||||
MysqlRecoverByUpload(req dto.CommonRecover) error
|
||||
PostgresqlRecoverByUpload(req dto.CommonRecover) error
|
||||
MongodbRecoverByUpload(req dto.CommonRecover) error
|
||||
|
||||
RedisBackup(db dto.CommonBackup) error
|
||||
RedisRecover(db dto.CommonRecover) error
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"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/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/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
dockerImage "github.com/docker/docker/api/types/image"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
const opensslSaltedHeader = "Salted__"
|
||||
|
||||
func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
|
||||
timeNow := time.Now().Format(constant.DateTimeSlimLayout)
|
||||
itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName)
|
||||
targetDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||
fileName := fmt.Sprintf("%s_%s.gz", req.DetailName, timeNow+common.RandStrAndNum(5))
|
||||
|
||||
record := &model.BackupRecord{
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
DetailName: req.DetailName,
|
||||
SourceAccountIDs: "1",
|
||||
DownloadAccountID: 1,
|
||||
FileDir: itemDir,
|
||||
FileName: fileName,
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
|
||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecover(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecoverByUpload(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, targetDir, fileName string) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
backupTask := parentTask
|
||||
if backupTask == nil {
|
||||
backupTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
itemHandler := func(t *task.Task) error {
|
||||
return doMongodbBackup(req.Name, req.Type, req.DetailName, targetDir, fileName, req.Secret, t)
|
||||
}
|
||||
if parentTask != nil {
|
||||
return itemHandler(parentTask)
|
||||
}
|
||||
|
||||
backupTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup),
|
||||
func(t *task.Task) error { return itemHandler(t) },
|
||||
nil,
|
||||
0,
|
||||
3*time.Hour,
|
||||
)
|
||||
go func() {
|
||||
if err := backupTask.Execute(); err != nil {
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMongodbRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
recoverTask := parentTask
|
||||
if recoverTask == nil {
|
||||
recoverTask, err = task.NewTaskWithOps(itemName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
recoverDatabase := func(t *task.Task) error {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(req.File) {
|
||||
return buserr.WithName("ErrFileNotFound", req.File)
|
||||
}
|
||||
|
||||
restoreFile, cleanup, err := prepareMongodbBackupFileForRestore(req.File, req.Secret, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
isOk := false
|
||||
if !isRollback {
|
||||
rollbackFile := path.Join(
|
||||
global.Dir.TmpDir,
|
||||
fmt.Sprintf("database/%s/%s_%s.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)),
|
||||
)
|
||||
if err := doMongodbBackup(req.Name, req.Type, req.DetailName, path.Dir(rollbackFile), path.Base(rollbackFile), "", t); err != nil {
|
||||
return fmt.Errorf("backup mongodb db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||
}
|
||||
defer func() {
|
||||
if !isOk {
|
||||
global.LOG.Info("recover failed, start to rollback now")
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, rollbackFile, t); err != nil {
|
||||
global.LOG.Errorf("rollback mongodb db %s from %s failed, err: %v", req.DetailName, rollbackFile, err)
|
||||
} else {
|
||||
global.LOG.Infof("rollback mongodb db %s from %s successful", req.DetailName, rollbackFile)
|
||||
}
|
||||
}
|
||||
_ = os.RemoveAll(rollbackFile)
|
||||
}()
|
||||
}
|
||||
|
||||
if req.DropAllCollections {
|
||||
if err := clearMongodbDatabase(req.Name, req.Type, req.DetailName, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, restoreFile, t); err != nil {
|
||||
global.LOG.Errorf("recover mongodb db %s from %s failed, err: %v", req.DetailName, restoreFile, err)
|
||||
return err
|
||||
}
|
||||
isOk = true
|
||||
return nil
|
||||
}
|
||||
if parentTask != nil {
|
||||
return recoverDatabase(parentTask)
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
switch req.Timeout {
|
||||
case -1:
|
||||
timeout = 0
|
||||
case 0:
|
||||
timeout = 3 * time.Hour
|
||||
default:
|
||||
timeout = time.Duration(req.Timeout) * time.Second
|
||||
}
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskRecover"), recoverDatabase, nil, 0, timeout)
|
||||
go func() {
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbBackup(database, dbType, dbName, targetDir, fileName, secret string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
if err := doRemoteMongodbBackup(database, dbName, targetDir, fileName, taskItem); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(path.Join(targetDir, fileName), secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := path.Join(targetDir, fileName)
|
||||
containerFile := path.Join("/tmp", fileName)
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
uri := buildMongodbDumpURI(appInfo.UserName, appInfo.Password, dbName)
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongodump",
|
||||
"--uri="+uri,
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdMgr.Run("docker", "cp", fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile), targetFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(targetFile, secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbRestore(database, dbType, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
return doRemoteMongodbRestore(database, dbName, sourceFile, taskItem)
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
|
||||
containerFile := path.Join("/tmp", fmt.Sprintf("1panel-mongodb-restore-%s.gz", common.RandStrAndNum(8)))
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run("docker", "cp", sourceFile, fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongorestore",
|
||||
"--uri="+uri,
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
"--drop",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareMongodbBackupFileForRestore(filePath, secret string, taskItem *task.Task) (string, func(), error) {
|
||||
isEncrypted, err := isOpenSSLEncryptedMongodbBackup(filePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !isEncrypted {
|
||||
return filePath, func() {}, nil
|
||||
}
|
||||
|
||||
if secret == "" {
|
||||
return "", nil, buserr.New("ErrBadDecrypt")
|
||||
}
|
||||
|
||||
if err := files.OpensslDecrypt(filePath, secret); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
restoreFile := path.Join(path.Dir(filePath), "tmp_"+path.Base(filePath))
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("Decrypt"), nil)
|
||||
return restoreFile, func() { _ = os.Remove(restoreFile) }, nil
|
||||
}
|
||||
|
||||
func isOpenSSLEncryptedMongodbBackup(filePath string) (bool, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
header := make([]byte, len(opensslSaltedHeader))
|
||||
n, err := io.ReadFull(file, header)
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return n == len(opensslSaltedHeader) && string(header) == opensslSaltedHeader, nil
|
||||
}
|
||||
|
||||
func clearMongodbDatabase(database, dbType, dbName string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
return clearRemoteMongodbDatabase(database, dbName, taskItem)
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear local mongodb database %s before restore", dbName))
|
||||
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
|
||||
return mongodbCmdMgr(taskItem).Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
uri,
|
||||
"--quiet",
|
||||
"--eval",
|
||||
fmt.Sprintf(`db.getSiblingDB(%q).dropDatabase()`, dbName),
|
||||
)
|
||||
}
|
||||
|
||||
func clearRemoteMongodbDatabase(database, dbName string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear remote mongodb database %s before restore", dbName))
|
||||
if err := client.Database(dbName).Drop(ctx); err != nil {
|
||||
return fmt.Errorf("drop mongodb database %s failed, err: %v", dbName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMongodbDumpURI(username, password, dbName string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/" + dbName,
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func buildMongodbRestoreURI(username, password string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/",
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func mongodbCmdMgr(taskItem *task.Task) *cmd.CommandHelper {
|
||||
if taskItem == nil {
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3 * time.Hour))
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3*time.Hour), cmd.WithTask(*taskItem))
|
||||
}
|
||||
|
||||
func doRemoteMongodbBackup(database, dbName, targetDir, fileName string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "backup", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start backup", imageTag))
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(path.Join(targetDir, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, constant.DirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file %s failed, err: %v", path.Join(targetDir, fileName), err)
|
||||
}
|
||||
defer func() { _ = targetFile.Close() }()
|
||||
|
||||
backupCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongodump",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--db="+dbName,
|
||||
"--archive",
|
||||
"--gzip",
|
||||
)
|
||||
backupCmd.Stdout = targetFile
|
||||
var stderr bytes.Buffer
|
||||
backupCmd.Stderr = &stderr
|
||||
if err := backupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle backup mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRemoteMongodbRestore(database, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "restore", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start restore", imageTag))
|
||||
|
||||
fi, err := os.Open(sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = fi.Close() }()
|
||||
|
||||
restoreCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongorestore",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive",
|
||||
"--gzip",
|
||||
"--drop",
|
||||
)
|
||||
restoreCmd.Stdin = fi
|
||||
var stderr bytes.Buffer
|
||||
restoreCmd.Stderr = &stderr
|
||||
if err := restoreCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle recover mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureMongodbImage(database string, taskItem *task.Task) (string, error) {
|
||||
imageTag, exists, err := loadMongodbImageTag(database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("check local image %s", imageTag))
|
||||
if exists {
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s exists", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s not found, start docker pull", imageTag))
|
||||
if err := mongodbCmdMgr(taskItem).Run("docker", "pull", imageTag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("docker pull %s finished", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
|
||||
func loadMongodbImageTag(database string) (string, bool, error) {
|
||||
databaseInfo, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
images, err := cli.ImageList(context.Background(), dockerImage.ListOptions{})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
imagePrefix := "mongo:" + loadMongodbImageMajor(databaseInfo.Version)
|
||||
for _, image := range images {
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, imagePrefix) {
|
||||
return tag, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return imagePrefix, false, nil
|
||||
}
|
||||
|
||||
func buildMongodbNamespace(sourceFile, targetDB string) string {
|
||||
sourceDB := loadMongodbBackupDBName(sourceFile, targetDB)
|
||||
return sourceDB + ".*"
|
||||
}
|
||||
|
||||
func loadMongodbBackupDBName(sourceFile, defaultDB string) string {
|
||||
baseName := path.Base(sourceFile)
|
||||
if strings.HasSuffix(baseName, ".gz") {
|
||||
baseName = strings.TrimSuffix(baseName, ".gz")
|
||||
}
|
||||
// Encrypted backups are restored from a decrypted temp file like tmp_<original>.
|
||||
// Strip the temp prefix before deriving the original database name.
|
||||
baseName = strings.TrimPrefix(baseName, "tmp_")
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`^1panel_mongodb_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^db_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
matches := pattern.FindStringSubmatch(baseName)
|
||||
if len(matches) == 2 && len(matches[1]) != 0 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
return defaultDB
|
||||
}
|
||||
|
||||
func loadMongodbImageMajor(version string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(version, "6"):
|
||||
return "6"
|
||||
case strings.HasPrefix(version, "7"):
|
||||
return "7"
|
||||
default:
|
||||
return "8"
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbImage(taskItem *task.Task, action, database, dbName, imageTag string, info mongodbConnectionInfo) {
|
||||
message := fmt.Sprintf(
|
||||
"use local docker image %s to %s remote mongodb %s/%s via %s:%d",
|
||||
imageTag,
|
||||
action,
|
||||
database,
|
||||
dbName,
|
||||
info.Address,
|
||||
info.Port,
|
||||
)
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbStep(taskItem *task.Task, message string) {
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
@@ -287,6 +287,15 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(db.Name), repo.WithByName(db.DetailName))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName))
|
||||
|
||||
@@ -165,7 +165,20 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||
|
||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)))
|
||||
switch dbInfo.DBType {
|
||||
case constant.AppMongodb:
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf(
|
||||
"db_%s_%s.gz",
|
||||
dbInfo.Name,
|
||||
startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5),
|
||||
))
|
||||
default:
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf(
|
||||
"db_%s_%s.sql.gz",
|
||||
dbInfo.Name,
|
||||
startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5),
|
||||
))
|
||||
}
|
||||
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
|
||||
if err := doMysqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -177,6 +190,17 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
if err := doMongodbBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName, cronjob.Secret, task); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
retry++
|
||||
return err
|
||||
} else {
|
||||
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
|
||||
cleanAccountMap(accountMap)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := doPostgresqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret, taskItem); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -404,6 +428,18 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
databaseService := NewIDatabaseService()
|
||||
mongodbItems, _ := databaseService.LoadItems(cronjob.DBType)
|
||||
for _, mongodb := range mongodbItems {
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodb.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodb.Database,
|
||||
Name: mongodb.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
pgItems, _ := postgresqlRepo.List()
|
||||
for _, pg := range pgItems {
|
||||
@@ -430,6 +466,15 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Name: mysqlItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
mongodbItem, _ := mongodbRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodbItem.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodbItem.MongodbName,
|
||||
Name: mongodbItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else {
|
||||
pgItem, _ := postgresqlRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
|
||||
@@ -90,7 +90,7 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType))
|
||||
var datas []dto.DatabaseItem
|
||||
for _, db := range dbs {
|
||||
if dbType == constant.AppPostgresql || dbType == constant.AppPostgresqlCluster {
|
||||
if db.Type == constant.AppPostgresql || db.Type == constant.AppPostgresqlCluster {
|
||||
items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name))
|
||||
for _, item := range items {
|
||||
var dItem dto.DatabaseItem
|
||||
@@ -100,6 +100,16 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
dItem.Database = db.Name
|
||||
datas = append(datas, dItem)
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
items, _ := mongodbRepo.List(mongodbRepo.WithByMongodbName(db.Name))
|
||||
for _, item := range items {
|
||||
var dItem dto.DatabaseItem
|
||||
if err := copier.Copy(&dItem, &item); err != nil {
|
||||
continue
|
||||
}
|
||||
dItem.Database = db.Name
|
||||
datas = append(datas, dItem)
|
||||
}
|
||||
} else {
|
||||
items, _ := mysqlRepo.List(mysqlRepo.WithByMysqlName(db.Name))
|
||||
for _, item := range items {
|
||||
@@ -138,6 +148,13 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
})
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, connectErr := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if connectErr == nil {
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
}
|
||||
err = connectErr
|
||||
case "mysql", "mariadb":
|
||||
_, err = mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -195,6 +212,13 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -265,6 +289,10 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
|
||||
if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
if err := mongodbRepo.Delete(context.Background(), mongodbRepo.WithByMongodbName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := postgresqlRepo.Delete(context.Background(), postgresqlRepo.WithByPostgresqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
@@ -297,6 +325,24 @@ func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(dto.DatabaseCreate{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
|
||||
@@ -0,0 +1,948 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/jinzhu/copier"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type MongodbService struct{}
|
||||
|
||||
type IMongodbService interface {
|
||||
SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error)
|
||||
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
|
||||
LoadFromRemote(req dto.MongodbLoadDB) error
|
||||
UpdateDescription(req dto.UpdateDescription) error
|
||||
BindUser(req dto.MongodbBind) error
|
||||
ChangePassword(req dto.MongodbPassword) error
|
||||
ChangeRootPassword(req dto.ChangeDBInfo) error
|
||||
LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error)
|
||||
ChangePrivileges(req dto.MongodbPrivileges) error
|
||||
DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error)
|
||||
Delete(ctx context.Context, req dto.MongodbDBDelete) error
|
||||
}
|
||||
|
||||
func NewIMongodbService() IMongodbService {
|
||||
return &MongodbService{}
|
||||
}
|
||||
|
||||
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) {
|
||||
total, mongodbs, err := mongodbRepo.Page(
|
||||
search.Page,
|
||||
search.PageSize,
|
||||
mongodbRepo.WithByMongodbName(search.Database),
|
||||
repo.WithByLikeName(search.Info),
|
||||
repo.WithOrderRuleBy(search.OrderBy, search.Order),
|
||||
)
|
||||
var dtoMongodbs []dto.MongodbDBInfo
|
||||
for _, mongodb := range mongodbs {
|
||||
var item dto.MongodbDBInfo
|
||||
if err := copier.Copy(&item, &mongodb); err != nil {
|
||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
dtoMongodbs = append(dtoMongodbs, item)
|
||||
}
|
||||
return total, dtoMongodbs, err
|
||||
}
|
||||
|
||||
func (u *MongodbService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return mongodbRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
||||
func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
mongodb, _ := mongodbRepo.Get(repo.WithByName(req.Name), mongodbRepo.WithByMongodbName(req.Database), repo.WithByFrom(req.From))
|
||||
if mongodb.ID != 0 {
|
||||
return nil, buserr.New("ErrRecordExist")
|
||||
}
|
||||
|
||||
var createItem model.DatabaseMongodb
|
||||
if err := copier.Copy(&createItem, &req); err != nil {
|
||||
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
createItem.MongodbName = req.Database
|
||||
|
||||
if err := runMongodbCreate(req.From, req.Database, req.Name, req.Username, req.Password, req.Permission); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
global.LOG.Infof("create mongodb database %s successful", req.Name)
|
||||
if err := mongodbRepo.Create(ctx, &createItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &createItem, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) error {
|
||||
databases, err := mongodbRepo.List(mongodbRepo.WithByMongodbName(req.Database))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datas, err := loadMongodbDatabases(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
global.LOG.Infof("sync mongodb databases from %s:%s, found %d items", req.From, req.Database, len(datas))
|
||||
|
||||
deleteList := append([]model.DatabaseMongodb(nil), databases...)
|
||||
for _, data := range datas {
|
||||
hasOld := false
|
||||
for i := 0; i < len(databases); i++ {
|
||||
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MongodbName, req.Database) {
|
||||
hasOld = true
|
||||
updateMap := map[string]interface{}{"is_delete": false}
|
||||
if len(data.Username) != 0 {
|
||||
updateMap["username"] = data.Username
|
||||
}
|
||||
_ = mongodbRepo.Update(databases[i].ID, updateMap)
|
||||
for j := 0; j < len(deleteList); j++ {
|
||||
if deleteList[j].ID == databases[i].ID {
|
||||
deleteList = append(deleteList[:j], deleteList[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOld {
|
||||
createItem := model.DatabaseMongodb{
|
||||
Name: data.Name,
|
||||
From: req.From,
|
||||
MongodbName: req.Database,
|
||||
Username: data.Username,
|
||||
Password: "",
|
||||
Description: "",
|
||||
}
|
||||
if err := mongodbRepo.Create(context.Background(), &createItem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, delItem := range deleteList {
|
||||
_ = mongodbRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) BindUser(req dto.MongodbBind) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindMongodbUser(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"password": pass,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbItem.Username == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := updateMongodbPassword(dbItem.From, dbItem.MongodbName, dbItem.Name, dbItem.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{"password": pass})
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangeRootPassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if req.From != constant.AppResourceLocal {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.UserName == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := updateMongodbPassword(req.From, req.Database, "admin", appInfo.UserName, req.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
remote, err := databaseRepo.Get(repo.WithByName(req.Database))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb root password failed, err: %v", err)
|
||||
}
|
||||
_ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error) {
|
||||
var res []dto.DBResource
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if db.From == "local" {
|
||||
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error) {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return loadMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username)
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePrivileges(req dto.MongodbPrivileges) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Username) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Permission)
|
||||
}
|
||||
|
||||
func (u *MongodbService) Delete(ctx context.Context, req dto.MongodbDBDelete) error {
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runMongodbDelete(db.From, req.Database, db.Name); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.DeleteBackup {
|
||||
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(uploadDir); err == nil {
|
||||
_ = os.RemoveAll(uploadDir)
|
||||
}
|
||||
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(backupDir); err == nil {
|
||||
_ = os.RemoveAll(backupDir)
|
||||
}
|
||||
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(req.Type), repo.WithByName(req.Database), repo.WithByDetailName(db.Name))
|
||||
global.LOG.Infof("delete mongodb database %s-%s backups successful", req.Database, db.Name)
|
||||
}
|
||||
|
||||
_ = mongodbRepo.Delete(ctx, repo.WithByID(db.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMongodbCreate(from, database, dbName, username, password, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbCreate(database, dbName, username, password, permission)
|
||||
}
|
||||
script, err := buildMongodbCreateScript(dbName, username, password, permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func runMongodbDelete(from, database, dbName string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbDelete(database, dbName)
|
||||
}
|
||||
script, err := buildMongodbDeleteScript(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func bindMongodbUser(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return bindRemoteMongodbUser(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbBindUserScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func updateMongodbPassword(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbPasswordScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func runMongodbAdminScript(database, script string) error {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func runMongodbAdminScriptWithStdout(database, script string) (string, error) {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return "", fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().RunWithStdout(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMongodbCreateScript(dbName, username, password, permission string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
targetDb.createCollection("_init");
|
||||
targetDb.createUser({
|
||||
user: userName,
|
||||
pwd: password,
|
||||
roles: [{ role: permission, db: dbName }]
|
||||
});
|
||||
`, dbNameJSON, usernameJSON, passwordJSON, permissionJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbDeleteScript(dbName string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const dropUsersResult = targetDb.runCommand({ dropAllUsersFromDatabase: 1 });
|
||||
if (!dropUsersResult || dropUsersResult.ok !== 1) {
|
||||
throw new Error("failed to drop users from " + dbName);
|
||||
}
|
||||
const dropDatabaseResult = targetDb.runCommand({ dropDatabase: 1 });
|
||||
if (!dropDatabaseResult || dropDatabaseResult.ok !== 1) {
|
||||
throw new Error("failed to drop database " + dbName);
|
||||
}
|
||||
`, dbNameJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbBindUserScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user " + userName);
|
||||
}
|
||||
const roles = [{ role: "readWrite", db: dbName }];
|
||||
if (Array.isArray(userInfo.users) && userInfo.users.length > 0) {
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user " + userName);
|
||||
}
|
||||
} else {
|
||||
const result = targetDb.runCommand({
|
||||
createUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to create mongodb user " + userName);
|
||||
}
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbPasswordScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user password " + userName);
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
type mongodbSyncItem struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func loadMongodbDatabases(req dto.MongodbLoadDB) ([]mongodbSyncItem, error) {
|
||||
if req.From == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbDatabases(req.Database)
|
||||
}
|
||||
return loadLocalMongodbDatabases(req.Database)
|
||||
}
|
||||
|
||||
func loadLocalMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
script := strings.TrimSpace(`
|
||||
const systemDbs = new Set(["admin", "config", "local"]);
|
||||
const result = db.adminCommand({ listDatabases: 1, nameOnly: false });
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to list mongodb databases");
|
||||
}
|
||||
const items = result.databases
|
||||
.filter(item => !systemDbs.has(item.name))
|
||||
.map(item => ({ name: item.name, username: "" }));
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(items));
|
||||
print("__1panel_json_end__");
|
||||
`)
|
||||
stdout, err := runMongodbAdminScriptWithStdout(database, script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0)
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func runRemoteMongodbCreate(database, dbName, username, password, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.CreateCollection(ctx, "_init"); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func runRemoteMongodbDelete(database, dbName string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.RunCommand(ctx, bson.D{{Key: "dropAllUsersFromDatabase", Value: 1}}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{{Key: "dropDatabase", Value: 1}}).Err()
|
||||
}
|
||||
|
||||
func bindRemoteMongodbUser(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct{} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) > 0 {
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func loadRemoteMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
adminDB := client.Database("admin")
|
||||
var listResult struct {
|
||||
Databases []struct {
|
||||
Name string `bson:"name"`
|
||||
} `bson:"databases"`
|
||||
}
|
||||
if err := adminDB.RunCommand(ctx, bson.D{
|
||||
{Key: "listDatabases", Value: 1},
|
||||
{Key: "nameOnly", Value: false},
|
||||
}).Decode(&listResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemDbs := map[string]struct{}{
|
||||
"admin": {},
|
||||
"config": {},
|
||||
"local": {},
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0, len(listResult.Databases))
|
||||
for _, databaseItem := range listResult.Databases {
|
||||
if _, ok := systemDbs[databaseItem.Name]; ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, mongodbSyncItem{
|
||||
Name: databaseItem.Name,
|
||||
Username: "",
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func loadMongodbPrivilege(from, connectionName, dbName, username string) (string, error) {
|
||||
if from == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
return loadLocalMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
|
||||
func updateMongodbPrivilege(from, connectionName, dbName, username, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
return updateLocalMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
|
||||
func loadLocalMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const result = db.getSiblingDB(dbName).runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
const roles = Array.isArray(result.users) && result.users.length > 0 ? result.users[0].roles || [] : [];
|
||||
const permissions = roles.filter(role => role.db === dbName).map(role => role.role);
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(permissions));
|
||||
print("__1panel_json_end__");
|
||||
`, databaseJSON, usernameJSON))
|
||||
stdout, err := runMongodbAdminScriptWithStdout(connectionName, script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var permissions []string
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &permissions); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateLocalMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
if (!Array.isArray(userInfo.users) || userInfo.users.length === 0) {
|
||||
throw new Error("mongodb user not found: " + userName);
|
||||
}
|
||||
const roles = (userInfo.users[0].roles || []).filter(role => role.db !== dbName);
|
||||
roles.push({ role: permission, db: dbName });
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user privileges");
|
||||
}
|
||||
`, databaseJSON, usernameJSON, permissionJSON))
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func loadRemoteMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var result struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
permissions := make([]string, 0)
|
||||
if len(result.Users) > 0 {
|
||||
for _, role := range result.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
permissions = append(permissions, role.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) == 0 {
|
||||
return fmt.Errorf("mongodb user %s not found in database %s", username, dbName)
|
||||
}
|
||||
roles := make(bson.A, 0, len(userInfo.Users[0].Roles)+1)
|
||||
for _, role := range userInfo.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: role.Role}, {Key: "db", Value: role.DB}})
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}})
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "roles", Value: roles},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func normalizeMongodbPrivilege(permissions []string) string {
|
||||
roleMap := map[string]struct{}{
|
||||
"dbOwner": {},
|
||||
"read": {},
|
||||
"readWrite": {},
|
||||
"userAdmin": {},
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if _, ok := roleMap[permission]; ok {
|
||||
return permission
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isSupportedMongodbPrivilege(permission string) bool {
|
||||
switch permission {
|
||||
case "dbOwner", "read", "readWrite", "userAdmin":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractMongodbJSONOutput(stdout string) ([]byte, error) {
|
||||
const (
|
||||
beginMark = "__1panel_json_begin__"
|
||||
endMark = "__1panel_json_end__"
|
||||
)
|
||||
|
||||
output := strings.TrimSpace(stdout)
|
||||
if output == "" {
|
||||
return nil, fmt.Errorf("empty mongodb command output")
|
||||
}
|
||||
|
||||
beginIndex := strings.Index(output, beginMark)
|
||||
endIndex := strings.LastIndex(output, endMark)
|
||||
if beginIndex >= 0 && endIndex > beginIndex {
|
||||
content := strings.TrimSpace(output[beginIndex+len(beginMark) : endIndex])
|
||||
if content != "" {
|
||||
return []byte(content), nil
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
return []byte(line), nil
|
||||
}
|
||||
return nil, fmt.Errorf("mongodb command output does not contain json result")
|
||||
}
|
||||
@@ -21,6 +21,7 @@ var (
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
mongodbRepo = repo.NewIMongodbRepo()
|
||||
databaseRepo = repo.NewIDatabaseRepo()
|
||||
|
||||
imageRepoRepo = repo.NewIImageRepoRepo()
|
||||
|
||||
+201
-14
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -56,6 +57,7 @@ type IFileService interface {
|
||||
Delete(op request.FileDelete) error
|
||||
BatchDelete(op request.FileBatchDelete) error
|
||||
Compress(c request.FileCompress) error
|
||||
StopCompress(taskID string) error
|
||||
DeCompress(c request.FileDeCompress) error
|
||||
GetContent(op request.FileContentReq) (response.FileInfo, error)
|
||||
GetPreviewContent(op request.FileContentReq) (response.FileInfo, error)
|
||||
@@ -229,8 +231,12 @@ func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *fil
|
||||
return f.buildFileTree(childNode, subInfo.Items, op, level-1)
|
||||
}
|
||||
|
||||
func hasInvalidFileName(fullPath string) bool {
|
||||
return files.IsInvalidChar(filepath.Base(fullPath))
|
||||
}
|
||||
|
||||
func (f *FileService) Create(op request.FileCreate) error {
|
||||
if files.IsInvalidChar(op.Path) {
|
||||
if hasInvalidFileName(op.Path) {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
@@ -282,16 +288,37 @@ func (f *FileService) Delete(op request.FileDelete) error {
|
||||
if recycleBinStatus.Value == "Disable" {
|
||||
op.ForceDelete = true
|
||||
}
|
||||
var historyTargets []string
|
||||
if op.ForceDelete {
|
||||
if op.IsDir {
|
||||
return fo.DeleteDir(op.Path)
|
||||
} else {
|
||||
return fo.DeleteFile(op.Path)
|
||||
var err error
|
||||
historyTargets, err = f.collectPermanentDeleteTargets(op.Path, op.IsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if op.ForceDelete {
|
||||
var err error
|
||||
if op.IsDir {
|
||||
err = fo.DeleteDir(op.Path)
|
||||
} else {
|
||||
err = fo.DeleteFile(op.Path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(op.Path); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", op.Path, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory(historyTargets)
|
||||
return nil
|
||||
}
|
||||
info, _ := fo.Fs.Stat(op.Path)
|
||||
if info == nil || files.IsSymlink(info.Mode()) {
|
||||
return os.Remove(op.Path)
|
||||
if err := os.Remove(op.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory([]string{op.Path})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}); err != nil {
|
||||
@@ -304,15 +331,27 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
|
||||
fo := files.NewFileOp()
|
||||
if op.IsDir {
|
||||
for _, file := range op.Paths {
|
||||
targets, err := f.collectPermanentDeleteTargets(file, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fo.DeleteDir(file); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(file); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", file, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory(targets)
|
||||
}
|
||||
} else {
|
||||
for _, file := range op.Paths {
|
||||
if err := fo.DeleteFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(file); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", file, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory([]string{file})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -320,7 +359,10 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
|
||||
|
||||
func (f *FileService) ChangeMode(op request.FileCreate) error {
|
||||
fo := files.NewFileOp()
|
||||
return fo.ChmodR(op.Path, op.Mode, op.Sub)
|
||||
if err := fo.ChmodR(op.Path, op.Mode, op.Sub); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error {
|
||||
@@ -339,9 +381,44 @@ func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) collectPermanentDeleteTargets(targetPath string, isDir bool) ([]string, error) {
|
||||
if !isDir {
|
||||
return []string{targetPath}, nil
|
||||
}
|
||||
|
||||
var targets []string
|
||||
if err := filepath.WalkDir(targetPath, func(currentPath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d == nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
targets = append(targets, currentPath)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (f *FileService) cleanupPermanentDeleteHistory(targets []string) {
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
for _, target := range targets {
|
||||
if err := historyService.DeleteRelatedHistory(target); err != nil {
|
||||
global.LOG.Warnf("cleanup file history failed for %s: %v", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error {
|
||||
fo := files.NewFileOp()
|
||||
return fo.ChownR(req.Path, req.User, req.Group, req.Sub)
|
||||
if err := fo.ChownR(req.Path, req.User, req.Group, req.Sub); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) Compress(c request.FileCompress) error {
|
||||
@@ -349,7 +426,58 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) {
|
||||
return buserr.New("ErrFileIsExist")
|
||||
}
|
||||
return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret)
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
defer func() {
|
||||
if !success {
|
||||
_ = os.Remove(dstFile)
|
||||
}
|
||||
}()
|
||||
if err := fo.Compress(t.TaskCtx, c.Files, c.Dst, c.Name, compressType, c.Secret, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return fmt.Errorf("compressed file not generated: %s", dstFile)
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
}, nil)
|
||||
_ = taskItem.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func preflightCompressTool(compressType files.CompressType) error {
|
||||
switch compressType {
|
||||
case files.TarGz, files.Rar, files.X7z:
|
||||
_, err := files.NewShellArchiver(compressType)
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) StopCompress(taskID string) error {
|
||||
if cancel, ok := global.TaskCtxMap[taskID]; ok {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
return buserr.New("TaskNotFound")
|
||||
}
|
||||
|
||||
func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
@@ -482,15 +610,36 @@ func (f *FileService) SaveContent(edit request.FileEdit) error {
|
||||
}
|
||||
|
||||
fo := files.NewFileOp()
|
||||
return fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode)
|
||||
oldContent, _ := os.ReadFile(edit.Path)
|
||||
if bytes.Equal(oldContent, []byte(edit.Content)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := historyService.RecordSave(edit.Path, oldContent, info.FileMode); err != nil {
|
||||
global.LOG.Warnf("record file save history failed for %s: %v", edit.Path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) ChangeName(req request.FileRename) error {
|
||||
if files.IsInvalidChar(req.NewName) {
|
||||
if hasInvalidFileName(req.NewName) {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
return fo.Rename(req.OldName, req.NewName)
|
||||
info, _ := files.NewFileInfo(files.FileOption{Path: req.OldName, Expand: false})
|
||||
content, _ := os.ReadFile(req.OldName)
|
||||
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
||||
return err
|
||||
}
|
||||
if info != nil && !info.IsDir {
|
||||
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, info.FileMode, req.OldName, req.NewName); histErr != nil {
|
||||
global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
@@ -512,6 +661,23 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
return buserr.New("ErrMovePathFailed")
|
||||
}
|
||||
}
|
||||
type moveSnapshot struct {
|
||||
path string
|
||||
content []byte
|
||||
mode os.FileMode
|
||||
isDir bool
|
||||
}
|
||||
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
|
||||
for _, oldPath := range m.OldPaths {
|
||||
content, _ := os.ReadFile(oldPath)
|
||||
mode := os.FileMode(0640)
|
||||
isDir := false
|
||||
if info, err := files.NewFileInfo(files.FileOption{Path: oldPath, Expand: false}); err == nil {
|
||||
mode = info.FileMode
|
||||
isDir = info.IsDir
|
||||
}
|
||||
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, isDir: isDir})
|
||||
}
|
||||
var errs []error
|
||||
if m.Type == "cut" {
|
||||
if len(m.CoverPaths) > 0 {
|
||||
@@ -522,7 +688,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
|
||||
if err := fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, snapshot := range snapshots {
|
||||
if !snapshot.isDir {
|
||||
targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths))
|
||||
if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil {
|
||||
global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if m.Type == "copy" {
|
||||
for _, src := range m.OldPaths {
|
||||
@@ -551,6 +728,16 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string {
|
||||
if strings.TrimSpace(dst) == "" {
|
||||
return sourcePath
|
||||
}
|
||||
if strings.TrimSpace(name) != "" && sourceCount == 1 {
|
||||
return filepath.Join(dst, name)
|
||||
}
|
||||
return filepath.Join(dst, filepath.Base(sourcePath))
|
||||
}
|
||||
|
||||
func (f *FileService) FileDownload(d request.FileDownload) (string, error) {
|
||||
filePath := d.Paths[0]
|
||||
if d.Compress {
|
||||
@@ -559,7 +746,7 @@ func (f *FileService) FileDownload(d request.FileDownload) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
if err := fo.Compress(d.Paths, tempPath, d.Name, files.CompressType(d.Type), ""); err != nil {
|
||||
if err := fo.Compress(context.Background(), d.Paths, tempPath, d.Name, files.CompressType(d.Type), "", nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath = filepath.Join(tempPath, d.Name)
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
fileHistorySettingEnable = "FileHistoryStatus"
|
||||
fileHistorySettingMaxPerPath = "FileHistoryMaxPerPath"
|
||||
fileHistorySettingDiskQuotaMB = "FileHistoryDiskQuotaMB"
|
||||
fileHistoryOpSave = "save"
|
||||
fileHistoryOpRestore = "restore"
|
||||
fileHistoryOpRename = "rename"
|
||||
fileHistoryOpMove = "move"
|
||||
fileHistoryRootDirName = "file-history"
|
||||
defaultFileHistoryMaxPerPath = 20
|
||||
defaultFileHistoryDiskQuotaMB = 1024
|
||||
)
|
||||
|
||||
var historyService = NewIFileHistoryService()
|
||||
var fileHistoryPathLocks sync.Map
|
||||
|
||||
type FileHistoryService struct {
|
||||
repo repo.IFileHistoryRepo
|
||||
}
|
||||
|
||||
type IFileHistoryService interface {
|
||||
GetSettingInfo() (*response.FileHistorySettingInfo, error)
|
||||
UpdateSetting(req request.FileHistorySettingUpdate) error
|
||||
RecordSave(path string, content []byte, fileMode os.FileMode) error
|
||||
HasRelatedHistory(path string) (bool, error)
|
||||
RecordOperation(operation string, filePath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string) error
|
||||
Restore(req request.FileHistoryRestoreReq) (response.FileInfo, error)
|
||||
Search(req request.FileHistorySearchReq) (int64, []response.FileHistoryInfo, error)
|
||||
GetContent(req request.FileHistoryContentReq) (response.FileHistoryInfo, error)
|
||||
Delete(req request.FileHistoryDeleteReq) error
|
||||
DeleteRelatedHistory(path string) error
|
||||
}
|
||||
|
||||
func NewIFileHistoryService() IFileHistoryService {
|
||||
return &FileHistoryService{repo: repo.NewIFileHistoryRepo()}
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) GetSettingInfo() (*response.FileHistorySettingInfo, error) {
|
||||
info := &response.FileHistorySettingInfo{
|
||||
Enable: constant.StatusEnable,
|
||||
MaxPerPath: defaultFileHistoryMaxPerPath,
|
||||
DiskQuotaMB: defaultFileHistoryDiskQuotaMB,
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingEnable); err == nil && value != "" {
|
||||
info.Enable = value
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingMaxPerPath); err == nil && value != "" {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && parsed > 0 {
|
||||
info.MaxPerPath = parsed
|
||||
}
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingDiskQuotaMB); err == nil && value != "" {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && parsed > 0 {
|
||||
info.DiskQuotaMB = parsed
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) UpdateSetting(req request.FileHistorySettingUpdate) error {
|
||||
if req.MaxPerPath <= 0 {
|
||||
req.MaxPerPath = defaultFileHistoryMaxPerPath
|
||||
}
|
||||
if req.DiskQuotaMB <= 0 {
|
||||
req.DiskQuotaMB = defaultFileHistoryDiskQuotaMB
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingEnable, req.Enable); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingMaxPerPath, fmt.Sprintf("%d", req.MaxPerPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingDiskQuotaMB, fmt.Sprintf("%d", req.DiskQuotaMB)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) RecordSave(filePath string, content []byte, fileMode os.FileMode) error {
|
||||
return s.RecordOperation(fileHistoryOpSave, filePath, content, fileMode, "", "")
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) HasRelatedHistory(filePath string) (bool, error) {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
absPath = filePath
|
||||
}
|
||||
if _, err = s.getLatestActiveRelatedByPath(absPath); err == nil {
|
||||
return true, nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, err
|
||||
}
|
||||
if _, err = s.getLatestRelatedByPath(absPath); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) RecordOperation(operation string, filePath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string) error {
|
||||
info, err := s.GetSettingInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Enable != constant.StatusEnable {
|
||||
return nil
|
||||
}
|
||||
if fileMode == 0 {
|
||||
fileMode = 0640
|
||||
}
|
||||
if content == nil {
|
||||
content = []byte{}
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
absPath = filePath
|
||||
}
|
||||
absSourcePath := normalizeAbsPath(sourcePath)
|
||||
absTargetPath := normalizeAbsPath(targetPath)
|
||||
resolvePaths := []string{absPath}
|
||||
recordPath := absPath
|
||||
switch operation {
|
||||
case fileHistoryOpRename, fileHistoryOpMove:
|
||||
resolvePaths = []string{absSourcePath, absTargetPath, absPath}
|
||||
if absTargetPath != "" {
|
||||
recordPath = absTargetPath
|
||||
}
|
||||
default:
|
||||
if absSourcePath != "" {
|
||||
resolvePaths = append(resolvePaths, absSourcePath)
|
||||
}
|
||||
if absTargetPath != "" {
|
||||
resolvePaths = append(resolvePaths, absTargetPath)
|
||||
}
|
||||
}
|
||||
|
||||
fileID, latestChainRecord, chainErr := s.resolveFileChain(resolvePaths...)
|
||||
if chainErr != nil && !errors.Is(chainErr, gorm.ErrRecordNotFound) {
|
||||
return chainErr
|
||||
}
|
||||
if fileID == "" {
|
||||
fileID = common.GetUuid()
|
||||
}
|
||||
if operation == fileHistoryOpRename || operation == fileHistoryOpMove {
|
||||
if !s.isEditableHistorySnapshot(fileMode, content) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
previousID := uint(0)
|
||||
if latestVersion, err := s.getLatestVersionByFileID(fileID); err == nil {
|
||||
switch operation {
|
||||
case fileHistoryOpSave:
|
||||
contentHash := sha256HexBytes(content)
|
||||
if latestVersion.ContentSHA == contentHash {
|
||||
return nil
|
||||
}
|
||||
previousID = latestVersion.ID
|
||||
case fileHistoryOpRestore:
|
||||
previousID = latestVersion.ID
|
||||
default:
|
||||
if latestChainRecord.ID != 0 {
|
||||
previousID = latestChainRecord.ID
|
||||
}
|
||||
}
|
||||
} else if latestChainRecord.ID != 0 {
|
||||
previousID = latestChainRecord.ID
|
||||
}
|
||||
|
||||
recordContent := content
|
||||
recordDeleted := false
|
||||
|
||||
recordID, storagePath, err := s.recordSnapshot(fileID, recordDeleted, operation, recordPath, recordContent, fileMode, absSourcePath, absTargetPath, previousID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.enforceRetention(fileID, recordID); err != nil {
|
||||
_ = s.repo.DeleteByIDs([]uint{recordID})
|
||||
_ = os.Remove(s.absStoragePath(storagePath))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) recordSnapshot(fileID string, deleted bool, operation string, absPath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string, previousID uint) (uint, string, error) {
|
||||
if fileMode == 0 {
|
||||
fileMode = 0640
|
||||
}
|
||||
now := time.Now()
|
||||
pathHash := sha256Hex(absPath)
|
||||
contentHash := sha256HexBytes(content)
|
||||
fileName := filepath.Base(absPath)
|
||||
extension := filepath.Ext(absPath)
|
||||
storagePath := s.buildStoragePath(absPath, now)
|
||||
|
||||
if err := os.MkdirAll(path.Dir(s.absStoragePath(storagePath)), os.ModePerm); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if err := os.WriteFile(s.absStoragePath(storagePath), content, 0640); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
record := &model.FileHistory{
|
||||
FileID: fileID,
|
||||
Path: absPath,
|
||||
PathHash: pathHash,
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
FileName: fileName,
|
||||
Extension: extension,
|
||||
FileMode: fmt.Sprintf("%04o", fileMode.Perm()),
|
||||
Operation: operation,
|
||||
Deleted: deleted,
|
||||
ContentSize: int64(len(content)),
|
||||
ContentSHA: contentHash,
|
||||
StoragePath: storagePath,
|
||||
PreviousID: previousID,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(record); err != nil {
|
||||
_ = os.Remove(s.absStoragePath(storagePath))
|
||||
return 0, "", err
|
||||
}
|
||||
return record.ID, storagePath, nil
|
||||
}
|
||||
|
||||
func normalizeAbsPath(filePath string) string {
|
||||
filePath = strings.TrimSpace(filePath)
|
||||
if filePath == "" {
|
||||
return ""
|
||||
}
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return filePath
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Search(req request.FileHistorySearchReq) (int64, []response.FileHistoryInfo, error) {
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
size := req.PageSize
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
offset := (page - 1) * size
|
||||
opts := []repo.DBOption{}
|
||||
opts = append(opts, s.repo.WithNotOperation("init"))
|
||||
switch strings.ToLower(strings.TrimSpace(req.Scope)) {
|
||||
case "current":
|
||||
if strings.TrimSpace(req.Path) == "" {
|
||||
return 0, nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
absPath, err := filepath.Abs(req.Path)
|
||||
if err != nil {
|
||||
absPath = req.Path
|
||||
}
|
||||
chain, err := s.getLatestActiveRelatedByPath(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
chain, err = s.getLatestRelatedByPath(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, []response.FileHistoryInfo{}, nil
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
} else {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
opts = append(opts, s.repo.WithByFileID(chain.FileID))
|
||||
case "all":
|
||||
if strings.TrimSpace(req.Path) != "" {
|
||||
opts = append(opts, s.repo.WithByRelatedPath(normalizeAbsPath(req.Path)))
|
||||
}
|
||||
default:
|
||||
return 0, nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
if strings.TrimSpace(req.Operation) != "" {
|
||||
opts = append(opts, s.repo.WithByOperation(strings.TrimSpace(req.Operation)))
|
||||
}
|
||||
|
||||
total, items, err := s.repo.Page(size, offset, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
res := make([]response.FileHistoryInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
currentPath := item.Path
|
||||
if resolved, err := s.getCurrentPathByFileID(item.FileID); err == nil && strings.TrimSpace(resolved) != "" {
|
||||
currentPath = resolved
|
||||
}
|
||||
res = append(res, response.FileHistoryInfo{
|
||||
ID: item.ID,
|
||||
FileID: item.FileID,
|
||||
Path: item.Path,
|
||||
CurrentPath: currentPath,
|
||||
PreviousID: item.PreviousID,
|
||||
SourcePath: item.SourcePath,
|
||||
TargetPath: item.TargetPath,
|
||||
FileName: item.FileName,
|
||||
Extension: item.Extension,
|
||||
FileMode: item.FileMode,
|
||||
Operation: item.Operation,
|
||||
Deleted: item.Deleted,
|
||||
ContentSize: item.ContentSize,
|
||||
ContentSHA: item.ContentSHA,
|
||||
StoragePath: item.StoragePath,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return total, res, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) GetContent(req request.FileHistoryContentReq) (response.FileHistoryInfo, error) {
|
||||
record, err := s.repo.Get(s.repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return response.FileHistoryInfo{}, err
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err != nil {
|
||||
return response.FileHistoryInfo{}, err
|
||||
}
|
||||
currentContent := s.getCurrentContent(record)
|
||||
return response.FileHistoryInfo{
|
||||
ID: record.ID,
|
||||
FileID: record.FileID,
|
||||
Path: record.Path,
|
||||
CurrentPath: func() string {
|
||||
if currentPath, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(currentPath) != "" {
|
||||
return currentPath
|
||||
}
|
||||
return record.Path
|
||||
}(),
|
||||
PreviousID: record.PreviousID,
|
||||
SourcePath: record.SourcePath,
|
||||
TargetPath: record.TargetPath,
|
||||
FileName: record.FileName,
|
||||
Extension: record.Extension,
|
||||
FileMode: record.FileMode,
|
||||
Operation: record.Operation,
|
||||
Deleted: record.Deleted,
|
||||
ContentSize: record.ContentSize,
|
||||
ContentSHA: record.ContentSHA,
|
||||
StoragePath: record.StoragePath,
|
||||
Content: string(content),
|
||||
CurrentContent: currentContent,
|
||||
CreatedAt: record.CreatedAt,
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Restore(req request.FileHistoryRestoreReq) (response.FileInfo, error) {
|
||||
record, err := s.repo.Get(s.repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
if !s.isVersionOperation(record.Operation) {
|
||||
return response.FileInfo{}, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
|
||||
currentPath := record.Path
|
||||
if chainPath, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(chainPath) != "" {
|
||||
currentPath = chainPath
|
||||
}
|
||||
|
||||
lock := s.getPathLock(currentPath)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
var rollbackContent []byte
|
||||
var rollbackMode os.FileMode
|
||||
var existedBefore bool
|
||||
currentInfo, currentErr := files.NewFileInfo(files.FileOption{
|
||||
Path: currentPath,
|
||||
Expand: false,
|
||||
})
|
||||
if currentErr == nil {
|
||||
existedBefore = true
|
||||
rollbackContent, err = os.ReadFile(currentPath)
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
rollbackMode = currentInfo.FileMode
|
||||
} else {
|
||||
rollbackMode = parseFileMode(record.FileMode)
|
||||
if rollbackMode == 0 {
|
||||
rollbackMode = 0640
|
||||
}
|
||||
}
|
||||
|
||||
targetMode := rollbackMode
|
||||
if targetMode == 0 {
|
||||
targetMode = parseFileMode(record.FileMode)
|
||||
}
|
||||
if targetMode == 0 {
|
||||
targetMode = 0640
|
||||
}
|
||||
|
||||
fo := files.NewFileOp()
|
||||
if err := fo.WriteFile(currentPath, strings.NewReader(string(content)), targetMode); err != nil {
|
||||
_ = s.rollbackRestore(currentPath, rollbackContent, rollbackMode, existedBefore)
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
if err := historyService.RecordOperation(fileHistoryOpRestore, currentPath, rollbackContent, rollbackMode, "", ""); err != nil {
|
||||
global.LOG.Warnf("record file restore history failed for %s: %v", currentPath, err)
|
||||
}
|
||||
|
||||
info, err := files.NewFileInfo(files.FileOption{
|
||||
Path: currentPath,
|
||||
Expand: true,
|
||||
})
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
return response.FileInfo{FileInfo: *info}, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Delete(req request.FileHistoryDeleteReq) error {
|
||||
if len(req.IDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
record, err := s.repo.Get(s.repo.WithByID(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.repo.DeleteByIDs([]uint{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(s.absStoragePath(record.StoragePath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) DeleteRelatedHistory(filePath string) error {
|
||||
absPath := normalizeAbsPath(filePath)
|
||||
if strings.TrimSpace(absPath) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileID, latestRecord, err := s.resolveFileChain(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(fileID) == "" {
|
||||
fileID = latestRecord.FileID
|
||||
}
|
||||
if strings.TrimSpace(fileID) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var records []model.FileHistory
|
||||
if err := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID).Find(&records).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(records))
|
||||
for _, record := range records {
|
||||
ids = append(ids, record.ID)
|
||||
_ = os.Remove(s.absStoragePath(record.StoragePath))
|
||||
}
|
||||
return s.repo.DeleteByIDs(ids)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) rollbackRestore(filePath string, rollbackContent []byte, rollbackMode os.FileMode, existedBefore bool) error {
|
||||
fo := files.NewFileOp()
|
||||
if !existedBefore {
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
if rollbackMode == 0 {
|
||||
rollbackMode = 0640
|
||||
}
|
||||
return fo.WriteFile(filePath, strings.NewReader(string(rollbackContent)), rollbackMode)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) enforceRetention(fileID string, keepID uint) error {
|
||||
info, err := s.GetSettingInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Enable != constant.StatusEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxPerPath := info.MaxPerPath
|
||||
if maxPerPath <= 0 {
|
||||
maxPerPath = 20
|
||||
}
|
||||
quotaBytes := int64(info.DiskQuotaMB) * 1024 * 1024
|
||||
|
||||
if maxPerPath > 0 {
|
||||
total, records, err := s.listFileOldest(fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int(total) > maxPerPath {
|
||||
removeCount := int(total) - maxPerPath
|
||||
for i := 0; i < removeCount && i < len(records); i++ {
|
||||
if err := s.repo.DeleteByIDs([]uint{records[i].ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(s.absStoragePath(records[i].StoragePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if quotaBytes <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalSize, err := s.totalSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if totalSize <= quotaBytes {
|
||||
return nil
|
||||
}
|
||||
return buserr.New("ErrHistoryQuotaExceeded")
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getPathLock(absPath string) *sync.Mutex {
|
||||
actual, _ := fileHistoryPathLocks.LoadOrStore(absPath, &sync.Mutex{})
|
||||
return actual.(*sync.Mutex)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) listFileOldest(fileID string) (int64, []model.FileHistory, error) {
|
||||
var total int64
|
||||
var items []model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID)
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("created_at asc").Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestRelatedByPath(absPath string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("path = ? OR source_path = ? OR target_path = ?", absPath, absPath, absPath).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestActiveRelatedByPath(absPath string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("deleted = ? AND (path = ? OR source_path = ? OR target_path = ?)", false, absPath, absPath, absPath).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestVersionByFileID(fileID string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ? AND operation IN ?", fileID, []string{fileHistoryOpSave, fileHistoryOpRestore}).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getCurrentPathByFileID(fileID string) (string, error) {
|
||||
record, err := s.getLatestRelatedByFileID(fileID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return record.Path, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getCurrentContent(record model.FileHistory) string {
|
||||
currentPath := ""
|
||||
if resolved, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(resolved) != "" {
|
||||
currentPath = resolved
|
||||
}
|
||||
candidates := []string{}
|
||||
if strings.TrimSpace(currentPath) != "" {
|
||||
candidates = append(candidates, currentPath)
|
||||
}
|
||||
if strings.TrimSpace(record.Path) != "" && record.Path != currentPath {
|
||||
candidates = append(candidates, record.Path)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
content, err := os.ReadFile(candidate)
|
||||
if err == nil {
|
||||
return string(content)
|
||||
}
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err == nil {
|
||||
return string(content)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) isEditableHistorySnapshot(fileMode os.FileMode, content []byte) bool {
|
||||
if fileMode.IsDir() || files.IsBlockDevice(fileMode) {
|
||||
return false
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return true
|
||||
}
|
||||
return !files.DetectBinary(content)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestRelatedByFileID(fileID string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) resolveFileChain(paths ...string) (string, model.FileHistory, error) {
|
||||
for _, itemPath := range paths {
|
||||
itemPath = strings.TrimSpace(itemPath)
|
||||
if itemPath == "" {
|
||||
continue
|
||||
}
|
||||
record, err := s.getLatestActiveRelatedByPath(itemPath)
|
||||
if err == nil {
|
||||
return record.FileID, record, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", model.FileHistory{}, err
|
||||
}
|
||||
}
|
||||
return "", model.FileHistory{}, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) isVersionOperation(operation string) bool {
|
||||
return operation == fileHistoryOpSave || operation == fileHistoryOpRestore
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) totalSize() (int64, error) {
|
||||
var total int64
|
||||
err := global.DB.Model(&model.FileHistory{}).Select("coalesce(sum(content_size),0)").Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) listOldest() ([]model.FileHistory, error) {
|
||||
var items []model.FileHistory
|
||||
err := global.DB.Model(&model.FileHistory{}).Order("created_at asc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) buildStoragePath(absPath string, now time.Time) string {
|
||||
fileName := filepath.Base(absPath)
|
||||
extension := filepath.Ext(fileName)
|
||||
baseName := strings.TrimSuffix(fileName, extension)
|
||||
if baseName == "" {
|
||||
baseName = fileName
|
||||
extension = ""
|
||||
}
|
||||
baseName = sanitizeHistoryFileName(baseName)
|
||||
if baseName == "" {
|
||||
baseName = "file"
|
||||
}
|
||||
dayDir := now.Format("2006-01-02")
|
||||
shortCode := strings.ReplaceAll(common.GetUuid(), "-", "")
|
||||
if len(shortCode) > 8 {
|
||||
shortCode = shortCode[:8]
|
||||
}
|
||||
return path.Join(fileHistoryRootDirName, dayDir, fmt.Sprintf("%s__%s%s", baseName, shortCode, extension))
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) absStoragePath(rel string) string {
|
||||
return path.Join(global.Dir.LocalBackupDir, rel)
|
||||
}
|
||||
|
||||
func sanitizeHistoryFileName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "_",
|
||||
"\\", "_",
|
||||
":", "_",
|
||||
"*", "_",
|
||||
"?", "_",
|
||||
"\"", "_",
|
||||
"<", "_",
|
||||
">", "_",
|
||||
"|", "_",
|
||||
)
|
||||
return replacer.Replace(name)
|
||||
}
|
||||
|
||||
func parseFileMode(mode string) os.FileMode {
|
||||
if strings.TrimSpace(mode) == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, err := strconv.ParseUint(strings.TrimSpace(mode), 8, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return os.FileMode(parsed)
|
||||
}
|
||||
|
||||
func sha256Hex(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func sha256HexBytes(value []byte) string {
|
||||
sum := sha256.Sum256(value)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
type mongodbConnectionInfo struct {
|
||||
Address string
|
||||
Port uint
|
||||
InitialDB string
|
||||
Username string
|
||||
Password string
|
||||
Timeout uint
|
||||
SSL bool
|
||||
RootCert string
|
||||
ClientKey string
|
||||
ClientCert string
|
||||
SkipVerify bool
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromCreate(req dto.DatabaseCreate) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
InitialDB: req.InitialDB,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromModel(db model.Database) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: db.Address,
|
||||
Port: db.Port,
|
||||
InitialDB: db.InitialDB,
|
||||
Username: db.Username,
|
||||
Password: db.Password,
|
||||
Timeout: db.Timeout,
|
||||
SSL: db.SSL,
|
||||
RootCert: db.RootCert,
|
||||
ClientKey: db.ClientKey,
|
||||
ClientCert: db.ClientCert,
|
||||
SkipVerify: db.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRemoteMongodbConnection(database string) (mongodbConnectionInfo, error) {
|
||||
db, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return mongodbConnectionInfo{}, err
|
||||
}
|
||||
return mongodbConnectionInfoFromModel(db), nil
|
||||
}
|
||||
|
||||
func newRemoteMongodbClient(info mongodbConnectionInfo) (*mongo.Client, context.Context, context.CancelFunc, error) {
|
||||
timeout := time.Duration(info.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
clientOptions := options.Client().ApplyURI(buildRemoteMongodbURI(info)).
|
||||
SetServerSelectionTimeout(timeout).
|
||||
SetConnectTimeout(timeout)
|
||||
if info.SSL {
|
||||
tlsConfig, err := buildRemoteMongodbTLSConfig(info)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
clientOptions.SetTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if err := client.Ping(ctx, readpref.Primary()); err != nil {
|
||||
_ = client.Disconnect(context.Background())
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return client, ctx, cancel, nil
|
||||
}
|
||||
|
||||
func buildRemoteMongodbURI(info mongodbConnectionInfo) string {
|
||||
uri := url.URL{
|
||||
Scheme: "mongodb",
|
||||
Host: net.JoinHostPort(info.Address, strconv.Itoa(int(info.Port))),
|
||||
Path: "/",
|
||||
}
|
||||
uri.User = url.UserPassword(info.Username, info.Password)
|
||||
query := url.Values{}
|
||||
authSource := info.InitialDB
|
||||
if authSource == "" {
|
||||
authSource = "admin"
|
||||
}
|
||||
query.Set("authSource", authSource)
|
||||
query.Set("directConnection", "true")
|
||||
uri.RawQuery = query.Encode()
|
||||
return uri.String()
|
||||
}
|
||||
|
||||
func buildRemoteMongodbTLSConfig(info mongodbConnectionInfo) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: info.SkipVerify,
|
||||
}
|
||||
|
||||
if info.RootCert != "" {
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM([]byte(info.RootCert)) {
|
||||
return nil, fmt.Errorf("load mongodb ca cert failed")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
if info.ClientCert != "" && info.ClientKey != "" {
|
||||
cert, err := tls.X509KeyPair([]byte(info.ClientCert), []byte(info.ClientKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
@@ -52,7 +52,12 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procCache := make(map[int32]ListeningProcess, 64)
|
||||
// One cache entry per (PID, socket type) so TCP and UDP sockets are not merged under one Protocol.
|
||||
type procKey struct {
|
||||
pid int32
|
||||
protocol uint32
|
||||
}
|
||||
procCache := make(map[procKey]ListeningProcess, 64)
|
||||
|
||||
for _, conn := range conn {
|
||||
if conn.Pid == 0 {
|
||||
@@ -60,7 +65,8 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
}
|
||||
|
||||
if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
|
||||
if _, exists := procCache[conn.Pid]; !exists {
|
||||
key := procKey{pid: conn.Pid, protocol: conn.Type}
|
||||
if _, exists := procCache[key]; !exists {
|
||||
proc, err := process.NewProcess(conn.Pid)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -72,11 +78,11 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
procData.Port = make(map[uint32]struct{})
|
||||
procData.Port[conn.Laddr.Port] = struct{}{}
|
||||
procData.Protocol = conn.Type
|
||||
procCache[conn.Pid] = procData
|
||||
procCache[key] = procData
|
||||
} else {
|
||||
p := procCache[conn.Pid]
|
||||
p := procCache[key]
|
||||
p.Port[conn.Laddr.Port] = struct{}{}
|
||||
procCache[conn.Pid] = p
|
||||
procCache[key] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -15,9 +17,20 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
recycleBinClashDir = ".1panel_clash"
|
||||
recycleBinFilesSubdir = "files"
|
||||
recycleBinInfoSubdir = "info"
|
||||
trashInfoSuffix = ".trashinfo"
|
||||
trashInfoSection = "Trash Info"
|
||||
trashInfoTimeLayout = "2006-01-02T15:04:05"
|
||||
)
|
||||
|
||||
type RecycleBinService struct {
|
||||
@@ -35,39 +48,32 @@ func NewIRecycleBinService() IRecycleBinService {
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) {
|
||||
var (
|
||||
result []response.RecycleBinDTO
|
||||
)
|
||||
var result []response.RecycleBinDTO
|
||||
partitions, err := disk.Partitions(false)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
op := files.NewFileOp()
|
||||
for _, p := range partitions {
|
||||
dir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
if !op.Stat(dir) {
|
||||
clashRoot := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if !op.Stat(clashRoot) {
|
||||
continue
|
||||
}
|
||||
clashFiles, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, file := range clashFiles {
|
||||
if strings.HasPrefix(file.Name(), "_1p_") {
|
||||
recycleDTO, err := getRecycleBinDTOFromName(file.Name())
|
||||
recycleDTO.IsDir = file.IsDir()
|
||||
recycleDTO.From = dir
|
||||
if err == nil {
|
||||
result = append(result, *recycleDTO)
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, collectTrashEntries(clashRoot)...)
|
||||
result = append(result, collectLegacyEntries(clashRoot)...)
|
||||
}
|
||||
startIndex := (search.Page - 1) * search.PageSize
|
||||
if search.PageSize <= 0 {
|
||||
return int64(len(result)), []response.RecycleBinDTO{}, nil
|
||||
}
|
||||
page := search.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
startIndex := (page - 1) * search.PageSize
|
||||
endIndex := startIndex + search.PageSize
|
||||
|
||||
if startIndex > len(result) {
|
||||
return int64(len(result)), result, nil
|
||||
if startIndex >= len(result) {
|
||||
return int64(len(result)), []response.RecycleBinDTO{}, nil
|
||||
}
|
||||
if endIndex > len(result) {
|
||||
endIndex = len(result)
|
||||
@@ -83,34 +89,55 @@ func (r RecycleBinService) Create(create request.RecycleBinCreate) error {
|
||||
if !op.Stat(create.SourcePath) {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
clashDir, err := getClashDir(create.SourcePath)
|
||||
clashRoot, err := getClashDir(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths := strings.Split(create.SourcePath, "/")
|
||||
rNamePre := strings.Join(paths, "_1p_")
|
||||
filesDir, infoDir, err := ensureTrashDirs(clashRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteTime := time.Now()
|
||||
openFile, err := op.OpenFile(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileInfo, err := openFile.Stat()
|
||||
_ = openFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := 0
|
||||
size := int64(0)
|
||||
if fileInfo.IsDir() {
|
||||
sizeF, err := op.GetDirSize(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size = int(sizeF)
|
||||
size = int64(sizeF)
|
||||
} else {
|
||||
size = int(fileInfo.Size())
|
||||
size = fileInfo.Size()
|
||||
}
|
||||
|
||||
rName := fmt.Sprintf("_1p_%s%s_p_%d_%d", "file", rNamePre, size, deleteTime.Unix())
|
||||
return op.Mv(create.SourcePath, path.Join(clashDir, rName))
|
||||
rName := allocateTrashEntryName(filesDir, infoDir, create.SourcePath)
|
||||
info := trashInfo{
|
||||
Path: create.SourcePath,
|
||||
DeletionDate: deleteTime,
|
||||
Size: size,
|
||||
IsDir: fileInfo.IsDir(),
|
||||
}
|
||||
infoPath := path.Join(infoDir, rName+trashInfoSuffix)
|
||||
if err := writeTrashInfo(infoPath, info); err != nil {
|
||||
return err
|
||||
}
|
||||
targetPath := path.Join(filesDir, rName)
|
||||
if err := op.Mv(create.SourcePath, targetPath); err != nil {
|
||||
if rmErr := os.Remove(infoPath); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
global.LOG.Warnf("rollback trashinfo failed for %s: %v", infoPath, rmErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
@@ -119,10 +146,13 @@ func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
if !op.Stat(filePath) {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
recycleBinDTO, err := getRecycleBinDTOFromName(reduce.RName)
|
||||
recycleBinDTO, err := loadRecycleBinDTO(reduce.From, reduce.RName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if recycleBinDTO.SourcePath == "" {
|
||||
return buserr.New("ErrSourcePathNotFound")
|
||||
}
|
||||
if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) {
|
||||
return buserr.New("ErrSourcePathNotFound")
|
||||
}
|
||||
@@ -131,7 +161,13 @@ func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return op.Mv(filePath, recycleBinDTO.SourcePath)
|
||||
if err := op.Mv(filePath, recycleBinDTO.SourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(filePath); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", filePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Clear() error {
|
||||
@@ -141,17 +177,22 @@ func (r RecycleBinService) Clear() error {
|
||||
}
|
||||
op := files.NewFileOp()
|
||||
for _, p := range partitions {
|
||||
dir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
dir := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if !op.Stat(dir) {
|
||||
continue
|
||||
}
|
||||
newDir := path.Join(p.Mountpoint, "1panel_clash")
|
||||
newDir := path.Join(p.Mountpoint, recycleBinClashDir+"-"+strings.ReplaceAll(common.GetUuid(), "-", ""))
|
||||
if err := op.Mv(dir, newDir); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = op.DeleteDir(newDir)
|
||||
}()
|
||||
go func(target string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
global.LOG.Warnf("clear recycle bin panic on %s: %v", target, r)
|
||||
}
|
||||
}()
|
||||
_ = op.DeleteDir(target)
|
||||
}(newDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -166,8 +207,8 @@ func getClashDir(realPath string) (string, error) {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(realPath, p.Mountpoint) {
|
||||
clashDir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil {
|
||||
clashDir := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if err = createClashDir(clashDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clashDir, nil
|
||||
@@ -186,6 +227,264 @@ func createClashDir(clashDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTrashDirs(clashRoot string) (string, string, error) {
|
||||
filesDir := path.Join(clashRoot, recycleBinFilesSubdir)
|
||||
infoDir := path.Join(clashRoot, recycleBinInfoSubdir)
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(filesDir) {
|
||||
if err := op.CreateDir(filesDir, constant.DirPerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
if !op.Stat(infoDir) {
|
||||
if err := op.CreateDir(infoDir, constant.DirPerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return filesDir, infoDir, nil
|
||||
}
|
||||
|
||||
func trashFilesDir(clashRoot string) string {
|
||||
return path.Join(clashRoot, recycleBinFilesSubdir)
|
||||
}
|
||||
|
||||
func trashInfoDir(clashRoot string) string {
|
||||
return path.Join(clashRoot, recycleBinInfoSubdir)
|
||||
}
|
||||
|
||||
func trashInfoPath(infoDir, entryName string) string {
|
||||
return path.Join(infoDir, entryName+trashInfoSuffix)
|
||||
}
|
||||
|
||||
type trashInfo struct {
|
||||
Path string
|
||||
DeletionDate time.Time
|
||||
Size int64
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func writeTrashInfo(dstPath string, info trashInfo) error {
|
||||
cfg := ini.Empty()
|
||||
section, err := cfg.NewSection(trashInfoSection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("Path", url.PathEscape(info.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("DeletionDate", info.DeletionDate.Format(trashInfoTimeLayout)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("Size", strconv.FormatInt(info.Size, 10)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("IsDir", strconv.FormatBool(info.IsDir)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := cfg.WriteTo(&buf); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := dstPath + ".tmp"
|
||||
if err := os.WriteFile(tmp, buf.Bytes(), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, dstPath)
|
||||
}
|
||||
|
||||
func readTrashInfo(srcPath string) (*trashInfo, error) {
|
||||
cfg, err := ini.Load(srcPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section, err := cfg.GetSection(trashInfoSection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawPath := section.Key("Path").Value()
|
||||
decodedPath, err := url.PathUnescape(rawPath)
|
||||
if err != nil {
|
||||
decodedPath = rawPath
|
||||
}
|
||||
info := &trashInfo{
|
||||
Path: decodedPath,
|
||||
IsDir: section.Key("IsDir").MustBool(false),
|
||||
}
|
||||
if deletionStr := section.Key("DeletionDate").Value(); deletionStr != "" {
|
||||
if t, parseErr := time.ParseInLocation(trashInfoTimeLayout, deletionStr, time.Local); parseErr == nil {
|
||||
info.DeletionDate = t
|
||||
}
|
||||
}
|
||||
if sizeStr := section.Key("Size").Value(); sizeStr != "" {
|
||||
if sz, sizeErr := strconv.ParseInt(sizeStr, 10, 64); sizeErr == nil {
|
||||
info.Size = sz
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func collectTrashEntries(clashRoot string) []response.RecycleBinDTO {
|
||||
filesDir := trashFilesDir(clashRoot)
|
||||
infoDir := trashInfoDir(clashRoot)
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(filesDir) {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(filesDir)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("read recycle files dir %s failed: %v", filesDir, err)
|
||||
return nil
|
||||
}
|
||||
var result []response.RecycleBinDTO
|
||||
for _, entry := range entries {
|
||||
dto, err := buildTrashDTO(filesDir, infoDir, entry)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("build recycle dto for %s failed: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
result = append(result, *dto)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func collectLegacyEntries(clashRoot string) []response.RecycleBinDTO {
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(clashRoot) {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(clashRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var result []response.RecycleBinDTO
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == recycleBinFilesSubdir || name == recycleBinInfoSubdir {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(name, "_1p_") {
|
||||
continue
|
||||
}
|
||||
dto, err := getRecycleBinDTOFromName(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dto.IsDir = entry.IsDir()
|
||||
dto.From = clashRoot
|
||||
result = append(result, *dto)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildTrashDTO(filesDir, infoDir string, entry os.DirEntry) (*response.RecycleBinDTO, error) {
|
||||
entryName := entry.Name()
|
||||
infoPath := trashInfoPath(infoDir, entryName)
|
||||
if info, err := readTrashInfo(infoPath); err == nil {
|
||||
return trashInfoToDTO(entryName, filesDir, info), nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
global.LOG.Warnf("read trashinfo %s failed: %v", infoPath, err)
|
||||
}
|
||||
|
||||
entryPath := path.Join(filesDir, entryName)
|
||||
fi, err := os.Stat(entryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size := fi.Size()
|
||||
if fi.IsDir() {
|
||||
if sz, sizeErr := files.NewFileOp().GetDirSize(entryPath); sizeErr == nil {
|
||||
size = int64(sz)
|
||||
}
|
||||
}
|
||||
return &response.RecycleBinDTO{
|
||||
Name: entryName,
|
||||
Size: clampToInt(size),
|
||||
Type: "file",
|
||||
DeleteTime: fi.ModTime(),
|
||||
RName: entryName,
|
||||
SourcePath: "",
|
||||
IsDir: fi.IsDir(),
|
||||
From: filesDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func trashInfoToDTO(entryName, filesDir string, info *trashInfo) *response.RecycleBinDTO {
|
||||
return &response.RecycleBinDTO{
|
||||
Name: path.Base(info.Path),
|
||||
Size: clampToInt(info.Size),
|
||||
Type: "file",
|
||||
DeleteTime: info.DeletionDate,
|
||||
RName: entryName,
|
||||
SourcePath: info.Path,
|
||||
IsDir: info.IsDir,
|
||||
From: filesDir,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRecycleBinDTO(from, name string) (*response.RecycleBinDTO, error) {
|
||||
if path.Base(from) == recycleBinFilesSubdir {
|
||||
infoDir := trashInfoDir(path.Dir(from))
|
||||
info, err := readTrashInfo(trashInfoPath(infoDir, name))
|
||||
if err == nil {
|
||||
return trashInfoToDTO(name, from, info), nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
global.LOG.Warnf("read trashinfo for %s failed: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
dto, err := getRecycleBinDTOFromName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto.From = from
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
func cleanupTrashInfoByEntryPath(filePath string) error {
|
||||
cleaned := path.Clean(filePath)
|
||||
parent := path.Dir(cleaned)
|
||||
if path.Base(parent) != recycleBinFilesSubdir {
|
||||
return nil
|
||||
}
|
||||
clashRoot := path.Dir(parent)
|
||||
if path.Base(clashRoot) != recycleBinClashDir {
|
||||
return nil
|
||||
}
|
||||
infoPath := trashInfoPath(trashInfoDir(clashRoot), path.Base(cleaned))
|
||||
if err := os.Remove(infoPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allocateTrashEntryName(filesDir, infoDir, sourcePath string) string {
|
||||
ext := path.Ext(path.Base(sourcePath))
|
||||
op := files.NewFileOp()
|
||||
for i := 0; i < 5; i++ {
|
||||
name := strings.ReplaceAll(common.GetUuid(), "-", "") + ext
|
||||
if op.Stat(path.Join(filesDir, name)) {
|
||||
continue
|
||||
}
|
||||
if op.Stat(trashInfoPath(infoDir, name)) {
|
||||
continue
|
||||
}
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%s-%d%s", strings.ReplaceAll(common.GetUuid(), "-", ""), time.Now().UnixNano(), ext)
|
||||
}
|
||||
|
||||
func clampToInt(v int64) int {
|
||||
if v > math.MaxInt {
|
||||
return math.MaxInt
|
||||
}
|
||||
if v < math.MinInt {
|
||||
return math.MinInt
|
||||
}
|
||||
return int(v)
|
||||
}
|
||||
|
||||
func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) {
|
||||
matches := re.GetRegex(re.RecycleBinFilePattern).FindStringSubmatch(filename)
|
||||
if len(matches) != 4 {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
@@ -24,9 +26,11 @@ type ISettingService interface {
|
||||
GetSettingInfo() (*dto.SettingInfo, error)
|
||||
GetTerminalAIInfo() (*dto.TerminalAIInfo, error)
|
||||
GetFileManageAIInfo() (*dto.FileManageAIInfo, error)
|
||||
GetFileHistorySettingInfo() (*response.FileHistorySettingInfo, error)
|
||||
Update(key, value string) error
|
||||
UpdateTerminalAI(req dto.TerminalAIInfo) error
|
||||
UpdateFileManageAI(req dto.FileManageAIInfo) error
|
||||
UpdateFileHistorySetting(req request.FileHistorySettingUpdate) error
|
||||
|
||||
TestConnByInfo(req dto.SSHConnData) bool
|
||||
SaveConnInfo(req dto.SSHConnData) error
|
||||
@@ -103,6 +107,10 @@ func (u *SettingService) GetFileManageAIInfo() (*dto.FileManageAIInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (u *SettingService) GetFileHistorySettingInfo() (*response.FileHistorySettingInfo, error) {
|
||||
return historyService.GetSettingInfo()
|
||||
}
|
||||
|
||||
func (u *SettingService) Update(key, value string) error {
|
||||
return settingRepo.UpdateOrCreate(key, value)
|
||||
}
|
||||
@@ -169,6 +177,10 @@ func (u *SettingService) UpdateFileManageAI(req dto.FileManageAIInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SettingService) UpdateFileHistorySetting(req request.FileHistorySettingUpdate) error {
|
||||
return historyService.UpdateSetting(req)
|
||||
}
|
||||
|
||||
func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
|
||||
if req.AuthMode == "password" && len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
|
||||
@@ -21,6 +21,9 @@ func (u *TaskLogService) Page(req dto.SearchTaskLogReq) (int64, []dto.TaskDTO, e
|
||||
opts := []repo.DBOption{
|
||||
repo.WithOrderDesc("created_at"),
|
||||
}
|
||||
if req.TaskID != "" {
|
||||
opts = append(opts, taskRepo.WithByID(req.TaskID))
|
||||
}
|
||||
if req.Status != "" {
|
||||
opts = append(opts, repo.WithByStatus(req.Status))
|
||||
}
|
||||
|
||||
@@ -379,6 +379,17 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
req request.AppInstallCreate
|
||||
install *model.AppInstall
|
||||
)
|
||||
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(create.AppInstall.AppDetailId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isAgentAppKey(app.Key) {
|
||||
return fmt.Errorf("%s does not support website deployment", app.Key)
|
||||
}
|
||||
req.Name = create.AppInstall.Name
|
||||
req.AppDetailId = create.AppInstall.AppDetailId
|
||||
req.Params = create.AppInstall.Params
|
||||
|
||||
@@ -443,7 +443,7 @@ func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
fileName := ca.Name + ".zip"
|
||||
if err = fileOp.Compress([]string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, ""); err != nil {
|
||||
if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, "", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(path.Join(dir, fileName))
|
||||
|
||||
@@ -758,7 +758,7 @@ func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
fileName := websiteSSL.PrimaryDomain + ".zip"
|
||||
if err = fileOp.Compress([]string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, ""); err != nil {
|
||||
if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, "", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(path.Join(dir, fileName))
|
||||
|
||||
@@ -3,6 +3,7 @@ base:
|
||||
mode: dev
|
||||
is_demo: false
|
||||
is_offline: false
|
||||
is_fxplay: false
|
||||
|
||||
log:
|
||||
level: debug
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
AppOpenresty = "openresty"
|
||||
AppOpenclaw = "openclaw"
|
||||
AppCopaw = "copaw"
|
||||
AppHermesAgent = "hermes-agent"
|
||||
AppMysql = "mysql"
|
||||
AppMariaDB = "mariadb"
|
||||
AppPostgresql = "postgresql"
|
||||
|
||||
@@ -8,6 +8,7 @@ type ServerConfig struct {
|
||||
|
||||
type Base struct {
|
||||
Port string `mapstructure:"port"`
|
||||
IsFxplay bool `mapstructure:"is_fxplay"`
|
||||
Edition string `mapstructure:"edition"` // [ cn / intl]
|
||||
Version string `mapstructure:"version"`
|
||||
EncryptKey string `mapstructure:"encrypt_key"`
|
||||
|
||||
@@ -42,18 +42,27 @@ var (
|
||||
)
|
||||
|
||||
func RepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/package/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/package/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2"
|
||||
}
|
||||
func ResourceURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2/resource"
|
||||
}
|
||||
func AppRepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
|
||||
+135
-133
@@ -6,127 +6,129 @@ replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go v1.55.0
|
||||
github.com/compose-spec/compose-go/v2 v2.9.1
|
||||
github.com/compose-spec/compose-go/v2 v2.10.2
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/docker/cli v29.2.1+incompatible
|
||||
github.com/docker/cli v29.4.0+incompatible
|
||||
github.com/docker/compose/v2 v2.40.3
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/docker/go-connections v0.7.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-acme/lego/v4 v4.33.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-acme/lego/v4 v4.34.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-playground/validator/v10 v10.30.2
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/go-resty/resty/v2 v2.17.1
|
||||
github.com/go-resty/resty/v2 v2.17.2
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/goh-chunlin/go-onedrive v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/minio/minio-go/v7 v7.0.74
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/minio/minio-go/v7 v7.0.100
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/qiniu/go-sdk/v7 v7.26.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.73
|
||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.35.0
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.30.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.260 // indirect
|
||||
github.com/bodgit/plumbing v1.2.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.12 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.25.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/connesc/cipherio v0.2.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd/api v1.10.0 // indirect
|
||||
github.com/containerd/containerd/v2 v2.2.1 // indirect
|
||||
github.com/containerd/containerd/v2 v2.2.3 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.2 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.4 // indirect
|
||||
github.com/containerd/ttrpc v1.2.8 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/buildx v0.31.1 // indirect
|
||||
github.com/docker/buildx v0.33.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gammazero/toposort v0.1.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
|
||||
github.com/go-acme/esa-20240910/v2 v2.48.0 // indirect
|
||||
@@ -135,27 +137,26 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.9.8 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/h2non/filetype v1.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 // indirect
|
||||
github.com/in-toto/attestation v1.1.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/h2non/filetype v1.1.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.192 // indirect
|
||||
github.com/in-toto/attestation v1.2.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.10.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@@ -163,27 +164,26 @@ require (
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.13 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/buildkit v0.28.1 // indirect
|
||||
github.com/moby/buildkit v0.29.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.2.1 // indirect
|
||||
github.com/moby/moby/api v1.54.1 // indirect
|
||||
github.com/moby/moby/client v0.4.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
@@ -193,39 +193,40 @@ require (
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.4.0 // indirect
|
||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.5.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sigstore/sigstore v1.10.4 // indirect
|
||||
github.com/sigstore/sigstore v1.10.5 // indirect
|
||||
github.com/sigstore/sigstore-go v1.1.4 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.80 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
@@ -235,43 +236,44 @@ require (
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.241 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/fileutil v1.3.3 // indirect
|
||||
modernc.org/libc v1.66.1 // indirect
|
||||
modernc.org/fileutil v1.4.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
)
|
||||
|
||||
+342
-330
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,9 @@ ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Up
|
||||
ErrAgentWebsiteBound: 'This agent is already bound to a website'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound'
|
||||
ErrAgentWebsiteInUse: 'This website is already bound to another agent'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Deployment websites cannot be unbound manually'
|
||||
ErrHermesPairingCodeUnavailable: 'The pairing code is temporarily unavailable in Hermes, possibly due to network issues. Please try again later.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'When the Feishu group policy is Allowlist, the DM policy cannot be Pairing Code.'
|
||||
|
||||
#backup
|
||||
Localhost: 'Local'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de
|
||||
ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos'
|
||||
ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Los sitios web de despliegue no se pueden desvincular manualmente'
|
||||
ErrHermesPairingCodeUnavailable: 'El código de emparejamiento no está disponible temporalmente en Hermes, posiblemente por un problema de red. Inténtalo de nuevo más tarde.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Cuando la política de grupo de Feishu es Lista permitida, la política de MD no puede ser Código de emparejamiento.'
|
||||
Localhost: 'Máquina local'
|
||||
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
|
||||
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {
|
||||
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
|
||||
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
|
||||
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'ワンクリックデプロイのサイトは手動で関連解除できません'
|
||||
ErrHermesPairingCodeUnavailable: 'Hermes でペアリングコードが一時的に見つかりません。ネットワーク要因の可能性があるため、しばらくしてから再試行してください。'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu のグループポリシーが許可リストの場合、DM ポリシーをペアリングコードにはできません。'
|
||||
Localhost: 'ローカルマシン'
|
||||
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
|
||||
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대
|
||||
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
|
||||
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
|
||||
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
|
||||
ErrAgentWebsiteUnbindUnsupported: '원클릭 배포 웹사이트는 수동으로 연결 해제할 수 없습니다'
|
||||
ErrHermesPairingCodeUnavailable: 'Hermes에서 페어링 코드가 일시적으로 존재하지 않습니다. 네트워크 문제일 수 있으니 잠시 후 다시 시도해 주세요.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu 그룹 정책이 허용 목록이면 DM 정책을 페어링 코드로 설정할 수 없습니다.'
|
||||
Localhost: '로컬 머신'
|
||||
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
|
||||
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Na
|
||||
ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan'
|
||||
ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Laman web one-click deployment tidak menyokong nyahikat manual'
|
||||
ErrHermesPairingCodeUnavailable: 'Kod pasangan buat sementara waktu tidak wujud dalam Hermes, mungkin disebabkan masalah rangkaian. Sila cuba lagi sebentar nanti.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Apabila dasar kumpulan Feishu ialah senarai benarkan, dasar DM tidak boleh menggunakan kod pasangan.'
|
||||
Localhost: 'Mesin Tempatan'
|
||||
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
|
||||
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de I
|
||||
ErrAgentWebsiteBound: 'Este agente já está vinculado a um site'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados'
|
||||
ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Sites implantados em um clique não podem ser desvinculados manualmente'
|
||||
ErrHermesPairingCodeUnavailable: 'O código de pareamento está temporariamente indisponível no Hermes, possivelmente por causa de rede. Tente novamente mais tarde.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Quando a política de grupo do Feishu é Lista de permissões, a política de DM não pode ser Código de pareamento.'
|
||||
Localhost: 'Máquina Local'
|
||||
ErrBackupInUsed: 'Conta de backup em uso por tarefa'
|
||||
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }
|
||||
ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты'
|
||||
ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Сайты one-click deployment нельзя отвязать вручную'
|
||||
ErrHermesPairingCodeUnavailable: 'Код сопряжения временно недоступен в Hermes, возможно из-за проблем с сетью. Повторите попытку позже.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Когда групповая политика Feishu — белый список, политика личных сообщений не может быть кодом сопряжения.'
|
||||
Localhost: 'Локальная машина'
|
||||
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
|
||||
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını dest
|
||||
ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir'
|
||||
ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Tek tıkla dağıtılan web sitelerinin bağlantısı manuel olarak kaldırılamaz'
|
||||
ErrHermesPairingCodeUnavailable: 'Eşleştirme kodu Hermes içinde geçici olarak bulunamıyor; bu durum ağ kaynaklı olabilir. Lütfen daha sonra tekrar deneyin.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu grup ilkesi izin listesi olduğunda, DM ilkesi eşleştirme kodu olamaz.'
|
||||
Localhost: 'Yerel Makine'
|
||||
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
|
||||
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
|
||||
|
||||
@@ -59,6 +59,9 @@ ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體
|
||||
ErrAgentWebsiteBound: '該智能體已關聯網站'
|
||||
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站'
|
||||
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
|
||||
ErrAgentWebsiteUnbindUnsupported: '一鍵部署網站不支援手動解綁'
|
||||
ErrHermesPairingCodeUnavailable: '配對碼在 Hermes 中暫時不存在,可能是由於網路原因,請稍後再試'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: '飛書群組策略為白名單時,私聊策略不能為配對碼'
|
||||
Localhost: '本機'
|
||||
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
|
||||
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
|
||||
|
||||
@@ -64,6 +64,9 @@ ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升
|
||||
ErrAgentWebsiteBound: "该智能体已关联网站"
|
||||
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站"
|
||||
ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
|
||||
ErrAgentWebsiteUnbindUnsupported: "一键部署网站不支持手动解绑"
|
||||
ErrHermesPairingCodeUnavailable: "配对码在 hermes 中暂时不存在,可能是由于网络原因,请稍后尝试"
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: "飞书群组策略为白名单时,私聊策略不能为配对码"
|
||||
|
||||
#backup
|
||||
Localhost: '本机'
|
||||
|
||||
@@ -57,6 +57,7 @@ func InitAgentDB() {
|
||||
migrations.UpdateDatabase,
|
||||
migrations.AddGPUMonitor,
|
||||
migrations.UpdateDatabaseMysql,
|
||||
migrations.AddDatabaseMongodb,
|
||||
migrations.InitIptablesStatus,
|
||||
migrations.UpdateWebsite,
|
||||
migrations.AddisIPtoWebsiteSSL,
|
||||
@@ -81,6 +82,7 @@ func InitAgentDB() {
|
||||
migrations.AddAgentWebsiteBinding,
|
||||
migrations.AddFileManageAISettings,
|
||||
migrations.AddFileShareTable,
|
||||
migrations.AddFileHistoryTable,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -49,6 +49,7 @@ var AddTable = &gormigrate.Migration{
|
||||
&model.Cronjob{},
|
||||
&model.Database{},
|
||||
&model.DatabaseMysql{},
|
||||
&model.DatabaseMongodb{},
|
||||
&model.DatabasePostgresql{},
|
||||
&model.Favorite{},
|
||||
&model.FileShare{},
|
||||
@@ -823,6 +824,13 @@ var UpdateDatabaseMysql = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var AddDatabaseMongodb = &gormigrate.Migration{
|
||||
ID: "20260413-add-database-mongodb",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.DatabaseMongodb{})
|
||||
},
|
||||
}
|
||||
|
||||
var InitIptablesStatus = &gormigrate.Migration{
|
||||
ID: "20251201-init-iptables-status",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
@@ -1225,3 +1233,30 @@ var AddFileShareTable = &gormigrate.Migration{
|
||||
return tx.AutoMigrate(&model.FileShare{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddFileHistoryTable = &gormigrate.Migration{
|
||||
ID: "20260414-add-file-history-table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.FileHistory{}); err != nil {
|
||||
return err
|
||||
}
|
||||
defaultRows := []model.Setting{
|
||||
{Key: "FileHistoryStatus", Value: constant.StatusEnable},
|
||||
{Key: "FileHistoryMaxPerPath", Value: "20"},
|
||||
{Key: "FileHistoryDiskQuotaMB", Value: "1024"},
|
||||
}
|
||||
for i := range defaultRows {
|
||||
var exist model.Setting
|
||||
if err := tx.Where("`key` = ?", defaultRows[i].Key).First(&exist).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&defaultRows[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !xpack
|
||||
//go:build !xpack && !xpackee
|
||||
|
||||
package router
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build xpack
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
xpackRouter "github.com/1Panel-dev/1Panel/agent/xpack/router"
|
||||
)
|
||||
|
||||
func RouterGroups() []CommonRouter {
|
||||
baseRouter := commonGroups()
|
||||
for _, ro := range xpackRouter.XpackGroups() {
|
||||
if val, ok := ro.(CommonRouter); ok {
|
||||
baseRouter = append(baseRouter, val)
|
||||
}
|
||||
}
|
||||
return baseRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = RouterGroups()
|
||||
@@ -47,8 +47,12 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken)
|
||||
aiToolsRouter.POST("/agents/remark", baseApi.UpdateAgentRemark)
|
||||
aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite)
|
||||
aiToolsRouter.POST("/agents/website/unbind", baseApi.UnbindAgentWebsite)
|
||||
aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig)
|
||||
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
|
||||
aiToolsRouter.POST("/agents/hermes/chat/sessions", baseApi.GetHermesChatSessions)
|
||||
aiToolsRouter.POST("/agents/hermes/chat/sessions/rename", baseApi.RenameHermesChatSession)
|
||||
aiToolsRouter.POST("/agents/hermes/chat/sessions/delete", baseApi.DeleteHermesChatSession)
|
||||
aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview)
|
||||
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
|
||||
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
|
||||
@@ -78,9 +82,11 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/channel/wecom/update", baseApi.UpdateAgentWecomConfig)
|
||||
aiToolsRouter.POST("/agents/channel/dingtalk/get", baseApi.GetAgentDingTalkConfig)
|
||||
aiToolsRouter.POST("/agents/channel/dingtalk/update", baseApi.UpdateAgentDingTalkConfig)
|
||||
aiToolsRouter.POST("/agents/channel/weixin/get", baseApi.GetAgentWeixinConfig)
|
||||
aiToolsRouter.POST("/agents/channel/weixin/login", baseApi.LoginAgentWeixinChannel)
|
||||
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
|
||||
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
|
||||
aiToolsRouter.POST("/agents/channel/delete", baseApi.DeleteAgentChannelConfig)
|
||||
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/plugin/upgrade", baseApi.UpgradeAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/plugin/uninstall", baseApi.UninstallAgentPlugin)
|
||||
@@ -95,6 +101,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/skills/search", baseApi.SearchAgentSkills)
|
||||
aiToolsRouter.POST("/agents/skills/update", baseApi.UpdateAgentSkill)
|
||||
aiToolsRouter.POST("/agents/skills/install", baseApi.InstallAgentSkill)
|
||||
aiToolsRouter.POST("/agents/skills/uninstall", baseApi.UninstallAgentSkill)
|
||||
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,5 +58,17 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
cmdRouter.POST("/pg/privileges", baseApi.ChangePostgresqlPrivileges)
|
||||
cmdRouter.POST("/pg/password", baseApi.ChangePostgresqlPassword)
|
||||
cmdRouter.POST("/pg/description", baseApi.UpdatePostgresqlDescription)
|
||||
|
||||
cmdRouter.POST("/mongodb", baseApi.CreateMongodb)
|
||||
cmdRouter.POST("/mongodb/search", baseApi.SearchMongodb)
|
||||
cmdRouter.POST("/mongodb/description", baseApi.UpdateMongodbDescription)
|
||||
cmdRouter.POST("/mongodb/load", baseApi.LoadMongodbFromRemote)
|
||||
cmdRouter.POST("/mongodb/bind", baseApi.BindMongodbUser)
|
||||
cmdRouter.POST("/mongodb/password", baseApi.ChangeMongodbPassword)
|
||||
cmdRouter.POST("/mongodb/root/password", baseApi.ChangeMongodbRootPassword)
|
||||
cmdRouter.POST("/mongodb/privileges", baseApi.LoadMongodbPrivileges)
|
||||
cmdRouter.POST("/mongodb/privileges/change", baseApi.ChangeMongodbPrivileges)
|
||||
cmdRouter.POST("/mongodb/del/check", baseApi.DeleteCheckMongodb)
|
||||
cmdRouter.POST("/mongodb/del", baseApi.DeleteMongodb)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,15 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
fileRouter.POST("/mode", baseApi.ChangeFileMode)
|
||||
fileRouter.POST("/owner", baseApi.ChangeFileOwner)
|
||||
fileRouter.POST("/compress", baseApi.CompressFile)
|
||||
fileRouter.POST("/compress/stop", baseApi.StopCompressFile)
|
||||
fileRouter.POST("/decompress", baseApi.DeCompressFile)
|
||||
fileRouter.POST("/content", baseApi.GetContent)
|
||||
fileRouter.POST("/preview", baseApi.PreviewContent)
|
||||
fileRouter.POST("/save", baseApi.SaveContent)
|
||||
fileRouter.POST("/history/search", baseApi.SearchFileHistory)
|
||||
fileRouter.POST("/history/content", baseApi.GetFileHistoryContent)
|
||||
fileRouter.POST("/history/del", baseApi.DeleteFileHistory)
|
||||
fileRouter.POST("/history/restore", baseApi.RestoreFileHistory)
|
||||
fileRouter.POST("/remarks", baseApi.BatchGetFileRemarks)
|
||||
fileRouter.POST("/remark", baseApi.SetFileRemark)
|
||||
fileRouter.POST("/check", baseApi.CheckFile)
|
||||
|
||||
@@ -14,10 +14,12 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
settingRouter.POST("/search", baseApi.GetSettingInfo)
|
||||
settingRouter.POST("/terminal/ai/search", baseApi.GetTerminalAISettingInfo)
|
||||
settingRouter.POST("/files/ai/search", baseApi.GetFileManageAISettingInfo)
|
||||
settingRouter.POST("/file-history/search", baseApi.GetFileHistorySettingInfo)
|
||||
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
|
||||
settingRouter.POST("/update", baseApi.UpdateSetting)
|
||||
settingRouter.POST("/terminal/ai/update", baseApi.UpdateTerminalAISetting)
|
||||
settingRouter.POST("/files/ai/update", baseApi.UpdateFileManageAISetting)
|
||||
settingRouter.POST("/file-history/update", baseApi.UpdateFileHistorySetting)
|
||||
settingRouter.GET("/get/:key", baseApi.GetSettingByKey)
|
||||
|
||||
settingRouter.POST("/description/save", baseApi.SaveDescription)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !xpack
|
||||
//go:build !xpack && !xpackee
|
||||
|
||||
package server
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build xpack
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
xpack "github.com/1Panel-dev/1Panel/agent/xpack"
|
||||
)
|
||||
|
||||
func InitOthers() {
|
||||
xpack.Init()
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
s3DefaultTimeout = 30 * time.Second
|
||||
s3TransferTimeout = 24 * time.Hour
|
||||
)
|
||||
|
||||
type s3Client struct {
|
||||
scType string
|
||||
bucket string
|
||||
Sess session.Session
|
||||
client *minio.Client
|
||||
}
|
||||
|
||||
func (s *s3Client) ctx(timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), timeout)
|
||||
}
|
||||
|
||||
func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
|
||||
@@ -31,69 +41,80 @@ func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
|
||||
if len(mode) == 0 {
|
||||
mode = "virtual hosted"
|
||||
}
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(true), S3ForcePathStyle: aws.Bool(mode == "path"),
|
||||
|
||||
lookupStyle := minio.BucketLookupDNS
|
||||
if mode == "path" {
|
||||
lookupStyle = minio.BucketLookupPath
|
||||
}
|
||||
|
||||
ssl := strings.Split(endpoint, ":")[0]
|
||||
secure := false
|
||||
tlsConfig := &tls.Config{}
|
||||
if ssl == "https" {
|
||||
secure = true
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
var transport http.RoundTripper = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
|
||||
endpoint = strings.TrimPrefix(endpoint, ssl+"://")
|
||||
|
||||
client, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: secure,
|
||||
Region: region,
|
||||
BucketLookup: lookupStyle,
|
||||
Transport: transport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s3Client{scType: scType, bucket: bucket, Sess: *sess}, nil
|
||||
return &s3Client{scType: scType, bucket: bucket, client: client}, nil
|
||||
}
|
||||
|
||||
func (s s3Client) ListBuckets() ([]interface{}, error) {
|
||||
var result []interface{}
|
||||
svc := s3.New(&s.Sess)
|
||||
res, err := svc.ListBuckets(nil)
|
||||
ctx, cancel := s.ctx(s3DefaultTimeout)
|
||||
defer cancel()
|
||||
buckets, err := s.client.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range res.Buckets {
|
||||
var result []interface{}
|
||||
for _, b := range buckets {
|
||||
result = append(result, b.Name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s s3Client) Exist(path string) (bool, error) {
|
||||
svc := s3.New(&s.Sess)
|
||||
if _, err := svc.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: &s.bucket,
|
||||
Key: &path,
|
||||
}); err != nil {
|
||||
if aerr, ok := err.(awserr.RequestFailure); ok {
|
||||
if aerr.StatusCode() == 404 {
|
||||
return false, nil
|
||||
}
|
||||
} else {
|
||||
return false, aerr
|
||||
ctx, cancel := s.ctx(s3DefaultTimeout)
|
||||
defer cancel()
|
||||
_, err := s.client.StatObject(ctx, s.bucket, path, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
resp := minio.ToErrorResponse(err)
|
||||
if resp.StatusCode == 404 {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *s3Client) Size(path string) (int64, error) {
|
||||
svc := s3.New(&s.Sess)
|
||||
file, err := svc.GetObject(&s3.GetObjectInput{
|
||||
Bucket: &s.bucket,
|
||||
Key: &path,
|
||||
})
|
||||
ctx, cancel := s.ctx(s3DefaultTimeout)
|
||||
defer cancel()
|
||||
info, err := s.client.StatObject(ctx, s.bucket, path, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return *file.ContentLength, nil
|
||||
return info.Size, nil
|
||||
}
|
||||
|
||||
func (s s3Client) Delete(path string) (bool, error) {
|
||||
svc := s3.New(&s.Sess)
|
||||
if _, err := svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(s.bucket), Key: aws.String(path)}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(path),
|
||||
}); err != nil {
|
||||
ctx, cancel := s.ctx(s3DefaultTimeout)
|
||||
defer cancel()
|
||||
if err := s.client.RemoveObject(ctx, s.bucket, path, minio.RemoveObjectOptions{}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
@@ -110,16 +131,21 @@ func (s s3Client) Upload(src, target string) (bool, error) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
uploader := s3manager.NewUploader(&s.Sess)
|
||||
if fileInfo.Size() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
||||
uploader.PartSize = fileInfo.Size() / (s3manager.MaxUploadParts - 1)
|
||||
opts := minio.PutObjectOptions{
|
||||
StorageClass: s.scType,
|
||||
}
|
||||
if _, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(target),
|
||||
Body: file,
|
||||
StorageClass: &s.scType,
|
||||
}); err != nil {
|
||||
|
||||
const maxParts = 10000
|
||||
const defaultPartSize = 64 * 1024 * 1024 // 64 MiB
|
||||
partSize := uint64(defaultPartSize)
|
||||
if fileInfo.Size() > int64(maxParts)*int64(defaultPartSize) {
|
||||
partSize = uint64(fileInfo.Size()) / (maxParts - 1)
|
||||
}
|
||||
opts.PartSize = partSize
|
||||
|
||||
ctx, cancel := s.ctx(s3TransferTimeout)
|
||||
defer cancel()
|
||||
if _, err := s.client.PutObject(ctx, s.bucket, target, file, fileInfo.Size(), opts); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
@@ -129,16 +155,9 @@ func (s s3Client) Download(src, target string) (bool, error) {
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
_ = os.Remove(target)
|
||||
}
|
||||
file, err := os.Create(target)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
downloader := s3manager.NewDownloader(&s.Sess)
|
||||
if _, err = downloader.Download(file, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(src),
|
||||
}); err != nil {
|
||||
ctx, cancel := s.ctx(s3TransferTimeout)
|
||||
defer cancel()
|
||||
if err := s.client.FGetObject(ctx, s.bucket, src, target, minio.GetObjectOptions{}); err != nil {
|
||||
os.Remove(target)
|
||||
return false, err
|
||||
}
|
||||
@@ -146,17 +165,18 @@ func (s s3Client) Download(src, target string) (bool, error) {
|
||||
}
|
||||
|
||||
func (s *s3Client) ListObjects(prefix string) ([]string, error) {
|
||||
svc := s3.New(&s.Sess)
|
||||
var result []string
|
||||
outputs, err := svc.ListObjects(&s3.ListObjectsInput{
|
||||
Bucket: &s.bucket,
|
||||
Prefix: &prefix,
|
||||
})
|
||||
if err != nil {
|
||||
return result, err
|
||||
opts := minio.ListObjectsOptions{
|
||||
Recursive: true,
|
||||
Prefix: prefix,
|
||||
}
|
||||
for _, item := range outputs.Contents {
|
||||
result = append(result, *item.Key)
|
||||
var result []string
|
||||
ctx, cancel := s.ctx(s3DefaultTimeout)
|
||||
defer cancel()
|
||||
for object := range s.client.ListObjects(ctx, s.bucket, opts) {
|
||||
if object.Err != nil {
|
||||
return result, object.Err
|
||||
}
|
||||
result = append(result, object.Key)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
type ShellArchiver interface {
|
||||
Extract(filePath, dstDir string, secret string) error
|
||||
Compress(sourcePaths []string, dstFile string, secret string) error
|
||||
Compress(ctx context.Context, sourcePaths []string, dstFile string, secret string) error
|
||||
}
|
||||
|
||||
func NewShellArchiver(compressType CompressType) (ShellArchiver, error) {
|
||||
@@ -18,12 +20,47 @@ func NewShellArchiver(compressType CompressType) (ShellArchiver, error) {
|
||||
}
|
||||
return NewTarArchiver(compressType), nil
|
||||
case TarGz:
|
||||
if err := checkCmdAvailability("tar"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTarGzArchiver(), nil
|
||||
case Zip:
|
||||
if err := checkCmdAvailability("zip"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewZipArchiver(), nil
|
||||
case Rar:
|
||||
if err := checkCmdAvailability("rar"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRarArchiver(), nil
|
||||
case X7z:
|
||||
if err := checkCmdAvailability("7z"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewX7zArchiver(), nil
|
||||
default:
|
||||
return nil, buserr.New("unsupported compress type")
|
||||
}
|
||||
}
|
||||
|
||||
func NewExtractShellArchiver(compressType CompressType) (ShellArchiver, error) {
|
||||
switch compressType {
|
||||
case Tar:
|
||||
if err := checkCmdAvailability("tar"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTarArchiver(compressType), nil
|
||||
case TarGz:
|
||||
if err := checkCmdAvailability("tar"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTarGzArchiver(), nil
|
||||
case Zip:
|
||||
if err := checkCmdAvailability("unzip"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewZipArchiver(), nil
|
||||
case Rar:
|
||||
if err := checkCmdAvailability("unrar"); err != nil {
|
||||
return nil, err
|
||||
@@ -35,7 +72,7 @@ func NewShellArchiver(compressType CompressType) (ShellArchiver, error) {
|
||||
}
|
||||
return NewX7zArchiver(), nil
|
||||
default:
|
||||
return nil, buserr.New("unsupported compress type")
|
||||
return nil, buserr.New("unsupported decompress type")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -542,7 +542,7 @@ func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
|
||||
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
|
||||
}
|
||||
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
|
||||
if err := cmd.RunDefaultBashCf(mvCommand); err != nil {
|
||||
if err := cmd.RunDefaultBashC(mvCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -801,7 +801,7 @@ func getFormat(cType CompressType) archiver.CompressedArchive {
|
||||
return format
|
||||
}
|
||||
|
||||
func (f FileOp) Compress(srcRiles []string, dst string, name string, cType CompressType, secret string) error {
|
||||
func (f FileOp) Compress(ctx context.Context, srcRiles []string, dst string, name string, cType CompressType, secret string, progress func(current, total int, message string)) error {
|
||||
format := getFormat(cType)
|
||||
|
||||
fileMaps := make(map[string]string, len(srcRiles))
|
||||
@@ -819,47 +819,84 @@ func (f FileOp) Compress(srcRiles []string, dst string, name string, cType Compr
|
||||
return err
|
||||
}
|
||||
dstFile := filepath.Join(dst, name)
|
||||
out, err := f.Fs.Create(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
switch cType {
|
||||
case Zip:
|
||||
if err := ZipFile(files, out); err == nil {
|
||||
case Zip, SdkZip:
|
||||
out, err := f.Fs.Create(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
if err := ZipFile(ctx, files, out, progress); err == nil {
|
||||
return nil
|
||||
}
|
||||
_ = f.DeleteFile(dstFile)
|
||||
return NewZipArchiver().Compress(srcRiles, dstFile, "")
|
||||
return NewZipArchiver().Compress(ctx, srcRiles, dstFile, "")
|
||||
case TarGz:
|
||||
err = NewTarGzArchiver().Compress(srcRiles, dstFile, secret)
|
||||
err = NewTarGzArchiver().Compress(ctx, srcRiles, dstFile, secret)
|
||||
if err != nil {
|
||||
_ = f.DeleteFile(dstFile)
|
||||
return err
|
||||
}
|
||||
case Rar:
|
||||
err = NewRarArchiver().Compress(srcRiles, dstFile, secret)
|
||||
if err := checkCmdAvailability("rar"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = NewRarArchiver().Compress(ctx, srcRiles, dstFile, secret)
|
||||
if err != nil {
|
||||
_ = f.DeleteFile(dstFile)
|
||||
return err
|
||||
}
|
||||
case X7z:
|
||||
err = NewX7zArchiver().Compress(srcRiles, dstFile, secret)
|
||||
if err := checkCmdAvailability("7z"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = NewX7zArchiver().Compress(ctx, srcRiles, dstFile, secret)
|
||||
if err != nil {
|
||||
_ = f.DeleteFile(dstFile)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
err = format.Archive(context.Background(), out, files)
|
||||
tmpFile, err := os.CreateTemp(dst, fmt.Sprintf("temp_*%s", filepath.Ext(name)))
|
||||
if err != nil {
|
||||
_ = f.DeleteFile(dstFile)
|
||||
return err
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
_ = tmpFile.Close()
|
||||
if !success {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
_ = f.DeleteFile(dstFile)
|
||||
}
|
||||
}()
|
||||
|
||||
err = format.Archive(ctx, &contextAwareWriter{ctx: ctx, writer: tmpFile}, files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Rename(tmpFile.Name(), dstFile); err != nil {
|
||||
return err
|
||||
}
|
||||
success = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type contextAwareWriter struct {
|
||||
ctx context.Context
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (w *contextAwareWriter) Write(p []byte) (int, error) {
|
||||
if err := w.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
|
||||
func isIgnoreFile(name string) bool {
|
||||
return strings.HasPrefix(name, "__MACOSX") || strings.HasSuffix(name, ".DS_Store") || strings.HasPrefix(name, "._")
|
||||
}
|
||||
@@ -950,7 +987,7 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
|
||||
|
||||
func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error {
|
||||
if cType == Tar || cType == Zip || cType == TarGz || cType == Rar || cType == X7z {
|
||||
shellArchiver, err := NewShellArchiver(cType)
|
||||
shellArchiver, err := NewExtractShellArchiver(cType)
|
||||
if !f.Stat(dst) {
|
||||
_ = f.CreateDir(dst, 0755)
|
||||
}
|
||||
@@ -975,11 +1012,19 @@ func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secre
|
||||
return f.decompressWithSDK(srcFile, dst, cType)
|
||||
}
|
||||
|
||||
func ZipFile(files []archiver.File, dst afero.File) error {
|
||||
func ZipFile(ctx context.Context, files []archiver.File, dst afero.File, progress func(current, total int, message string)) error {
|
||||
zw := zip.NewWriter(dst)
|
||||
defer zw.Close()
|
||||
|
||||
for _, file := range files {
|
||||
total := len(files)
|
||||
for i, file := range files {
|
||||
if ctx != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
hdr, err := zip.FileInfoHeader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1009,16 +1054,40 @@ func ZipFile(files []archiver.File, dst afero.File) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, fileReader)
|
||||
_, err = io.Copy(w, newContextReader(ctx, fileReader))
|
||||
fileReader.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if progress != nil {
|
||||
progress(i+1, total, file.NameInArchive)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type contextReader struct {
|
||||
ctx context.Context
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func newContextReader(ctx context.Context, r io.Reader) io.Reader {
|
||||
if ctx == nil {
|
||||
return r
|
||||
}
|
||||
return &contextReader{ctx: ctx, r: r}
|
||||
}
|
||||
|
||||
func (r *contextReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, r.ctx.Err()
|
||||
default:
|
||||
return r.r.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.CompressedArchive) error {
|
||||
input, err := f.Fs.Open(srcFile)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
@@ -25,7 +26,7 @@ func (z RarArchiver) Extract(filePath, dstDir string, _ string) error {
|
||||
return cmd.RunDefaultBashCf("unrar x -y -o+ %q %q", filePath, dstDir)
|
||||
}
|
||||
|
||||
func (z RarArchiver) Compress(sourcePaths []string, dstFile string, _ string) (err error) {
|
||||
func (z RarArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) (err error) {
|
||||
if err = checkCmdAvailability("rar"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,7 +46,7 @@ func (z RarArchiver) Compress(sourcePaths []string, dstFile string, _ string) (e
|
||||
}
|
||||
|
||||
cmdArgs := append([]string{"a", "-r", tmpFile}, relativePaths...)
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir))
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir), cmd.WithContext(ctx))
|
||||
if err = cmdMgr.Run("rar", cmdArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
@@ -20,7 +22,7 @@ func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) erro
|
||||
return cmd.RunDefaultBashCf("%s %s \"%s\" -C \"%s\"", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)
|
||||
}
|
||||
|
||||
func (t TarArchiver) Compress(sourcePaths []string, dstFile string, secret string) error {
|
||||
func (t TarArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, secret string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
)
|
||||
|
||||
type TarGzArchiver struct {
|
||||
@@ -37,7 +41,17 @@ func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TarGzArchiver) Compress(sourcePaths []string, dstFile string, secret string) error {
|
||||
func (t TarGzArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, secret string) error {
|
||||
tmpFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("%s%s.tar.gz", common.RandStr(50), time.Now().Format("20060102150405")))
|
||||
op := NewFileOp()
|
||||
var err error
|
||||
defer func() {
|
||||
_ = op.DeleteFile(tmpFile)
|
||||
if err != nil {
|
||||
_ = op.DeleteFile(dstFile)
|
||||
}
|
||||
}()
|
||||
|
||||
var itemDirs []string
|
||||
for _, item := range sourcePaths {
|
||||
itemDirs = append(itemDirs, fmt.Sprintf("\"%s\"", filepath.Base(item)))
|
||||
@@ -49,14 +63,18 @@ func (t TarGzArchiver) Compress(sourcePaths []string, dstFile string, secret str
|
||||
}
|
||||
commands := ""
|
||||
if len(secret) != 0 {
|
||||
extraCmd := fmt.Sprintf("| openssl enc -aes-256-cbc -salt -k '%s' -out '%s'", secret, dstFile)
|
||||
extraCmd := fmt.Sprintf("| openssl enc -aes-256-cbc -salt -k '%s' -out '%s'", secret, tmpFile)
|
||||
commands = fmt.Sprintf("tar -zcf - -C \"%s\" %s %s", aheadDir, itemDir, extraCmd)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" '%s' ", secret), " ****** "))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar -zcf \"%s\" -C \"%s\" %s", dstFile, aheadDir, itemDir)
|
||||
commands = fmt.Sprintf("tar -zcf \"%s\" -C \"%s\" %s", tmpFile, aheadDir, itemDir)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
if err := cmd.RunDefaultBashC(commands); err != nil {
|
||||
err = cmd.NewCommandMgr(cmd.WithContext(ctx)).RunBashC(commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = op.Mv(tmpFile, dstFile); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
@@ -25,7 +26,7 @@ func (z X7zArchiver) Extract(filePath, dstDir string, _ string) error {
|
||||
return cmd.RunDefaultBashCf("7z x -y -o%q %q", dstDir, filePath)
|
||||
}
|
||||
|
||||
func (z X7zArchiver) Compress(sourcePaths []string, dstFile string, _ string) (err error) {
|
||||
func (z X7zArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) (err error) {
|
||||
if err = checkCmdAvailability("7z"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,7 +46,7 @@ func (z X7zArchiver) Compress(sourcePaths []string, dstFile string, _ string) (e
|
||||
}
|
||||
|
||||
cmdArgs := append([]string{"a", "-r", tmpFile}, relativePaths...)
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir))
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir), cmd.WithContext(ctx))
|
||||
if err = cmdMgr.Run("7z", cmdArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -26,7 +27,7 @@ func (z ZipArchiver) Extract(filePath, dstDir string, secret string) error {
|
||||
return cmd.RunDefaultBashCf("unzip -qo %s -d %s", filePath, dstDir)
|
||||
}
|
||||
|
||||
func (z ZipArchiver) Compress(sourcePaths []string, dstFile string, _ string) error {
|
||||
func (z ZipArchiver) Compress(ctx context.Context, sourcePaths []string, dstFile string, _ string) error {
|
||||
var err error
|
||||
tmpFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("%s%s.zip", common.RandStr(50), time.Now().Format(constant.DateTimeSlimLayout)))
|
||||
op := NewFileOp()
|
||||
@@ -41,7 +42,7 @@ func (z ZipArchiver) Compress(sourcePaths []string, dstFile string, _ string) er
|
||||
for i, sp := range sourcePaths {
|
||||
relativePaths[i] = path.Base(sp)
|
||||
}
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir))
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithWorkDir(baseDir), cmd.WithContext(ctx))
|
||||
if err = cmdMgr.Run("zip", "-qr", tmpFile, strings.Join(relativePaths, " ")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ func ResolveGeneratorConfigFromAgentSettings() (GeneratorConfig, uint, time.Dura
|
||||
return config, uint(accountID), timeout, err
|
||||
}
|
||||
|
||||
// ResolveGeneratorConfigFromFileSettings loads file-management AI search settings (independent from terminal AI).
|
||||
func ResolveGeneratorConfigFromFileSettings() (GeneratorConfig, uint, time.Duration, error) {
|
||||
status, err := loadAgentSettingValue("FileAIStatus")
|
||||
if err != nil {
|
||||
@@ -151,7 +150,6 @@ func ResolveGeneratorConfigFromFileSettings() (GeneratorConfig, uint, time.Durat
|
||||
return config, uint(accountID), timeout, err
|
||||
}
|
||||
|
||||
// LoadFileAIRuntimeConfig returns LLM client config for file-management AI search.
|
||||
func LoadFileAIRuntimeConfig() (GeneratorConfig, time.Duration, error) {
|
||||
cfg, _, timeout, err := ResolveGeneratorConfigFromFileSettings()
|
||||
return cfg, timeout, err
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !xpack
|
||||
//go:build !xpack && !xpackee
|
||||
|
||||
package xpack
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build xpack
|
||||
|
||||
package xpack
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
edition "github.com/1Panel-dev/1Panel/agent/xpack/edition"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RemoveTamper(website string) {
|
||||
edition.RemoveTamper(website)
|
||||
}
|
||||
|
||||
func StartClam(startClam *model.Clam, isUpdate bool) (int, error) {
|
||||
return edition.StartClam(startClam, isUpdate)
|
||||
}
|
||||
|
||||
func LoadNodeInfo(isBase bool) (model.NodeInfo, error) {
|
||||
return edition.LoadNodeInfo(isBase)
|
||||
}
|
||||
|
||||
func GetImagePrefix() string {
|
||||
return edition.GetImagePrefix()
|
||||
}
|
||||
|
||||
func IsUseCustomApp() bool {
|
||||
return edition.IsUseCustomApp()
|
||||
}
|
||||
|
||||
func IsXpack() bool {
|
||||
return edition.IsXpack()
|
||||
}
|
||||
|
||||
func CreateTaskScanSMSAlertLog(info dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
|
||||
return edition.CreateTaskScanSMSAlertLog(info, alertType, create, pushAlert, method)
|
||||
}
|
||||
|
||||
func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error {
|
||||
return edition.CreateSMSAlertLog(alertType, info, create, project, params, method)
|
||||
}
|
||||
|
||||
func CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error {
|
||||
return edition.CreateTaskScanWebhookAlertLog(alert, alertType, create, pushAlert, method, transport, agentInfo)
|
||||
}
|
||||
|
||||
func CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error {
|
||||
return edition.CreateWebhookAlertLog(alertType, info, create, project, params, method, transport, agentInfo)
|
||||
}
|
||||
|
||||
func GetLicenseErrorAlert() (uint, error) {
|
||||
return edition.GetLicenseErrorAlert()
|
||||
}
|
||||
|
||||
func GetNodeErrorAlert() (uint, error) {
|
||||
return edition.GetNodeErrorAlert()
|
||||
}
|
||||
|
||||
func LoadRequestTransport() *http.Transport { return edition.LoadRequestTransport() }
|
||||
|
||||
func ValidateCertificate(c *gin.Context) bool {
|
||||
return edition.ValidateCertificate(c)
|
||||
}
|
||||
|
||||
func PushSSLToNode(websiteSSL *model.WebsiteSSL) error {
|
||||
return edition.PushSSLToNode(websiteSSL)
|
||||
}
|
||||
|
||||
func GetAgentInfo() (*dto.AgentInfo, error) {
|
||||
return edition.GetAgentInfo()
|
||||
}
|
||||
@@ -86,6 +86,8 @@ var WebUrlMap = map[string]struct{}{
|
||||
"/databases": {},
|
||||
"/databases/mysql": {},
|
||||
"/databases/mysql/remote": {},
|
||||
"/databases/mongodb": {},
|
||||
"/databases/mongodb/remote": {},
|
||||
"/databases/postgresql": {},
|
||||
"/databases/postgresql/remote": {},
|
||||
"/databases/redis": {},
|
||||
|
||||
@@ -36,18 +36,27 @@ var (
|
||||
type DBOption func(*gorm.DB) *gorm.DB
|
||||
|
||||
func RepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/package/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/package/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2"
|
||||
}
|
||||
func ResourceURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2/resource"
|
||||
}
|
||||
func AppRepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
|
||||
+75
-58
@@ -4,106 +4,123 @@ go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/1panel-dev/base64Captcha v1.3.8
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/gzip v1.2.6
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2
|
||||
github.com/go-playground/validator/v10 v10.22.0
|
||||
github.com/go-resty/resty/v2 v2.15.3
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-playground/validator/v10 v10.30.2
|
||||
github.com/go-resty/resty/v2 v2.17.2
|
||||
github.com/go-webauthn/webauthn v0.16.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/files/v2 v2.0.2
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.35.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/text v0.36.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.30.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/spec v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/loading v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.26 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mailru/easyjson v0.9.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/image v0.38.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/image v0.39.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
modernc.org/libc v1.66.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
)
|
||||
|
||||
+155
@@ -6,20 +6,33 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
|
||||
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -30,32 +43,68 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
|
||||
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
|
||||
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
|
||||
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.23.0 h1:c25HFTJ6uWGmoe5BQI6p72p4o7KnlWYsy1MeFlAumsw=
|
||||
github.com/go-openapi/jsonpointer v0.23.0/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
|
||||
github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
|
||||
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
|
||||
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
|
||||
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
|
||||
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
|
||||
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
|
||||
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
|
||||
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
|
||||
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
|
||||
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -64,24 +113,42 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
|
||||
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
|
||||
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
||||
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
|
||||
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
|
||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
@@ -106,6 +173,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -121,12 +190,18 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -136,8 +211,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
@@ -145,13 +224,23 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -161,26 +250,43 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -193,6 +299,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
@@ -201,37 +308,61 @@ github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
|
||||
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
@@ -241,6 +372,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
@@ -259,11 +392,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -271,6 +408,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -278,9 +417,13 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -288,6 +431,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
@@ -297,18 +442,26 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.0.3 h1:y81b9r3asCh6Xtse6Nz85aYGB0cG3M3U6222yap1KWI=
|
||||
modernc.org/goabi0 v0.0.3/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/libc v1.66.1 h1:4uQsntXbVyAgrV+j6NhKvDiUypoJL48BWQx6sy9y8ok=
|
||||
modernc.org/libc v1.66.1/go.mod h1:AiZxInURfEJx516LqEaFcrC+X38rt9G7+8ojIXQKHbo=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -319,6 +472,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -30,6 +30,16 @@ func Init() {
|
||||
}
|
||||
LocalAgentProxy = &httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
if req.Header.Get("X-Forwarded-Proto") == "" {
|
||||
if req.TLS != nil {
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
}
|
||||
if req.Header.Get("X-Forwarded-Host") == "" && req.Host != "" {
|
||||
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||
}
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = "unix"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ type sessionItem struct {
|
||||
type PSession struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]sessionItem
|
||||
cleanupCursor uint64
|
||||
cleanupCursor atomic.Uint64
|
||||
lastFullCleanup time.Time
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ func (p *PSession) cleanupExpiredSample(now time.Time, limit int) {
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
start := int(atomic.AddUint64(&p.cleanupCursor, uint64(limit)) % uint64(total))
|
||||
start := int(p.cleanupCursor.Add(uint64(limit)) % uint64(total))
|
||||
checked := 0
|
||||
|
||||
idx := 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !xpack
|
||||
//go:build !xpack && !xpackee
|
||||
|
||||
package router
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build xpack
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
xpackRouter "github.com/1Panel-dev/1Panel/core/xpack/router"
|
||||
)
|
||||
|
||||
func RouterGroups() []CommonRouter {
|
||||
baseRouter := commonGroups()
|
||||
for _, ro := range xpackRouter.XpackGroups() {
|
||||
if val, ok := ro.(CommonRouter); ok {
|
||||
baseRouter = append(baseRouter, val)
|
||||
}
|
||||
}
|
||||
return baseRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = RouterGroups()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user