Compare commits

...
Author SHA1 Message Date
蘭 5ab758d626 fix: Fix the pagination issue in the historical version list (#12583) 2026-04-23 11:20:12 +08:00
王贺 3a0a6537f0 ci: skip add-labels for dependabot PRs
* ci: skip add-labels for dependabot PRs
2026-04-22 17:31:53 +08:00
蘭 4eda92f25c refactor: simplify pagination logic and update theme change listener (#12564) 2026-04-22 16:53:24 +08:00
王贺 94f8c5d2f1 chore: add dependabot config (#12565) 2026-04-22 16:53:12 +08:00
王贺 92598effc1 fix: keep app tags on one line (#12563) 2026-04-22 15:26:38 +08:00
CityFun 748cecaee8 feat: add fu-step (#12562) 2026-04-22 15:26:26 +08:00
王贺 b783034d0a refactor: refine translations (#12561) 2026-04-22 14:13:06 +08:00
CityFun 94a4e39793 feat: remove some config for hermes channel (#12560) 2026-04-22 13:43:24 +08:00
CityFun 1864dee591 style: change file history table style (#12559) 2026-04-22 03:03:01 +00:00
ssongliu 8c4ea85d02 feat: support fxplay resource URLs (#12558) 2026-04-22 03:01:00 +00:00
CityFun a714d5a84e feat: update hermes qq wecom channel config (#12555) 2026-04-21 19:49:32 +08:00
Wu MX 4f97a7890f feat: Enhance container data handling and status synchronization (#12554) 2026-04-21 18:45:50 +08:00
蘭 f15f91e5fa feat: add support for stopping compression tasks and enhance compression functionality (#12553) 2026-04-21 18:40:45 +08:00
CityFun 02bd500917 style: change table style (#12552) 2026-04-21 16:35:34 +08:00
CityFun 810c6fd232 feat: change hermes channel logic (#12551) 2026-04-21 16:35:24 +08:00
ssongliu d50a8983bc fix: adjust home dashboard styles (#12550) 2026-04-21 16:35:15 +08:00
ssongliu 89647304bb fix: adjust home dashboard styles (#12549) 2026-04-21 16:35:04 +08:00
蘭 9ebad13bf8 fix: Fix the error of switching between the current file and the historical version of the opened file (#12548) 2026-04-21 16:34:54 +08:00
王贺 cc0ac8cc43 fix mongodb root password sync (#12547)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 04:00:57 +00:00
王贺 9f5a1d842a fix mongodb password action (#12546)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:58:58 +00:00
王贺 beb33957be fix mongodb upload alert visibility (#12545)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:56:57 +00:00
蘭 ffcbbb705b fix: restore codicons and folding support in Monaco editor (#12544) 2026-04-21 03:54:58 +00:00
王贺 058bc71630 fix mongodb remote edit action (#12543)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:52:58 +00:00
王贺 0a38979bd9 fix mongodb uninstall cleanup (#12542)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:50:57 +00:00
CityFun 5fa4a15914 style: Optimize the table styling. (#12540) 2026-04-20 22:21:45 +08:00
CityFun 81c551a11b feat: Optimize the change detection for application Compose files. (#12539) 2026-04-20 17:47:52 +08:00
CityFun 5681449353 fix: Optimize the website name badge style. (#12538) 2026-04-20 17:47:42 +08:00
CityFun 88849e6355 fix: Fix the issue where the directory can still be modified while editing the runtime environment. (#12537) 2026-04-20 17:47:31 +08:00
蘭 d6bbcdc32f fix: improve pagination handling and directory naming in recycle bin (#12535) 2026-04-20 06:44:56 +00:00
KOMATA abd1581c1d fix: enhance file and trash services with metadata validation (#12529)
* feat: Enhance file and recycle bin services with metadata handling and validation

* refactor: Rename recycle bin metadata handling to trash info and update related functions

* refactor: Remove orphan trash info pruning logic and related map usage in recycle bin service
2026-04-20 14:11:15 +08:00
CityFun cd30ec7632 feat: support session delete for hermes-agent (#12533) 2026-04-20 06:08:56 +00:00
蘭 646826769a feat: Enhance file sharing functions with optional headers for improved localization support (#12530) 2026-04-20 06:06:56 +00:00
卢亦之 074c0abb3b fix: correct listening status display on the firewall page (#12516)
The bug only appeared when the same process had both TCP and UDP sockets:
every listening port on that process was attributed to whichever protocol
was encountered first. For example, if a process listened on TCP 8000 and
UDP 8001, the UI could wrongly show TCP 8001 as in use while UDP 8001
looked unused.

Use (PID, conn.Type) as the cache key so each protocol has its own
ListeningProcess entry.
2026-04-20 11:27:18 +08:00
ssongliu 3a00e893bc fix: fix psession atomic alignment (#12528) 2026-04-20 03:26:55 +00:00
蘭 2f37677650 fix: Refactor file history management to include current content and improve error handling (#12527) 2026-04-20 02:58:56 +00:00
CityFun 12ae89923b feat: support skill management for hermes-agent (#12514) 2026-04-17 16:36:17 +08:00
蘭 031a203a74 fix: Enhance file history settings with default values and validation (#12512) 2026-04-17 16:05:57 +08:00
蘭 646b99974a feat: File editing supports Dockerfile language (#12511) 2026-04-17 16:05:46 +08:00
蘭 254ed954e9 feat: Add file history management features (#12510) 2026-04-16 19:11:05 +08:00
CityFun a0050dca09 feat: update golang Dependency (#12509) 2026-04-16 17:36:52 +08:00
王贺 cd80b4e6c3 fix(frontend): clean up build warnings (#12508) 2026-04-16 16:53:24 +08:00
CityFun df5d3d782c feat: Optimize the openclaw channel bot add flow (#12507) 2026-04-16 08:52:48 +00:00
CityFun 3c5b3c3467 feat: Optimize the Hermes WeChat login flow (#12506) 2026-04-16 16:42:21 +08:00
Copilotandwan92hen 2bdba42d87 docs: add translation contribution guide (#12505)
* docs: add translation contribution guide with reference PR and checklist

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/e6a41d10-557e-4ca1-a78b-18888a9f17be

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

* docs: clarify frontend vs backend locale file naming conventions

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/e6a41d10-557e-4ca1-a78b-18888a9f17be

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

* docs: update reference PR to #10352 (merged Spanish locale)

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/2b14f230-892a-412e-baed-07e67614d1e9

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>
2026-04-16 15:00:11 +08:00
CityFun 0b146f7ff3 feat: add unbind website for agent (#12500) 2026-04-15 18:32:46 +08:00
BugKing c28a4f6d69 Remove link to development guideline from CONTRIBUTING.md (#12502)
Removed a link to the development guideline to streamline the contributing process.
2026-04-15 18:32:13 +08:00
CityFun f93a34acc4 fix(ai): Fixed issue with delete hermes-agent failed (#12499) 2026-04-15 08:24:46 +00:00
CityFun 4e78841bf6 feat(ai): support Hermes Agent chat sessions in 1Panel (#12497)
* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): change docker command
2026-04-15 15:44:20 +08:00
王贺 7f5bb58c72 fix: mongodb restore fixes (#12496)
* fix mongodb restore flow

* chore cleanup i18n and terminal style
2026-04-15 15:26:30 +08:00
王贺 6a28359ac2 fix mongodb route fallback (#12495) 2026-04-15 13:51:20 +08:00
王贺 59655695b5 fix(frontend): correct anti-leech component import path (#12494) 2026-04-15 11:29:27 +08:00
CityFun 786c5c1d7c feat: support hermes-agent (#12493) 2026-04-15 03:10:45 +00:00
KOMATAandssongliu 08388124b3 feat: Migrate to MinIO SDK and enhance S3 client with TLS and context (#12207)
* chore: update dependencies and migrate S3 client to MinIO SDK

- Removed AWS SDK dependency and replaced S3 client implementation with MinIO SDK.
- Updated various dependencies in go.mod and go.sum, including version upgrades for `klauspost/compress`, `minio/minio-go`, and `rs/xid`.
- Adjusted S3 client methods to utilize MinIO's API for bucket listing, object existence checks, uploads, downloads, and deletions.

* refactor: enhance S3 client with context management and TLS support

- Introduced context management with timeouts for S3 client operations to improve reliability.
- Added TLS configuration to support secure connections based on the endpoint scheme.
- Updated S3 client methods to utilize the new context and transport settings for enhanced performance and security.

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-04-15 10:00:08 +08:00
王贺 34ccc68b3a fix: clear firewall rule description (#12491)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12488

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:44:46 +00:00
王贺 97ef386f17 fix: correct tamper english i18n (#12490)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12482
#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:42:45 +00:00
王贺 394339d7dc feat: support mongodb management (#12486)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/7759

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:40:44 +00:00
蘭 8362578355 fix: Resolve the issue of QR code sharing files not being able to download (#12485) 2026-04-14 07:12:44 +00:00
CityFun 6dc218cf80 perf: Optimize the frontend architecture. (#12479) 2026-04-13 18:36:28 +08:00
CityFun 89888cef73 feat: Optimize frontend structure (#12475) 2026-04-13 05:48:42 +00:00
ssongliu 4d06f444ac refactor: restore symlink-based xpack integration (#12473) 2026-04-13 10:34:59 +08:00
488 changed files with 20818 additions and 4679 deletions
+14
View File
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "gomod"
directories:
- "/agent"
- "/core"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "daily"
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+142
View File
@@ -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
+15
View File
@@ -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)
+294
View File
@@ -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)
}
+9 -7
View File
@@ -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()
+20
View File
@@ -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
+91
View File
@@ -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)
}
+36
View File
@@ -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
+11 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+78 -1
View File
@@ -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"`
+1
View File
@@ -7,6 +7,7 @@ import (
type SearchTaskLogReq struct {
Status string `json:"status"`
Type string `json:"type"`
TaskID string `json:"taskID"`
PageInfo
}
+5
View File
@@ -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 {
+52
View File
@@ -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
View File
@@ -1 +0,0 @@
package request
+31
View File
@@ -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"`
}
+12
View File
@@ -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"`
}
+19
View File
@@ -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
View File
@@ -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
}
+113
View File
@@ -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)
}
}
+121
View File
@@ -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)
}
}
+3
View File
@@ -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
View File
@@ -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
+276 -29
View File
@@ -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,
+652
View File
@@ -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
}
+591
View File
@@ -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,
)
}
+126
View File
@@ -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)
}
+215
View File
@@ -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
}
+55 -3
View File
@@ -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 {
+8 -11
View File
@@ -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)
}
+25 -2
View File
@@ -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
}
+2 -1
View File
@@ -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 {
+2 -2
View File
@@ -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
}
}
+35 -4
View File
@@ -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), &currentCompose); 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 {
+3
View File
@@ -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
+588
View File
@@ -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)
}
}
+9
View File
@@ -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))
+46 -1
View File
@@ -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{
+47 -1
View File
@@ -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",
+948
View File
@@ -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")
}
+1
View File
@@ -21,6 +21,7 @@ var (
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo()
imageRepoRepo = repo.NewIImageRepoRepo()
+201 -14
View File
@@ -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)
+750
View File
@@ -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[:])
}
+147
View File
@@ -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
}
+11 -5
View File
@@ -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
}
}
}
+338 -39
View File
@@ -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 {
+12
View File
@@ -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)
+3
View File
@@ -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))
}
+11
View File
@@ -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
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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))
+1
View File
@@ -3,6 +3,7 @@ base:
mode: dev
is_demo: false
is_offline: false
is_fxplay: false
log:
level: debug
+1
View File
@@ -9,6 +9,7 @@ const (
AppOpenresty = "openresty"
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppHermesAgent = "hermes-agent"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
+1
View File
@@ -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"`
+9
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -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'
+3
View File
@@ -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 }}'
+3
View File
@@ -59,6 +59,9 @@ ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
ErrAgentWebsiteUnbindUnsupported: 'ワンクリックデプロイのサイトは手動で関連解除できません'
ErrHermesPairingCodeUnavailable: 'Hermes でペアリングコードが一時的に見つかりません。ネットワーク要因の可能性があるため、しばらくしてから再試行してください。'
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu のグループポリシーが許可リストの場合、DM ポリシーをペアリングコードにはできません。'
Localhost: 'ローカルマシン'
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
+3
View File
@@ -59,6 +59,9 @@ ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
ErrAgentWebsiteUnbindUnsupported: '원클릭 배포 웹사이트는 수동으로 연결 해제할 수 없습니다'
ErrHermesPairingCodeUnavailable: 'Hermes에서 페어링 코드가 일시적으로 존재하지 않습니다. 네트워크 문제일 수 있으니 잠시 후 다시 시도해 주세요.'
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu 그룹 정책이 허용 목록이면 DM 정책을 페어링 코드로 설정할 수 없습니다.'
Localhost: '로컬 머신'
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
+3
View File
@@ -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 }}'
+3
View File
@@ -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 }}'
+3
View File
@@ -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 }}'
+3
View File
@@ -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 }}'
+3
View File
@@ -59,6 +59,9 @@ ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個 AI 智能體
ErrAgentWebsiteBound: '該智能體已關聯網站'
ErrAgentWebsiteTypeUnsupported: '只能關聯反向代理或靜態網站'
ErrAgentWebsiteInUse: '該網站已被其他智能體關聯'
ErrAgentWebsiteUnbindUnsupported: '一鍵部署網站不支援手動解綁'
ErrHermesPairingCodeUnavailable: '配對碼在 Hermes 中暫時不存在,可能是由於網路原因,請稍後再試'
ErrHermesFeishuGroupAllowlistRequiresAllowlist: '飛書群組策略為白名單時,私聊策略不能為配對碼'
Localhost: '本機'
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
+3
View File
@@ -64,6 +64,9 @@ ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升
ErrAgentWebsiteBound: "该智能体已关联网站"
ErrAgentWebsiteTypeUnsupported: "只能关联反向代理或静态网站"
ErrAgentWebsiteInUse: "该网站已被其他智能体关联"
ErrAgentWebsiteUnbindUnsupported: "一键部署网站不支持手动解绑"
ErrHermesPairingCodeUnavailable: "配对码在 hermes 中暂时不存在,可能是由于网络原因,请稍后尝试"
ErrHermesFeishuGroupAllowlistRequiresAllowlist: "飞书群组策略为白名单时,私聊策略不能为配对码"
#backup
Localhost: '本机'
+2
View File
@@ -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)
+35
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
//go:build !xpack
//go:build !xpack && !xpackee
package router
+19
View File
@@ -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()
+7
View File
@@ -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)
}
}
+12
View File
@@ -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)
}
}
+5
View File
@@ -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)
+2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
//go:build !xpack
//go:build !xpack && !xpackee
package server
+11
View File
@@ -0,0 +1,11 @@
//go:build xpack
package server
import (
xpack "github.com/1Panel-dev/1Panel/agent/xpack"
)
func InitOthers() {
xpack.Init()
}
+91 -71
View File
@@ -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
}
+39 -2
View File
@@ -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")
}
}
+88 -19
View File
@@ -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 {
+3 -2
View File
@@ -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
}
+3 -1
View File
@@ -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
}
+22 -4
View File
@@ -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
+3 -2
View File
@@ -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
}
+3 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
//go:build !xpack
//go:build !xpack && !xpackee
package xpack
+74
View File
@@ -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()
}
+2
View File
@@ -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": {},
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+10
View File
@@ -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"
},
+2 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
//go:build !xpack
//go:build !xpack && !xpackee
package router
+19
View File
@@ -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