Compare commits

...
98 Commits
Author SHA1 Message Date
CityFun 6e052b8ef2 fix: Fixed issue with openclaw skill page return empty (#12347) 2026-03-26 17:57:07 +08:00
ssongliu 1d40bdc11b fix: resolve menu item wrapping issue (#12346) 2026-03-26 09:16:24 +00:00
CityFun 3b62197f29 fix: Fixed issue with openclaw skill page return empty (#12345) 2026-03-26 09:10:24 +00:00
CityFun 2d78ca5a86 fix: Fixed issue with agent acount model name is empty (#12343) 2026-03-26 05:44:27 +00:00
KOMATA a9b65b7ce5 feat: add support for trusted SSL certificates in website proxy configuration (#12342) 2026-03-26 13:42:11 +08:00
wanghe-fit2cloud 291b40a45d chore: fix ru locale formatting 2026-03-26 11:08:36 +08:00
CityFun a959da7d44 feat: Optimize the agent's decision-making logic (#12341) 2026-03-26 10:57:48 +08:00
ssongliu 26c4e8323f fix: update quick command batch separator (#12340) 2026-03-26 02:46:10 +00:00
王贺 0534badce7 fix: unify vLLM copy casing (#12339) 2026-03-26 10:32:36 +08:00
CityFun 594d818db0 style: change openclaw style (#12336) 2026-03-25 10:58:17 +00:00
ssongliu 04dceb93a7 fix: unify terminal scroll sensitivity defaults (#12334)
Refs #11911
2026-03-25 09:34:18 +00:00
CityFun 7b554c0d61 feat: Add skill hub for openclaw (#12333) 2026-03-25 09:32:18 +00:00
ssongliu bb642cd8a8 feat: improve terminal quick command workflow (#12331) 2026-03-25 08:56:18 +00:00
CityFun 40249b527d feat: add version control for openclaw config menu (#12329) 2026-03-25 08:54:18 +00:00
蘭 74ff7ddc19 feat: Upload multiple files and add overall and current file upload progress messages (#12327)
https://github.com/1Panel-dev/1Panel/issues/10593
2026-03-25 07:30:19 +00:00
ssongliu 096ec8edc7 chore: refine terminal ai prefix handling (#12328) 2026-03-25 07:28:22 +00:00
ssongliu 0d19069059 feat: improve terminal ai command interception (#12326) 2026-03-25 06:04:18 +00:00
CityFun 0da833302b feat: change openclaw upgrade logic (#12325)
#### 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-03-25 06:02:17 +00:00
ssongliu 4a69cf0ff5 fix: migrate ai agents menu title 2026-03-24 19:54:38 +08:00
CityFun 39704ef4e5 feat: Add openclaw overview button (#12319) 2026-03-24 10:42:05 +00:00
王贺 e52cfeeb78 fix: sync fit2cloud table locale on first load (#12316) 2026-03-24 18:37:17 +08:00
蘭 476429005c ref: Change tooltip placement from top to left for better visibility (#12317)
#11922
2026-03-24 10:26:05 +00:00
ssongliu 6fb41dc711 feat: refine terminal ai assistant settings (#12315) 2026-03-24 09:10:07 +00:00
CityFun 2cb2058cfa feat: support edit config file (#12313) 2026-03-24 09:08:07 +00:00
蘭 8533dd1d33 fix: Fix the issue where the gz file cannot be previewed after decompression (#12314) 2026-03-24 09:06:06 +00:00
CityFun 0cfbcd25fc feat: Optimize plugin installation logic (#12312) 2026-03-24 08:04:05 +00:00
CityFun b91ca8bdb3 feat: add openclaw skill management menu (#12311) 2026-03-24 05:48:06 +00:00
ssongliu 72764863bc fix: unify terminal ai validation and loading state (#12310) 2026-03-24 05:46:06 +00:00
蘭 a7b9fd0181 feat: add validation and error messages for wget URL input (#12309) 2026-03-24 05:44:07 +00:00
ssongliu 2b3164fe82 chore: format terminal ai i18n entries (#12303) 2026-03-23 17:26:50 +08:00
ssongliu 2c6307049a fix: handle upgrade clean leaf selection (#12302) 2026-03-23 17:15:50 +08:00
CityFun acc1b9270f feat: add npm registry config for openclaw (#12300) 2026-03-23 09:12:03 +00:00
王贺 60e4143525 Format aiSummary string for better readability (#12301) 2026-03-23 16:48:09 +08:00
KOMATA 99bcd6d059 feat: add SSL verification option to website proxy configuration (#12258)
* feat: add SSL verification option to website proxy configuration

* localization: update proxy SSL verification messages in multiple languages
2026-03-23 16:32:27 +08:00
ssongliu c4259dd004 feat: warn when compose file is missing (#12299) 2026-03-23 08:22:03 +00:00
ssongliu 1c5a4bc978 feat: improve container file browser path jump (#12293)
Refs #12282
2026-03-23 08:20:04 +00:00
CityFun 42f6e9a9a6 feat: add wenxin channel for openclaw (#12298) 2026-03-23 07:54:03 +00:00
蘭 569363530a fix: Fix the issue of preserving archive modification time during the decompression process of gz files (#12297) 2026-03-23 07:52:04 +00:00
蘭 ec9eba0b9a feat: enhance tab management and file handling in editor (#12294)
#12098
2026-03-23 07:50:03 +00:00
ssongliu 5376d54089 feat: refine terminal ai settings and safeguards (#12289) 2026-03-23 07:48:04 +00:00
KOMATA c10e5747b0 fix: trim whitespace from command in ExecComposer method (#12290) 2026-03-23 15:47:31 +08:00
jakub961241 5a80931a49 fix: auto theme does not switch when OS changes day/night mode (#6813) (#12278)
When theme is set to "auto" (follow system), changing the OS from
light to dark (or vice versa) without reloading the page had no
effect. The theme was only evaluated once on page load.

Added a matchMedia 'change' event listener that calls switchTheme()
whenever the OS color scheme changes, but only when theme is "auto".
Listener is properly cleaned up on component unmount.

Fixes #6813 (point 1)
2026-03-23 14:57:15 +08:00
jakub961241 f541379daa fix: open container directory at WorkingDir instead of root (#12265)
When clicking "Directory" on a container, the file browser always
opened at `/` (root). Now it inspects the container first and opens
at the container's configured WorkingDir (e.g., `/app`, `/var/www`).

Falls back to `/` if WorkingDir is not set or inspect fails.

Fixes #12255
2026-03-23 14:52:57 +08:00
王贺 0018335d64 fix: unify sidebar menu item heights (#12253) 2026-03-20 18:32:17 +08:00
CityFun df90286253 feat: optimize the validation logic for model accounts (#12251) 2026-03-20 09:55:54 +00:00
ssongliu 8dfa7945d7 feat: add xpack node multi overview entry (#12248)
Refs #11735
2026-03-20 09:53:54 +00:00
王贺 1f4292d9cf fix: add missing ssl column controls (#12252) 2026-03-20 17:41:50 +08:00
蘭 e635a7beba feat: add stop functionality for wget file downloads (#12245)
#9791
2026-03-20 09:05:54 +00:00
CityFun 59c657e403 feat: agent model account support xiaomi (#12242)
https://github.com/1Panel-dev/1Panel/issues/12220
2026-03-20 07:27:55 +00:00
ssongliu ff8f79ed52 fix: align compose partial status actions (#12239)
Refs #12232
2026-03-20 06:51:54 +00:00
CityFun 57a7edf481 feat: improve Minimax validation (#12240) 2026-03-20 06:39:56 +00:00
CityFun bbd33c66f1 fix: fix the issue where the website fails to start after being stopped (#12241) 2026-03-20 14:38:18 +08:00
ssongliu b05ed1a0ca fix: refine sidebar node dropdown style (#12238)
Refs #12231
2026-03-20 03:21:54 +00:00
蘭 b645c4edb4 feat: enhance file operations to preserve file attributes during copy (#12237)
#11542
2026-03-20 03:19:54 +00:00
CityFun 9f61aac8be feat: improve the OpenClaw terminal (#12230) 2026-03-19 18:26:37 +08:00
CityFun 11d78644ee feat: add dingTalk channel for openclaw (#12229) 2026-03-19 09:55:44 +00:00
蘭 b96428e1d6 feat: Extract the file and retain the original file modification time (#12228)
#11542
2026-03-19 09:53:47 +00:00
蘭 6432c35b1a feat: File Selection Add Favorite Pop Up Window for Quick Access to Favorite Files (#12227)
#11326
2026-03-19 09:53:14 +00:00
CityFun fa843f0940 feat: improve agent module logic (#12226)
* feat: openclaw supports configuring multiple models

* feat: change openclaw bind type

* feat: change openclaw bind type

* feat: improve agent module logic
2026-03-19 17:00:15 +08:00
ssongliu 5c7ffc561b chore: upgrade agent docker dependencies (#12224) 2026-03-19 07:27:20 +00:00
ssongliu c2f0c8c6ac feat: add ai terminal settings (#12215) 2026-03-19 07:25:19 +00:00
ssongliu 5bf6d17451 fix: handle mfa and entrance auth errors (#12222) 2026-03-19 15:11:37 +08:00
王贺 7f799004c3 fix: fix openresty logs (#12219) 2026-03-19 14:31:54 +08:00
CityFun c68251cada feat: openclaw supports configuring multiple models (#12213) 2026-03-18 10:36:11 +00:00
王贺 23e7d03d8d fix: missing form fields in es/pt (#12211) 2026-03-18 18:32:28 +08:00
王贺 5085db7a2f chore: upgrade qiniu sdk to fix kodo hash mismatch (#12214) 2026-03-18 18:31:51 +08:00
CityFun a9cf7a7d1d feat: Optimize the logic of the OpenClaw Caddyfile (#12209)
* feat: Optimize the logic of the OpenClaw Caddyfile

* feat: Optimize the logic of the OpenClaw Caddyfile
2026-03-18 15:38:01 +08:00
CityFun cf18995182 fix: Fix the issue preventing the successful creation of the OpenClaw deployment website. (#12206)
* fix: Fix the issue preventing the successful creation of the OpenClaw  deployment website.

* fix: change update app_install port logic
2026-03-18 15:37:48 +08:00
CityFun 24d7dc0786 fix: Fixed the issue where the OpenClaw access address could not be set to a domain name. (#12201) 2026-03-17 09:55:21 +00:00
CityFun 142c9b9a71 fix: Fixed issue with build backend failed (#12199) 2026-03-17 14:50:45 +08:00
ssongliu f3c7603f6e fix: respect region-based release doc source (#12198) 2026-03-17 14:33:11 +08:00
ssongliuandssongliu db26da1700 chore: format english proxy helper text (#12197)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-17 14:06:20 +08:00
ssongliuandssongliu d5ff59f4e7 fix: remove unused compose backup status param (#12196)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-17 13:56:34 +08:00
王贺 85b88db99e docs: refine Docker proxy desc (#12195) 2026-03-17 11:15:27 +08:00
ssongliuandssongliu f7053934b5 fix: improve compose backup and container restore handling (#12192)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 22:39:34 +08:00
wangdage12andfanbook-wangdage bf1339bb40 fix: show loading state when confirming deletion of multiple folders and reduce unnecessary basedir requests
Co-authored-by: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com>
2026-03-16 19:07:19 +08:00
CityFun 43c5e62c23 feat: change allowedOrigin param for openclaw upgrade (#12188) 2026-03-16 10:53:50 +00:00
ssongliuandssongliu 28634ce631 fix(middleware): preserve numeric format in operation logs (#12173) (#12185)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 17:33:07 +08:00
ssongliuandssongliu 72263dfdda fix(container): allow backup for stopped containers (#12184)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 16:39:16 +08:00
ssongliuandssongliu 479d36e25f fix(log): localize upgrade and rebuild operation labels (#12183)
Co-authored-by: ssongliu <songlius11@163.com>
2026-03-16 16:25:34 +08:00
ssongliu 2f3ad3537e feat: add docs source config and runtime env linkage (#12182) 2026-03-16 08:13:51 +00:00
CityFun 2dfe5096f0 feat: Optimize reverse proxy handling for applications with HTTPS ports. (#12181) 2026-03-16 08:07:51 +00:00
CityFun 4375722aef feat: add checkOnStart config for openclaw (#12178) 2026-03-16 06:45:49 +00:00
CityFun bde4d06842 fix: Fixed issue with build source code failed (#12177)
Refs https://github.com/1Panel-dev/1Panel/issues/12176
2026-03-16 05:51:50 +00:00
CityFun 17a486b2c4 feat: Modify the OpenClaw upgrade logic. (#12175) 2026-03-16 04:11:50 +00:00
CityFun 8499c09571 feat: Add OpenClaw security adaptation (#12168) 2026-03-13 18:41:44 +08:00
CityFun 7cb70f785a feat: change openclaw port jump logic (#12164) 2026-03-12 14:27:34 +00:00
CityFun f3781320de feat: add icon for agent (#12163) 2026-03-12 18:52:04 +08:00
CityFun 869ee75020 feat: Add allowedOrigins config for openclaw (#12162) 2026-03-12 09:37:33 +00:00
ssongliu 1d5a26373d chore: optimize lint and cleanup warnings (#12161) 2026-03-12 09:35:33 +00:00
ssongliu 176fd0f8ad feat: support file operations inside containers (#12160)
Refs #523
2026-03-12 09:33:33 +00:00
ssongliu 341a55ae63 feat: add Agent quick jump with priority show strategy (#12159)
Refs #12158
2026-03-12 09:31:33 +00:00
CityFun fb87cf545f feat: Modify the Gemini account verification logic (#12155)
Refs https://github.com/1Panel-dev/1Panel/issues/12149
2026-03-12 09:07:33 +00:00
ssongliu 6db2a9352e feat: refactor image update flow and align i18n messages (#12154)
Refs #10786
2026-03-12 09:05:34 +00:00
ssongliu 21826bcc1a feat: complete backup and recovery for containers and compose (#12153)
Refs #9656
2026-03-12 09:03:34 +00:00
ssongliu eedbc2e95c feat: load registry auth from docker cli config when pulling image (#12152)
Refs #10460
2026-03-12 09:01:33 +00:00
CityFun 46763df7bb fix: Fixed issue with ai menu change failed (#12147) 2026-03-11 10:55:27 +00:00
CityFun 175162753e feat: openclaw channel support wecom (#12146) 2026-03-11 10:53:27 +00:00
222 changed files with 17446 additions and 4412 deletions
+3 -1
View File
@@ -73,4 +73,6 @@ agent/.golangci.yml
openspec
CLAUDE.md
AGENTS.md
opencode.json
opencode.json
superpowers
.worktrees/
+333 -26
View File
@@ -111,6 +111,27 @@ func (b *BaseApi) UpdateAgentModelConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent overview
// @Accept json
// @Param request body dto.AgentOverviewReq true "request"
// @Success 200 {object} dto.AgentOverview
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/overview [post]
func (b *BaseApi) GetAgentOverview(c *gin.Context) {
var req dto.AgentOverviewReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.GetOverview(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Get Providers
// @Success 200 {array} dto.ProviderInfo
@@ -190,6 +211,87 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
})
}
// @Tags AI
// @Summary List Agent account models
// @Accept json
// @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models [post]
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.GetAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Create Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/create [post]
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.CreateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Update Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/update [post]
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Delete Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/delete [post]
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.DeleteAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Verify Agent account
// @Accept json
@@ -315,13 +417,13 @@ func (b *BaseApi) UpdateAgentTelegramConfig(c *gin.Context) {
// @Tags AI
// @Summary Get Agent Discord channel config
// @Accept json
// @Param request body dto.AgentDiscordConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentDiscordConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/discord/get [post]
func (b *BaseApi) GetAgentDiscordConfig(c *gin.Context) {
var req dto.AgentDiscordConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -356,13 +458,95 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) {
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
// @Param request body dto.AgentQQBotConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentWecomConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/wecom/get [post]
func (b *BaseApi) GetAgentWecomConfig(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetWecomConfig(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent WeCom channel config
// @Accept json
// @Param request body dto.AgentWecomConfigUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/wecom/update [post]
func (b *BaseApi) UpdateAgentWecomConfig(c *gin.Context) {
var req dto.AgentWecomConfigUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateWecomConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent DingTalk channel config
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentDingTalkConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/dingtalk/get [post]
func (b *BaseApi) GetAgentDingTalkConfig(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetDingTalkConfig(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent DingTalk channel config
// @Accept json
// @Param request body dto.AgentDingTalkConfigUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/dingtalk/update [post]
func (b *BaseApi) UpdateAgentDingTalkConfig(c *gin.Context) {
var req dto.AgentDingTalkConfigUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateDingTalkConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentQQBotConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/qqbot/get [post]
func (b *BaseApi) GetAgentQQBotConfig(c *gin.Context) {
var req dto.AgentQQBotConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -436,19 +620,19 @@ func (b *BaseApi) CheckAgentPlugin(c *gin.Context) {
}
// @Tags AI
// @Summary Get Agent Browser config
// @Summary Get Agent Security config
// @Accept json
// @Param request body dto.AgentBrowserConfigReq true "request"
// @Success 200 {object} dto.AgentBrowserConfig
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentSecurityConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/browser/get [post]
func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
var req dto.AgentBrowserConfigReq
// @Router /ai/agents/security/get [post]
func (b *BaseApi) GetAgentSecurityConfig(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetBrowserConfig(req)
data, err := agentService.GetSecurityConfig(req)
if err != nil {
helper.BadRequest(c, err)
return
@@ -457,19 +641,19 @@ func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
}
// @Tags AI
// @Summary Update Agent Browser config
// @Summary Update Agent Security config
// @Accept json
// @Param request body dto.AgentBrowserConfigUpdateReq true "request"
// @Param request body dto.AgentSecurityConfigUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/browser/update [post]
func (b *BaseApi) UpdateAgentBrowserConfig(c *gin.Context) {
var req dto.AgentBrowserConfigUpdateReq
// @Router /ai/agents/security/update [post]
func (b *BaseApi) UpdateAgentSecurityConfig(c *gin.Context) {
var req dto.AgentSecurityConfigUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateBrowserConfig(req); err != nil {
if err := agentService.UpdateSecurityConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
@@ -479,13 +663,13 @@ func (b *BaseApi) UpdateAgentBrowserConfig(c *gin.Context) {
// @Tags AI
// @Summary Get Agent Other config
// @Accept json
// @Param request body dto.AgentOtherConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentOtherConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/other/get [post]
func (b *BaseApi) GetAgentOtherConfig(c *gin.Context) {
var req dto.AgentOtherConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -518,19 +702,142 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
}
// @Tags AI
// @Summary Approve Agent Feishu pairing code
// @Summary Get Agent config file
// @Accept json
// @Param request body dto.AgentFeishuPairingApproveReq true "request"
// @Success 200
// @Param request body dto.AgentConfigFileReq true "request"
// @Success 200 {object} dto.AgentConfigFile
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/feishu/approve [post]
func (b *BaseApi) ApproveAgentFeishuPairing(c *gin.Context) {
var req dto.AgentFeishuPairingApproveReq
// @Router /ai/agents/config-file/get [post]
func (b *BaseApi) GetAgentConfigFile(c *gin.Context) {
var req dto.AgentConfigFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.ApproveFeishuPairing(req); err != nil {
data, err := agentService.GetConfigFile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent config file
// @Accept json
// @Param request body dto.AgentConfigFileUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/config-file/update [post]
func (b *BaseApi) UpdateAgentConfigFile(c *gin.Context) {
var req dto.AgentConfigFileUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateConfigFile(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary List Agent skills
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {array} dto.AgentSkillItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/list [post]
func (b *BaseApi) ListAgentSkills(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.ListSkills(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Search Agent skills
// @Accept json
// @Param request body dto.AgentSkillSearchReq true "request"
// @Success 200 {array} dto.AgentSkillSearchItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/search [post]
func (b *BaseApi) SearchAgentSkills(c *gin.Context) {
var req dto.AgentSkillSearchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.SearchSkills(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent skill status
// @Accept json
// @Param request body dto.AgentSkillUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/update [post]
func (b *BaseApi) UpdateAgentSkill(c *gin.Context) {
var req dto.AgentSkillUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateSkill(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Install Agent skill
// @Accept json
// @Param request body dto.AgentSkillInstallReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/install [post]
func (b *BaseApi) InstallAgentSkill(c *gin.Context) {
var req dto.AgentSkillInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.InstallSkill(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Login Agent Weixin channel
// @Accept json
// @Param request body dto.AgentWeixinLoginReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/weixin/login [post]
func (b *BaseApi) LoginAgentWeixinChannel(c *gin.Context) {
var req dto.AgentWeixinLoginReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.LoginWeixinChannel(req); err != nil {
helper.BadRequest(c, err)
return
}
+30
View File
@@ -430,6 +430,16 @@ func (b *BaseApi) Backup(c *gin.Context) {
helper.InternalServer(c, err)
return
}
case "container":
if err := backupService.ContainerBackup(req); err != nil {
helper.InternalServer(c, err)
return
}
case "compose":
if err := backupService.ComposeBackup(req); err != nil {
helper.InternalServer(c, err)
return
}
}
helper.Success(c)
}
@@ -485,6 +495,16 @@ func (b *BaseApi) Recover(c *gin.Context) {
helper.InternalServer(c, err)
return
}
case "container":
if err := backupService.ContainerRecover(req); err != nil {
helper.InternalServer(c, err)
return
}
case "compose":
if err := backupService.ComposeRecover(req); err != nil {
helper.InternalServer(c, err)
return
}
}
helper.Success(c)
}
@@ -525,6 +545,16 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
helper.InternalServer(c, err)
return
}
case "container":
if err := backupService.ContainerRecover(req); err != nil {
helper.InternalServer(c, err)
return
}
case "compose":
if err := backupService.ComposeRecover(req); err != nil {
helper.InternalServer(c, err)
return
}
}
helper.Success(c)
}
+159
View File
@@ -1,6 +1,9 @@
package v2
import (
"net/http"
"net/url"
"path"
"strconv"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
@@ -53,6 +56,162 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) {
helper.SuccessWithData(c, containerService.LoadUsers(req))
}
// @Tags Container
// @Summary List container files
// @Accept json
// @Param request body dto.ContainerFileReq true "request"
// @Success 200 {array} dto.ContainerFileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/search [post]
func (b *BaseApi) ListContainerFiles(c *gin.Context) {
var req dto.ContainerFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
files, err := containerService.ListContainerFiles(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, files)
}
// @Tags Container
// @Summary Upload container file
// @Accept multipart/form-data
// @Param containerID formData string true "containerID"
// @Param path formData string true "path"
// @Param file formData file true "file"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/upload [post]
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器 [containerID] 上传文件到 [path]","formatEN":"Upload file to [path] in container [containerID]"}
func (b *BaseApi) UploadContainerFile(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
helper.BadRequest(c, err)
return
}
containerIDs := form.Value["containerID"]
paths := form.Value["path"]
uploadFiles := form.File["file"]
if len(containerIDs) == 0 || len(paths) == 0 || len(uploadFiles) == 0 {
helper.BadRequest(c, errors.New("invalid container file upload params"))
return
}
req := dto.ContainerFileReq{
ContainerID: containerIDs[0],
Path: paths[0],
}
for _, uploadFile := range uploadFiles {
file, err := uploadFile.Open()
if err != nil {
helper.InternalServer(c, err)
return
}
err = containerService.UploadContainerFile(req, path.Base(uploadFile.Filename), uploadFile.Size, file)
_ = file.Close()
if err != nil {
helper.InternalServer(c, err)
return
}
}
helper.Success(c)
}
// @Tags Container
// @Summary Get container file content
// @Accept json
// @Param request body dto.ContainerFileReq true "request"
// @Success 200 {object} dto.ContainerFileContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/content [post]
func (b *BaseApi) GetContainerFileContent(c *gin.Context) {
var req dto.ContainerFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := containerService.GetContainerFileContent(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, content)
}
// @Tags Container
// @Summary Get container file size
// @Accept json
// @Param request body dto.ContainerFileReq true "request"
// @Success 200 {int} size
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/size [post]
func (b *BaseApi) GetContainerFileSize(c *gin.Context) {
var req dto.ContainerFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
size, err := containerService.GetContainerFileSize(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, size)
}
// @Tags Container
// @Summary Delete container file
// @Accept json
// @Param request body dto.ContainerFileBatchDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/del [post]
// @x-panel-log {"bodyKeys":["containerID","paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器 [containerID] 文件 [paths]","formatEN":"Delete files [paths] in container [containerID]"}
func (b *BaseApi) DeleteContainerFile(c *gin.Context) {
var req dto.ContainerFileBatchDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.DeleteContainerFile(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Container
// @Summary Download container file
// @Accept json
// @Param request body dto.ContainerFileReq true "request"
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/files/download [post]
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载容器 [containerID] 文件 [path]","formatEN":"Download file [path] from container [containerID]"}
func (b *BaseApi) DownloadContainerFile(c *gin.Context) {
var req dto.ContainerFileReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.BadRequest(c, err)
return
}
if req.ContainerID == "" || req.Path == "" {
helper.BadRequest(c, errors.New("invalid container file download params"))
return
}
reader, fileName, contentType, err := containerService.DownloadContainerFile(req)
if err != nil {
helper.InternalServer(c, err)
return
}
defer reader.Close()
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
c.DataFromReader(http.StatusOK, -1, contentType, reader, nil)
}
// @Tags Container
// @Summary List containers
// @Accept json
+1
View File
@@ -70,6 +70,7 @@ var (
recycleBinService = service.NewIRecycleBinService()
favoriteService = service.NewIFavoriteService()
hostService = service.NewIHostService()
websiteCAService = service.NewIWebsiteCAService()
taskService = service.NewITaskService()
+23
View File
@@ -515,6 +515,29 @@ func (b *BaseApi) WgetFile(c *gin.Context) {
})
}
// @Tags File
// @Summary Stop wget file download
// @Accept json
// @Param request body request.FileProcessReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/stop [post]
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"停止下载任务 [key]","formatEN":"Stop wget task [key]"}
func (b *BaseApi) StopWget(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if strings.TrimSpace(req.Key) == "" {
helper.BadRequest(c, errors.New("key is required"))
return
}
files.CancelDownload(req.Key)
helper.Success(c)
}
// @Tags File
// @Summary Move file
// @Accept json
+167
View File
@@ -0,0 +1,167 @@
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/utils/encrypt"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
host, err := hostService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, host)
}
func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, hostService.TestByInfo(req))
}
func (b *BaseApi) TestByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, hostService.TestLocalConn(req.ID))
}
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := hostService.SearchForTree(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := hostService.SearchWithPage(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Items: list, Total: total})
}
func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := hostService.Delete(req.IDs); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var err error
if len(req.Password) != 0 && req.AuthMode == "password" {
req.Password, err = hostService.EncryptHost(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.PrivateKey = ""
req.PassPhrase = ""
}
if len(req.PrivateKey) != 0 && req.AuthMode == "key" {
req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey)
if err != nil {
helper.BadRequest(c, err)
return
}
if len(req.PassPhrase) != 0 {
req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase)
if err != nil {
helper.BadRequest(c, err)
return
}
}
req.Password = ""
}
upMap := map[string]interface{}{
"name": req.Name,
"group_id": req.GroupID,
"addr": req.Addr,
"port": req.Port,
"user": req.User,
"auth_mode": req.AuthMode,
"remember_password": req.RememberPassword,
"description": req.Description,
}
if req.AuthMode == "password" {
upMap["password"] = req.Password
upMap["private_key"] = ""
upMap["pass_phrase"] = ""
} else {
upMap["password"] = ""
upMap["private_key"] = req.PrivateKey
upMap["pass_phrase"] = req.PassPhrase
}
hostItem, err := hostService.Update(req.ID, upMap)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, hostItem)
}
func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if _, err := hostService.Update(req.ID, map[string]interface{}{"group_id": req.GroupID}); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
info, err := hostService.GetHostByID(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, info)
}
+22
View File
@@ -27,6 +27,15 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting)
}
func (b *BaseApi) GetTerminalAISettingInfo(c *gin.Context) {
setting, err := settingService.GetTerminalAIInfo()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, setting)
}
// @Tags System Setting
// @Summary Load system available status
// @Success 200
@@ -59,6 +68,19 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
helper.Success(c)
}
func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
var req dto.TerminalAIInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := settingService.UpdateTerminalAI(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags System Setting
// @Summary Load local backup dir
// @Success 200 {string} path
+29 -3
View File
@@ -9,8 +9,10 @@ import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/terminal"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
@@ -40,9 +42,33 @@ func (b *BaseApi) WsSSH(c *gin.Context) {
return
}
client, err := loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
var client *ssh.SSHClient
if hostID > 0 {
host, err := service.GetHostInfo(uint(hostID))
if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) {
return
}
connInfo := ssh.ConnInfo{
Addr: host.Addr,
Port: int(host.Port),
User: host.User,
AuthMode: host.AuthMode,
Password: host.Password,
PrivateKey: []byte(host.PrivateKey),
}
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
client, err = ssh.NewClient(connInfo)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
} else {
client, err = loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
}
defer client.Close()
command := c.DefaultQuery("command", "")
+205 -88
View File
@@ -3,32 +3,27 @@ package dto
import "time"
type AgentCreateReq struct {
Name string `json:"name" validate:"required"`
AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required"`
BridgePort int `json:"bridgePort"`
AgentType string `json:"agentType"`
Provider string `json:"provider"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
AccountID uint `json:"accountId"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"`
Token string `json:"token"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
AllowPort bool `json:"allowPort"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy"`
CpuQuota float64 `json:"cpuQuota"`
MemoryLimit float64 `json:"memoryLimit"`
MemoryUnit string `json:"memoryUnit"`
PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
Name string `json:"name" validate:"required"`
AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
AllowPort bool `json:"allowPort"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy"`
CpuQuota float64 `json:"cpuQuota"`
MemoryLimit float64 `json:"memoryLimit"`
MemoryUnit string `json:"memoryUnit"`
PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
}
type AgentItem struct {
@@ -74,17 +69,66 @@ type AgentModelConfigUpdateReq struct {
Model string `json:"model" validate:"required"`
}
type AgentOverviewReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentIDReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentOverview struct {
Snapshot AgentOverviewSnapshot `json:"snapshot"`
}
type AgentOverviewSnapshot struct {
ContainerStatus string `json:"containerStatus"`
AppVersion string `json:"appVersion"`
DefaultModel string `json:"defaultModel"`
ChannelCount int `json:"channelCount"`
SkillCount int `json:"skillCount"`
JobCount int `json:"jobCount"`
SessionCount int `json:"sessionCount"`
}
type AgentAccountModel struct {
RecordID uint `json:"recordId"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"`
}
type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
}
type AgentAccountModelUpdateReq struct {
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
}
type AgentAccountModelDeleteReq struct {
AccountID uint `json:"accountId" validate:"required"`
RecordID uint `json:"recordId" validate:"required"`
}
type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType" validate:"required"`
Remark string `json:"remark"`
}
type AgentAccountUpdateReq struct {
@@ -93,10 +137,7 @@ type AgentAccountUpdateReq struct {
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
APIType string `json:"apiType" validate:"required"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
}
@@ -118,25 +159,27 @@ type AgentAccountSearch struct {
}
type AgentAccountInfo struct {
ID uint `json:"id"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
}
type ProviderModelInfo struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type ProviderInfo struct {
@@ -194,12 +237,51 @@ type AgentTelegramConfig struct {
type AgentChannelPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom dingtalk-connector"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentQQBotConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
type AgentWecomConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open"`
BotID string `json:"botId" validate:"required"`
Secret string `json:"secret" validate:"required"`
}
type AgentWecomConfig struct {
Enabled bool `json:"enabled"`
DmPolicy string `json:"dmPolicy"`
BotID string `json:"botId"`
Secret string `json:"secret"`
Installed bool `json:"installed"`
}
type AgentDingTalkConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
ClientID string `json:"clientId" validate:"required"`
ClientSecret string `json:"clientSecret" validate:"required"`
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"`
}
type AgentDingTalkConfig struct {
Enabled bool `json:"enabled"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
DmPolicy string `json:"dmPolicy"`
AllowFrom []string `json:"allowFrom"`
GroupPolicy string `json:"groupPolicy"`
GroupAllowFrom []string `json:"groupAllowFrom"`
Installed bool `json:"installed"`
}
type AgentWeixinLoginReq struct {
AgentID uint `json:"agentId" validate:"required"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentQQBotConfigUpdateReq struct {
@@ -218,23 +300,19 @@ type AgentQQBotConfig struct {
type AgentPluginInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=qqbot"`
Type string `json:"type" validate:"required,oneof=qqbot wecom dingtalk weixin"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginCheckReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=qqbot"`
Type string `json:"type" validate:"required,oneof=qqbot wecom dingtalk weixin"`
}
type AgentPluginStatus struct {
Installed bool `json:"installed"`
}
type AgentDiscordConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentDiscordConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
@@ -252,35 +330,74 @@ type AgentDiscordConfig struct {
Proxy string `json:"proxy"`
}
type AgentBrowserConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
type AgentSecurityConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
AllowedOrigins []string `json:"allowedOrigins"`
}
type AgentBrowserConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
Headless bool `json:"headless"`
NoSandbox bool `json:"noSandbox"`
DefaultProfile string `json:"defaultProfile" validate:"required"`
}
type AgentBrowserConfig struct {
Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"`
Headless bool `json:"headless"`
NoSandbox bool `json:"noSandbox"`
DefaultProfile string `json:"defaultProfile"`
}
type AgentOtherConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
type AgentSecurityConfig struct {
AllowedOrigins []string `json:"allowedOrigins"`
}
type AgentOtherConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone" validate:"required"`
AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry" validate:"required"`
}
type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"`
UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"`
}
type AgentConfigFileReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentConfigFileUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Content string `json:"content" validate:"required"`
}
type AgentConfigFile struct {
Content string `json:"content"`
}
type AgentSkillSearchReq struct {
AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub skillhub"`
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"`
}
type AgentSkillSearchItem struct {
Slug string `json:"slug"`
Name string `json:"name"`
Description string `json:"description"`
Summary string `json:"summary"`
Version string `json:"version"`
Source string `json:"source"`
Score string `json:"score"`
}
type AgentSkillUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Name string `json:"name" validate:"required"`
Enabled bool `json:"enabled"`
}
type AgentSkillInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub skillhub"`
Slug string `json:"slug" validate:"required"`
TaskID string `json:"taskID" validate:"required"`
}
+3 -2
View File
@@ -65,10 +65,11 @@ 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"`
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"`
Secret string `json:"secret"`
StopBefore bool `json:"stopBefore"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
@@ -77,7 +78,7 @@ type CommonBackup struct {
}
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"`
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"`
+6
View File
@@ -11,6 +11,12 @@ type SearchPageWithType struct {
Type string `json:"type"`
}
type SearchPageWithGroup struct {
PageInfo
GroupID uint `json:"groupID"`
Info string `json:"info"`
}
type PageInfo struct {
Page int `json:"page" validate:"required,number"`
PageSize int `json:"pageSize" validate:"required,number"`
+39 -10
View File
@@ -48,6 +48,34 @@ type ContainerOptions struct {
State string `json:"state"`
}
type ContainerFileReq struct {
ContainerID string `json:"containerID" validate:"required"`
Path string `json:"path" validate:"required"`
}
type ContainerFileBatchDeleteReq struct {
ContainerID string `json:"containerID" validate:"required"`
Paths []string `json:"paths" validate:"required,min=1,dive,required"`
}
type ContainerFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
IsLink bool `json:"isLink"`
LinkTo string `json:"linkTo"`
Size int64 `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
}
type ContainerFileContent struct {
Content string `json:"content"`
Size int64 `json:"size"`
Truncated bool `json:"truncated"`
IsBinary bool `json:"isBinary"`
}
type ContainerStatus struct {
Created int `json:"created"`
Running int `json:"running"`
@@ -259,16 +287,17 @@ type BatchDelete struct {
}
type ComposeInfo struct {
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerCount int `json:"containerCount"`
RunningCount int `json:"runningCount"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env string `json:"env"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerCount int `json:"containerCount"`
RunningCount int `json:"runningCount"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
ComposeFileExists bool `json:"composeFileExists"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env string `json:"env"`
}
type ComposeContainer struct {
ContainerID string `json:"containerID"`
+1
View File
@@ -4,6 +4,7 @@ import "time"
type DashboardBase struct {
WebsiteNumber int `json:"websiteNumber"`
AgentNumber int `json:"agentNumber"`
DatabaseNumber int `json:"databaseNumber"`
CronjobNumber int `json:"cronjobNumber"`
AppInstalledNumber int `json:"appInstalledNumber"`
@@ -1,8 +1,6 @@
package dto
import (
"time"
)
import "time"
type HostOperate struct {
ID uint `json:"id"`
@@ -34,11 +32,6 @@ type SearchForTree struct {
Info string `json:"info"`
}
type ChangeHostGroup struct {
ID uint `json:"id" validate:"required"`
GroupID uint `json:"groupID" validate:"required"`
}
type HostInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
+1
View File
@@ -269,6 +269,7 @@ type WebsiteProxyConfig struct {
Replaces map[string]string `json:"replaces"`
SNI bool `json:"sni"`
ProxySSLName string `json:"proxySSLName"`
SSLVerify bool `json:"sslVerify"`
CorsConfig
}
+8
View File
@@ -87,6 +87,14 @@ type SystemProxy struct {
Password string `json:"password"`
}
type TerminalAIInfo struct {
AIStatus string `json:"aiStatus"`
AIAccountID string `json:"aiAccountId"`
AIPrefix string `json:"aiPrefix" validate:"required,oneof=@ai #ai /ai"`
AIRiskCommands string `json:"aiRiskCommands"`
AIRiskCommandsDefault string `json:"aiRiskCommandsDefault"`
}
type CommonDescription struct {
ID string `json:"id" validate:"required"`
Type string `json:"type" validate:"required"`
-3
View File
@@ -6,10 +6,7 @@ type AgentAccount struct {
Name string `json:"name"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
+17
View File
@@ -0,0 +1,17 @@
package model
type AgentAccountModel struct {
BaseModel
AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
}
func (AgentAccountModel) TableName() string {
return "agent_account_models"
}
+232 -65
View File
@@ -5,8 +5,19 @@ import (
)
type Model struct {
ID string
Name string
ID string
Name string
ContextWindow int
MaxTokens int
Reasoning bool
Input []string
}
type RuntimeDefault struct {
APIType string
ContextWindow int
MaxTokens int
Input []string
}
type Meta struct {
@@ -15,8 +26,8 @@ type Meta struct {
Sort uint
DefaultBaseURL string
EnvKey string
Default RuntimeDefault
Models []Model
Enabled bool
}
var catalog = map[string]Meta{
@@ -26,14 +37,24 @@ var catalog = map[string]Meta{
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Enabled: true,
Models: []Model{},
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"ollama": {
Key: "ollama",
DisplayName: "Ollama",
Sort: 15,
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 160000,
MaxTokens: 8192,
Input: []string{"text"},
},
},
"vllm": {
Key: "vllm",
@@ -41,8 +62,13 @@ var catalog = map[string]Meta{
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Enabled: true,
Models: []Model{},
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"deepseek": {
Key: "deepseek",
@@ -50,11 +76,16 @@ var catalog = map[string]Meta{
Sort: 25,
DefaultBaseURL: "https://api.deepseek.com/v1",
EnvKey: "DEEPSEEK_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"},
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", Reasoning: true},
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B", Reasoning: true},
},
},
"bailian-coding-plan": {
@@ -63,16 +94,21 @@ var catalog = map[string]Meta{
Sort: 30,
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
EnvKey: "QWEN_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max"},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next"},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5"},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5"},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5"},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7"},
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
},
},
"ark-coding-plan": {
@@ -81,14 +117,19 @@ var catalog = map[string]Meta{
Sort: 35,
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "ark-coding-plan/doubao-seed-2.0-code", Name: "Doubao-Seed-2.0-Code"},
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao-Seed-Code"},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5"},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7"},
{ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2"},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking"},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5", Reasoning: true},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2", Reasoning: true},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking", Reasoning: true},
},
},
"zai": {
@@ -97,12 +138,17 @@ var catalog = map[string]Meta{
Sort: 40,
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5"},
{ID: "zai/glm-4.7", Name: "GLM-4.7"},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash"},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX"},
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
},
},
"minimax": {
@@ -111,10 +157,36 @@ var catalog = map[string]Meta{
Sort: 45,
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
EnvKey: "MINIMAX_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.1", Name: "MiniMax M2.1"},
{ID: "minimax/MiniMax-M2.1-lightning", Name: "MiniMax M2.1 Lightning"},
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
},
},
"xiaomi": {
Key: "xiaomi",
DisplayName: "Xiaomi",
Sort: 46,
DefaultBaseURL: "https://api.xiaomimimo.com/anthropic",
EnvKey: "XIAOMI_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 262144,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"},
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"},
{ID: "xiaomi/mimo-v2-tts", Name: "Xiaomi MiMo V2 TTS"},
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"},
},
},
"kimi": {
@@ -123,11 +195,16 @@ var catalog = map[string]Meta{
Sort: 50,
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
"kimi-coding": {
@@ -136,9 +213,14 @@ var catalog = map[string]Meta{
Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", Reasoning: true},
},
},
"openai": {
@@ -147,14 +229,18 @@ var catalog = map[string]Meta{
Sort: 55,
DefaultBaseURL: "https://api.openai.com/v1",
EnvKey: "OPENAI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
{ID: "openai/gpt-4o", Name: "GPT-4o"},
{ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"},
{ID: "openai/gpt-5", Name: "GPT-5"},
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"},
{ID: "openai/codex-mini-latest", Name: "Codex Mini", Reasoning: true},
{ID: "openai/gpt-5", Name: "GPT-5", Reasoning: true},
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini", Reasoning: true},
{ID: "openai/gpt-5.4", Name: "GPT-5.4", Reasoning: true},
{ID: "openai/gpt-5.3-codex", Name: "GPT-5.3-Codex", Reasoning: true},
},
},
"openrouter": {
@@ -163,7 +249,12 @@ var catalog = map[string]Meta{
Sort: 56,
DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
@@ -175,13 +266,18 @@ var catalog = map[string]Meta{
Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"},
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"},
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet", Reasoning: true},
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1", Reasoning: true},
},
},
"gemini": {
@@ -190,14 +286,16 @@ var catalog = map[string]Meta{
Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-1.5-flash", Name: "Gemini 1.5 Flash"},
{ID: "google/gemini-1.5-pro", Name: "Gemini 1.5 Pro"},
{ID: "google/gemini-2.0-flash", Name: "Gemini 2.0 Flash"},
{ID: "google/gemini-2.5-flash", Name: "Gemini 2.5 Flash"},
{ID: "google/gemini-2.5-pro", Name: "Gemini 2.5 Pro"},
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
},
"moonshot": {
@@ -206,11 +304,16 @@ var catalog = map[string]Meta{
Sort: 70,
DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
}
@@ -231,11 +334,6 @@ func All() map[string]Meta {
return result
}
func IsEnabled(key string) bool {
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
return ok && meta.Enabled
}
func DefaultBaseURL(key string) (string, bool) {
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
@@ -263,11 +361,80 @@ func DisplayName(key string) string {
return meta.DisplayName
}
func FindModel(key, modelID string) (Model, bool) {
meta, ok := Get(key)
if !ok {
return Model{}, false
}
for _, item := range meta.Models {
if item.ID == modelID {
return item, true
}
}
return Model{}, false
}
func cloneMeta(meta Meta) Meta {
clone := meta
if len(meta.Default.Input) > 0 {
clone.Default.Input = make([]string, len(meta.Default.Input))
copy(clone.Default.Input, meta.Default.Input)
}
if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
copy(clone.Models, meta.Models)
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta, item)
}
}
return clone
}
func normalizeModel(meta Meta, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 {
clone.MaxTokens = meta.Default.MaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = meta.Default.ContextWindow
}
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
clone.Input = make([]string, len(meta.Default.Input))
copy(clone.Input, meta.Default.Input)
}
return clone
}
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
defaultAPIType := "openai-completions"
defaultMaxTokens := 8192
defaultContextWindow := 256000
if meta, ok := Get(provider); ok {
if meta.Default.APIType != "" {
defaultAPIType = meta.Default.APIType
}
if meta.Default.MaxTokens > 0 {
defaultMaxTokens = meta.Default.MaxTokens
}
if meta.Default.ContextWindow > 0 {
defaultContextWindow = meta.Default.ContextWindow
}
}
resolvedAPI := apiType
if strings.TrimSpace(apiType) == "" {
resolvedAPI = defaultAPIType
}
resolvedMaxTokens := defaultMaxTokens
resolvedContextWindow := defaultContextWindow
if maxTokens > 0 {
resolvedMaxTokens = maxTokens
}
if contextWindow > 0 {
resolvedContextWindow = contextWindow
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
+338
View File
@@ -0,0 +1,338 @@
package provider
import (
"fmt"
"strings"
)
type OpenClawPatch struct {
PrimaryModel string
Models map[string]interface{}
}
type openClawModelSpec struct {
ID string
Name string
Reasoning bool
Input []string
ContextWindow int
MaxTokens int
}
type openClawPatchSpec struct {
PrimaryModel string
Provider string
APIKey string
BaseURL string
APIType string
AuthHeader bool
Model openClawModelSpec
}
func BuildOpenClawPatch(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
modelName = strings.TrimSpace(modelName)
if modelName == "" {
return nil, fmt.Errorf("model is required")
}
apiType, maxTokens, contextWindow = ResolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
modelID := modelName
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
modelID = parts[1]
}
var spec openClawPatchSpec
switch provider {
case "deepseek":
spec = buildDeepseekPatchSpec(modelName, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "gemini":
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "moonshot", "kimi":
spec = buildMoonshotPatchSpec(provider, modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "bailian-coding-plan":
spec = buildBailianPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "ark-coding-plan":
spec = buildArkPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "minimax":
spec = buildMiniMaxPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "xiaomi":
spec = buildXiaomiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "custom", "vllm":
spec = buildCustomPatchSpec(provider, modelName, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "ollama":
spec = buildOllamaPatchSpec(modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL)
case "kimi-coding":
spec = buildKimiCodingPatchSpec(modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "zai":
spec = buildZaiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
default:
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
}
return buildOpenClawPatch(spec), nil
}
func buildOpenClawPatch(spec openClawPatchSpec) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: spec.PrimaryModel,
Models: providerModels(
spec.Provider,
spec.APIKey,
spec.BaseURL,
spec.APIType,
spec.AuthHeader,
buildOpenClawModel(spec.Model),
),
}
}
func buildOpenClawModel(spec openClawModelSpec) map[string]interface{} {
return map[string]interface{}{
"id": spec.ID,
"name": spec.Name,
"reasoning": spec.Reasoning,
"input": spec.Input,
"contextWindow": spec.ContextWindow,
"maxTokens": spec.MaxTokens,
"cost": map[string]interface{}{},
}
}
func buildDeepseekPatchSpec(modelName string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: modelName,
Provider: "deepseek",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "openai-completions",
Model: openClawModelSpec{
ID: "deepseek-chat",
Name: "DeepSeek Chat",
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildMoonshotPatchSpec(provider, modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
configProvider := provider
primaryModel := modelName
if provider == "kimi" {
configProvider = "moonshot"
primaryModel = "moonshot/" + modelID
}
return openClawPatchSpec{
PrimaryModel: primaryModel,
Provider: configProvider,
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "openai-completions",
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildBailianPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: "bailian-coding-plan/" + modelID,
Provider: "bailian-coding-plan",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "openai-completions",
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildArkPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: "ark-coding-plan/" + modelID,
Provider: "ark-coding-plan",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "openai-completions",
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildMiniMaxPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: "minimax/" + modelID,
Provider: "minimax",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "anthropic-messages",
AuthHeader: true,
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildXiaomiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
normalizedID := strings.TrimSpace(modelID)
return openClawPatchSpec{
PrimaryModel: "xiaomi/" + normalizedID,
Provider: "xiaomi",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "anthropic-messages",
Model: openClawModelSpec{
ID: normalizedID,
Name: normalizedID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildCustomPatchSpec(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
customModelID := normalizeCustomModel(modelName)
return openClawPatchSpec{
PrimaryModel: provider + "/" + customModelID,
Provider: provider,
APIKey: strings.TrimSpace(apiKey),
BaseURL: strings.TrimSpace(baseURL),
APIType: apiType,
Model: openClawModelSpec{
ID: customModelID,
Name: customModelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildOllamaPatchSpec(modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: modelName,
Provider: "ollama",
APIKey: "ollama",
BaseURL: strings.TrimSpace(baseURL),
APIType: apiType,
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildKimiCodingPatchSpec(modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: modelName,
Provider: "kimi-coding",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "anthropic-messages",
Model: openClawModelSpec{
ID: modelID,
Name: "Kimi for Coding",
Reasoning: reasoning,
Input: []string{"text", "image"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildZaiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: "zai/" + modelID,
Provider: "zai",
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: "openai-completions",
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func buildGenericPatchSpec(provider, modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
providerName := provider
primaryModel := modelName
if provider == "gemini" {
providerName = "google"
primaryModel = "google/" + modelID
}
return openClawPatchSpec{
PrimaryModel: primaryModel,
Provider: providerName,
APIKey: strings.TrimSpace(apiKey),
BaseURL: baseURL,
APIType: apiType,
Model: openClawModelSpec{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: contextWindow,
MaxTokens: maxTokens,
},
}
}
func providerModels(provider, apiKey, baseURL, api string, authHeader bool, model map[string]interface{}) map[string]interface{} {
providerConfig := map[string]interface{}{
"apiKey": apiKey,
"baseUrl": baseURL,
"api": api,
"models": []map[string]interface{}{model},
}
if authHeader {
providerConfig["authHeader"] = true
}
return map[string]interface{}{
"mode": "merge",
"providers": map[string]interface{}{
provider: providerConfig,
},
}
}
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
}
+190
View File
@@ -0,0 +1,190 @@
package provider
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
type VerifyRequest struct {
Method string
URL string
Headers map[string]string
Body []byte
}
const (
defaultVerifyTimeout = 30 * time.Second
)
func SkipVerification(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "custom", "vllm", "ollama", "kimi-coding":
return true
default:
return false
}
}
func VerifyAccount(provider, baseURL, apiKey string) error {
req := BuildVerifyRequest(provider, baseURL, apiKey)
var body *bytes.Buffer
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil {
return err
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func verifyTimeout() time.Duration {
return defaultVerifyTimeout
}
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
provider = strings.ToLower(strings.TrimSpace(provider))
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers := map[string]string{}
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
switch provider {
case "anthropic", "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
request.Method = http.MethodPost
if strings.Contains(base, "/v1beta") {
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
} else {
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
}
headers["x-goog-api-key"] = apiKey
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"contents": []map[string]interface{}{{
"parts": []map[string]string{{
"text": "Explain how AI works in a few words",
}},
}},
})
case "zai":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
request.URL = base + "/models"
case "bailian-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "doubao-seed-2.0-code",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
}
request.Body = mustJSON(map[string]interface{}{
"model": "MiniMax-M2.5",
"max_tokens": 1,
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
})
case "xiaomi":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
}
request.Body = mustJSON(map[string]interface{}{
"model": "mimo-v2-flash",
"max_tokens": 1,
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
})
case "openrouter":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/key"
} else {
request.URL = base + "/v1/key"
}
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
}
return request
}
func mustJSON(value interface{}) []byte {
payload, err := json.Marshal(value)
if err != nil {
return []byte("{}")
}
return payload
}
+50
View File
@@ -0,0 +1,50 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
type AgentAccountModelRepo struct{}
type IAgentAccountModelRepo interface {
List(opts ...DBOption) ([]model.AgentAccountModel, error)
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
Create(item *model.AgentAccountModel) error
Save(item *model.AgentAccountModel) error
DeleteByID(id uint) error
Delete(opts ...DBOption) error
}
func NewIAgentAccountModelRepo() IAgentAccountModelRepo {
return &AgentAccountModelRepo{}
}
func (a AgentAccountModelRepo) List(opts ...DBOption) ([]model.AgentAccountModel, error) {
var list []model.AgentAccountModel
if err := getDb(opts...).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (a AgentAccountModelRepo) GetFirst(opts ...DBOption) (*model.AgentAccountModel, error) {
var item model.AgentAccountModel
if err := getDb(opts...).First(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (a AgentAccountModelRepo) Create(item *model.AgentAccountModel) error {
return getDb().Create(item).Error
}
func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) error {
return getDb().Save(item).Error
}
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
return getDb().Delete(&model.AgentAccountModel{}, id).Error
}
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
}
+2 -1
View File
@@ -80,9 +80,10 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
}
if !withAll && len(quicks) == 0 {
return []model.QuickJump{
{Name: "Agent", Title: "aiTools.agents.agent", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"},
{Name: "Website", Title: "menu.website", Recommend: 10, IsShow: true, Router: "/websites"},
{Name: "Database", Title: "menu.database", Recommend: 30, IsShow: true, Router: "/databases"},
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: true, Router: "/cronjobs"},
{Name: "Cronjob", Title: "menu.cronjob", Recommend: 50, IsShow: false, Router: "/cronjobs"},
{Name: "AppInstalled", Title: "home.appInstalled", Recommend: 70, IsShow: true, Router: "/apps/installed"},
}
}
+12
View File
@@ -19,6 +19,12 @@ func WithByID(id uint) DBOption {
}
}
func WithByGroupID(id uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("group_id = ?", id)
}
}
func WithByNOTID(id uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("id != ?", id)
@@ -43,6 +49,12 @@ func WithByName(name string) DBOption {
}
}
func WithByAddr(addr string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("addr = ?", addr)
}
}
func WithByKey(key string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("key = ?", key)
+86
View File
@@ -10,6 +10,18 @@ import (
type HostRepo struct{}
type IHostRepo interface {
Get(opts ...DBOption) (model.Host, error)
GetList(opts ...DBOption) ([]model.Host, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error)
Create(host *model.Host) error
Update(id uint, vars map[string]interface{}) error
UpdateGroup(group, newGroup uint) error
Delete(opts ...DBOption) error
WithByInfo(info string) DBOption
WithByPort(port uint) DBOption
WithByUser(user string) DBOption
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
SaveFirewallRecord(firewall *model.Firewall) error
@@ -30,6 +42,80 @@ func NewIHostRepo() IHostRepo {
return &HostRepo{}
}
func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) {
var host model.Host
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&host).Error
return host, err
}
func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&hosts).Error
return hosts, err
}
func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&hosts).Error
return count, hosts, err
}
func (h *HostRepo) WithByInfo(info string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}
}
func (h *HostRepo) WithByPort(port uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("port = ?", port)
}
}
func (h *HostRepo) WithByUser(user string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("user = ?", user)
}
}
func (h *HostRepo) Create(host *model.Host) error {
return global.DB.Create(host).Error
}
func (h *HostRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error
}
func (h *HostRepo) UpdateGroup(group, newGroup uint) error {
return global.DB.Model(&model.Host{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error
}
func (h *HostRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Host{}).Error
}
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
var firewall model.Firewall
db := global.DB
+421 -1963
View File
File diff suppressed because it is too large Load Diff
+619
View File
@@ -0,0 +1,619 @@
package service
import (
"fmt"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractFeishuConfig(conf)
return &result, nil
}
func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setFeishuConfig(conf, dto.AgentFeishuConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotName: req.BotName,
AppID: req.AppID,
AppSecret: req.AppSecret,
})
setFeishuPluginEnabled(conf, req.Enabled)
return nil
})
}
func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractTelegramConfig(conf)
return &result, nil
}
func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error {
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,
BotToken: req.BotToken,
Proxy: req.Proxy,
})
return nil
})
}
func (a AgentService) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractDiscordConfig(conf)
return &result, nil
}
func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error {
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,
GroupPolicy: req.GroupPolicy,
Token: req.Token,
Proxy: req.Proxy,
})
return nil
})
}
func (a AgentService) GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractQQBotConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "qqbot")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setQQBotConfig(conf, dto.AgentQQBotConfig{
Enabled: req.Enabled,
AppID: req.AppID,
ClientSecret: req.ClientSecret,
})
return nil
})
}
func (a AgentService) GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractWecomConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "wecom")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setWecomConfig(conf, dto.AgentWecomConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotID: req.BotID,
Secret: req.Secret,
})
return nil
})
}
func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractDingTalkConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "dingtalk")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setDingTalkConfig(conf, dto.AgentDingTalkConfig{
Enabled: req.Enabled,
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
})
return nil
})
}
func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
spec, pluginID, err := resolvePluginMeta(req.Type)
if err != nil {
return err
}
installTask, err := task.NewTaskWithOps(req.Type, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Type == "qqbot" {
legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot")
if err := mgr.RunBashCf("docker exec %s test -d %s", install.ContainerName, legacyPluginPath); err == nil {
if err := mgr.Run("docker", "exec", "-i", install.ContainerName, "sh", "-c", "printf 'yes\\n' | openclaw plugins uninstall qqbot"); err != nil {
return err
}
}
}
if err := mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawPluginInstallScript(spec, pluginID)); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
appendPluginAllow(conf, pluginID)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("install openclaw plugin failed: %v", err)
}
}()
return nil
}
func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
loginTask, err := task.NewTaskWithOps("weixin", task.TaskExec, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
loginTask.AddSubTask("Login OpenClaw Weixin channel", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(30*time.Minute))
return mgr.RunBashCf("docker exec %s openclaw channels login --channel openclaw-weixin", install.ContainerName)
}, nil)
go func() {
if err := loginTask.Execute(); err != nil {
global.LOG.Errorf("login openclaw weixin channel failed: %v", err)
}
}()
return nil
}
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
installed, err := checkPluginInstalled(install.ContainerName, req.Type)
if err != nil {
return nil, err
}
return &dto.AgentPluginStatus{Installed: installed}, nil
}
func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := cmd.RunDefaultBashCf(
"docker exec %s openclaw pairing approve %s %q",
install.ContainerName,
req.Type,
strings.TrimSpace(req.PairingCode),
); err != nil {
return err
}
return nil
}
func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
feishu, ok := channels["feishu"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := feishu["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
accounts, ok := feishu["accounts"].(map[string]interface{})
if !ok {
return result
}
main, ok := accounts["main"].(map[string]interface{})
if !ok {
return result
}
if appID, ok := main["appId"].(string); ok {
result.AppID = appID
}
if appSecret, ok := main["appSecret"].(string); ok {
result.AppSecret = appSecret
}
if botName, ok := main["botName"].(string); ok {
result.BotName = botName
}
return result
}
func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) {
channels := ensureChildMap(conf, "channels")
feishu := ensureChildMap(channels, "feishu")
feishu["enabled"] = config.Enabled
feishu["dmPolicy"] = config.DmPolicy
accounts := ensureChildMap(feishu, "accounts")
main := ensureChildMap(accounts, "main")
main["appId"] = config.AppID
main["appSecret"] = config.AppSecret
main["botName"] = config.BotName
if strings.EqualFold(config.DmPolicy, "open") {
feishu["allowFrom"] = []string{"*"}
}
}
func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
feishu := ensureChildMap(entries, "feishu")
feishu["enabled"] = enabled
}
func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig {
result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
telegram, ok := channels["telegram"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := telegram["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
if botToken, ok := telegram["botToken"].(string); ok {
result.BotToken = botToken
}
if proxy, ok := telegram["proxy"].(string); ok {
result.Proxy = proxy
}
return result
}
func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) {
channels := ensureChildMap(conf, "channels")
telegram := map[string]interface{}{
"enabled": config.Enabled,
"dmPolicy": config.DmPolicy,
"botToken": config.BotToken,
}
if strings.EqualFold(config.DmPolicy, "open") {
telegram["allowFrom"] = []string{"*"}
}
if strings.TrimSpace(config.Proxy) != "" {
telegram["proxy"] = strings.TrimSpace(config.Proxy)
}
channels["telegram"] = telegram
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
discord, ok := channels["discord"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := discord["enabled"].(bool); ok {
result.Enabled = enabled
}
if token, ok := discord["token"].(string); ok {
result.Token = token
}
if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
result.GroupPolicy = groupPolicy
}
if proxy, ok := discord["proxy"].(string); ok {
result.Proxy = proxy
}
if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" {
result.DmPolicy = policy
return result
}
dm, ok := discord["dm"].(map[string]interface{})
if ok {
if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" {
result.DmPolicy = policy
}
}
return result
}
func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) {
channels := ensureChildMap(conf, "channels")
discord := ensureChildMap(channels, "discord")
discord["enabled"] = config.Enabled
discord["token"] = config.Token
discord["dmPolicy"] = config.DmPolicy
discord["groupPolicy"] = config.GroupPolicy
if strings.EqualFold(config.DmPolicy, "open") {
discord["allowFrom"] = []string{"*"}
} else {
delete(discord, "allowFrom")
}
if strings.TrimSpace(config.Proxy) != "" {
discord["proxy"] = strings.TrimSpace(config.Proxy)
} else {
delete(discord, "proxy")
}
delete(discord, "dm")
}
func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
result := dto.AgentQQBotConfig{Enabled: true}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
qqbot, ok := channels["qqbot"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := qqbot["enabled"].(bool); ok {
result.Enabled = enabled
}
if appID, ok := qqbot["appId"].(string); ok {
result.AppID = appID
}
if clientSecret, ok := qqbot["clientSecret"].(string); ok {
result.ClientSecret = clientSecret
}
return result
}
func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
wecom, ok := channels["wecom"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := wecom["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = strings.TrimSpace(dmPolicy)
}
if botID, ok := wecom["botId"].(string); ok {
result.BotID = botID
}
if secret, ok := wecom["secret"].(string); ok {
result.Secret = secret
}
return result
}
func extractDingTalkConfig(conf map[string]interface{}) dto.AgentDingTalkConfig {
result := dto.AgentDingTalkConfig{
Enabled: true,
DmPolicy: "pairing",
GroupPolicy: "disabled",
AllowFrom: []string{},
GroupAllowFrom: []string{},
}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
dingtalk, ok := channels["dingtalk-connector"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := dingtalk["enabled"].(bool); ok {
result.Enabled = enabled
}
if clientID, ok := dingtalk["clientId"].(string); ok {
result.ClientID = clientID
}
if clientSecret, ok := dingtalk["clientSecret"].(string); ok {
result.ClientSecret = clientSecret
}
if dmPolicy, ok := dingtalk["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
if groupPolicy, ok := dingtalk["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
result.GroupPolicy = groupPolicy
}
result.AllowFrom = extractStringList(dingtalk["allowFrom"])
result.GroupAllowFrom = extractStringList(dingtalk["groupAllowFrom"])
return result
}
func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
channels := ensureChildMap(conf, "channels")
wecom := ensureChildMap(channels, "wecom")
wecom["enabled"] = config.Enabled
wecom["botId"] = strings.TrimSpace(config.BotID)
wecom["secret"] = strings.TrimSpace(config.Secret)
wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy)
if strings.EqualFold(config.DmPolicy, "open") {
wecom["allowFrom"] = []string{"*"}
} else {
wecom["allowFrom"] = []string{}
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
wecomEntry["enabled"] = config.Enabled
}
func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConfig) {
channels := ensureChildMap(conf, "channels")
dingtalk := ensureChildMap(channels, "dingtalk-connector")
dingtalk["enabled"] = config.Enabled
dingtalk["clientId"] = strings.TrimSpace(config.ClientID)
dingtalk["clientSecret"] = strings.TrimSpace(config.ClientSecret)
dingtalk["dmPolicy"] = config.DmPolicy
dingtalk["groupPolicy"] = config.GroupPolicy
dingtalk["gatewayToken"] = extractGatewayToken(conf)
switch config.DmPolicy {
case "open":
dingtalk["allowFrom"] = []string{"*"}
case "allowlist":
dingtalk["allowFrom"] = append([]string(nil), config.AllowFrom...)
default:
delete(dingtalk, "allowFrom")
}
switch config.GroupPolicy {
case "open":
dingtalk["groupAllowFrom"] = []string{"*"}
case "allowlist":
dingtalk["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
default:
delete(dingtalk, "groupAllowFrom")
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
dingtalkEntry := ensureChildMap(entries, "dingtalk-connector")
dingtalkEntry["enabled"] = config.Enabled
gateway := ensureChildMap(conf, "gateway")
httpMap := ensureChildMap(gateway, "http")
endpoints := ensureChildMap(httpMap, "endpoints")
chatCompletions := ensureChildMap(endpoints, "chatCompletions")
chatCompletions["enabled"] = true
}
func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
channels := ensureChildMap(conf, "channels")
qqbot := ensureChildMap(channels, "qqbot")
delete(qqbot, "dmPolicy")
qqbot["enabled"] = config.Enabled
qqbot["allowFrom"] = []string{"*"}
qqbot["appId"] = strings.TrimSpace(config.AppID)
qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret)
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
delete(entries, "qqbot")
qqbotEntry := ensureChildMap(entries, "openclaw-qqbot")
qqbotEntry["enabled"] = config.Enabled
}
func appendPluginAllow(conf map[string]interface{}, pluginID string) {
plugins := ensureChildMap(conf, "plugins")
allow := make([]string, 0, 4)
seen := map[string]struct{}{}
switch values := plugins["allow"].(type) {
case []interface{}:
for _, value := range values {
text, ok := value.(string)
if !ok || text == "" {
continue
}
if _, ok := seen[text]; ok {
continue
}
seen[text] = struct{}{}
allow = append(allow, text)
}
}
if _, ok := seen[pluginID]; ok {
plugins["allow"] = allow
return
}
plugins["allow"] = append(allow, pluginID)
}
func buildOpenclawPluginInstallScript(spec, pluginID string) string {
return fmt.Sprintf(
"set -e; workdir=%s/%s; rm -rf \"$workdir\"; mkdir -p \"$workdir\"; cd \"$workdir\"; npm pack --silent %q >/dev/null 2>&1; pkg=$(find \"$workdir\" -maxdepth 1 -type f -name '*.tgz' | head -n 1); printf '%%s\\n' \"$pkg\"; openclaw plugins install \"$pkg\"; rm -rf \"$workdir\"",
openclawPluginPackageTmpDir,
pluginID,
spec,
)
}
func resolvePluginMeta(pluginType string) (string, string, error) {
switch pluginType {
case "qqbot":
return "@tencent-connect/openclaw-qqbot@latest", "openclaw-qqbot", nil
case "wecom":
return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
case "dingtalk":
return "@dingtalk-real-ai/dingtalk-connector", "dingtalk-connector", nil
case "weixin":
return "@tencent-weixin/openclaw-weixin", "openclaw-weixin", nil
default:
return "", "", fmt.Errorf("unsupported plugin type")
}
}
func checkPluginInstalled(containerName, pluginType string) (bool, error) {
_, pluginDir, err := resolvePluginMeta(pluginType)
if err != nil {
return false, err
}
if strings.TrimSpace(containerName) == "" {
return false, buserr.New("ErrRecordNotFound")
}
pluginPath := path.Join(openclawPluginBaseDir, pluginDir)
mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil {
return false, nil
}
return true, nil
}
+244
View File
@@ -0,0 +1,244 @@
package service
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
const (
openclawCronJobsRelativePath = "data/conf/cron/jobs.json"
openclawSessionStoreRelativeGlob = "data/conf/agents/*/sessions/sessions.json"
openclawSessionFilesRelativeGlob = "data/conf/agents/*/sessions/*.jsonl"
)
func (a AgentService) GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error) {
agent, install, conf, err := a.loadOpenclawAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
overview := &dto.AgentOverview{
Snapshot: dto.AgentOverviewSnapshot{
ContainerStatus: install.Status,
AppVersion: install.Version,
DefaultModel: extractOpenclawDefaultModel(conf),
ChannelCount: countOpenclawConfiguredChannels(conf),
},
}
if overview.Snapshot.DefaultModel == "" {
overview.Snapshot.DefaultModel = agent.Model
}
if install.Status != constant.StatusRunning {
return overview, nil
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
skillCount, err := loadOpenclawOverviewSkillStats(install.ContainerName)
if err == nil {
overview.Snapshot.SkillCount = skillCount
}
}()
go func() {
defer wg.Done()
sessionCount, err := loadOpenclawOverviewSessionCount(install.GetPath())
if err == nil {
overview.Snapshot.SessionCount = sessionCount
}
}()
go func() {
defer wg.Done()
jobCount, err := loadOpenclawOverviewJobCount(install.GetPath())
if err == nil {
overview.Snapshot.JobCount = jobCount
}
}()
wg.Wait()
return overview, nil
}
func extractOpenclawDefaultModel(conf map[string]interface{}) string {
agents, ok := conf["agents"].(map[string]interface{})
if !ok {
return ""
}
defaults, ok := agents["defaults"].(map[string]interface{})
if !ok {
return ""
}
model, ok := defaults["model"].(map[string]interface{})
if !ok {
return ""
}
primary, _ := model["primary"].(string)
return strings.TrimSpace(primary)
}
func countOpenclawConfiguredChannels(conf map[string]interface{}) int {
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return 0
}
count := 0
for _, value := range channels {
channel, ok := value.(map[string]interface{})
if !ok || len(channel) == 0 {
continue
}
count++
}
return count
}
func loadOpenclawOverviewSkillStats(containerName string) (int, error) {
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills list --json 2>&1",
30*time.Second,
containerName,
)
if err != nil {
return 0, err
}
if strings.TrimSpace(output) == "" {
return 0, nil
}
skills, err := parseOpenclawSkillsList(output)
if err != nil {
return 0, err
}
return len(skills), nil
}
func loadOpenclawOverviewSessionCount(installPath string) (int, error) {
sessionStorePaths, err := filepath.Glob(filepath.Join(installPath, openclawSessionStoreRelativeGlob))
if err != nil {
return 0, err
}
total := 0
for _, sessionStorePath := range sessionStorePaths {
count, err := loadOpenclawSessionCountFromFile(sessionStorePath)
if err != nil {
continue
}
total += count
}
if total > 0 {
return total, nil
}
sessionFiles, err := filepath.Glob(filepath.Join(installPath, openclawSessionFilesRelativeGlob))
if err != nil {
return 0, err
}
return len(sessionFiles), nil
}
func loadOpenclawSessionCountFromFile(sessionStorePath string) (int, error) {
content, err := os.ReadFile(sessionStorePath)
if err != nil {
return 0, err
}
return parseOpenclawSessionCount(string(content))
}
func loadOpenclawOverviewJobCount(installPath string) (int, error) {
content, err := os.ReadFile(filepath.Join(installPath, openclawCronJobsRelativePath))
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
return parseOpenclawCronCount(string(content))
}
func parseOpenclawSessionCount(output string) (int, error) {
if strings.TrimSpace(output) == "" {
return 0, nil
}
var payload interface{}
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return 0, err
}
return countOpenclawSessions(payload), nil
}
func parseOpenclawCronCount(output string) (int, error) {
if strings.TrimSpace(output) == "" {
return 0, nil
}
var payload interface{}
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return 0, err
}
switch value := payload.(type) {
case []interface{}:
return len(value), nil
case map[string]interface{}:
if total, ok := value["total"].(float64); ok {
return int(total), nil
}
if jobs, ok := value["jobs"].([]interface{}); ok {
return len(jobs), nil
}
return len(value), nil
default:
return 0, nil
}
}
func countOpenclawSessions(payload interface{}) int {
switch value := payload.(type) {
case []interface{}:
return len(value)
case map[string]interface{}:
if count, ok := value["count"].(float64); ok {
return int(count)
}
if sessions, ok := value["sessions"]; ok {
return countOpenclawSessions(sessions)
}
count := 0
for _, item := range value {
switch typed := item.(type) {
case map[string]interface{}:
if looksLikeOpenclawSession(typed) {
count++
}
case string:
if strings.HasSuffix(strings.TrimSpace(typed), ".jsonl") {
count++
}
}
}
return count
default:
return 0
}
}
func looksLikeOpenclawSession(item map[string]interface{}) bool {
if _, ok := item["sessionFile"]; ok {
return true
}
if _, ok := item["agentId"]; ok {
return true
}
if _, ok := item["lastActivityTs"]; ok {
return true
}
if _, ok := item["updatedAt"]; ok {
return true
}
return false
}
+286
View File
@@ -0,0 +1,286 @@
package service
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type openclawSkillsList struct {
Skills []openclawSkillListItem `json:"skills"`
}
type openclawSkillListItem struct {
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Bundled bool `json:"bundled"`
Disabled bool `json:"disabled"`
}
type openclawSkillInfo struct {
SkillKey string `json:"skillKey"`
}
type skillhubSearchPayload struct {
Skills []dto.AgentSkillSearchItem `json:"skills"`
Results []dto.AgentSkillSearchItem `json:"results"`
}
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)
if err != nil {
return nil, err
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return nil, err
}
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills list --json 2>&1",
30*time.Second,
install.ContainerName,
)
if err != nil {
return nil, err
}
if len(output) == 0 {
return nil, nil
}
return parseOpenclawSkillsList(output)
}
func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return nil, err
}
output, err := loadOpenclawSkillSearchOutput(install.ContainerName, req.Source, req.Keyword)
if err != nil {
return nil, err
}
if len(output) == 0 {
return nil, nil
}
switch req.Source {
case "skillhub":
return parseSkillhubSearchResult(output)
default:
return parseClawhubSearchResult(output), nil
}
}
func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
skillKey, err := getOpenclawSkillKey(install.ContainerName, req.Name)
if err != nil {
return err
}
setOpenclawSkillEnabled(conf, skillKey, req.Enabled)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
}
installTask, err := task.NewTaskWithOps(req.Slug, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug))
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("install openclaw skill failed: %v", err)
}
}()
return nil
}
func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
payloadBytes, err := extractEmbeddedJSON(output)
if err != nil {
return nil, err
}
if len(payloadBytes) == 0 {
return nil, nil
}
var payload openclawSkillsList
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, err
}
items := make([]dto.AgentSkillItem, 0, len(payload.Skills))
for _, item := range payload.Skills {
items = append(items, dto.AgentSkillItem{
Name: item.Name,
Description: item.Description,
Source: item.Source,
Bundled: item.Bundled,
Disabled: item.Disabled,
})
}
return items, nil
}
func loadOpenclawSkillSearchOutput(containerName, source, keyword string) (string, error) {
switch source {
case "skillhub":
return cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s skillhub search %q --json 2>&1",
30*time.Second,
containerName,
keyword,
)
default:
return cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s clawhub search %q 2>&1",
30*time.Second,
containerName,
keyword,
)
}
}
func parseSkillhubSearchResult(output string) ([]dto.AgentSkillSearchItem, error) {
trimmed := strings.TrimSpace(output)
if trimmed == "" {
return nil, nil
}
var list []dto.AgentSkillSearchItem
if err := json.Unmarshal([]byte(trimmed), &list); err == nil {
for i := range list {
list[i].Source = "skillhub"
}
return list, nil
}
var payload skillhubSearchPayload
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return nil, err
}
items := payload.Skills
if len(items) == 0 {
items = payload.Results
}
for i := range items {
items[i].Source = "skillhub"
}
return items, nil
}
func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem {
lines := strings.Split(strings.TrimSpace(output), "\n")
items := make([]dto.AgentSkillSearchItem, 0, len(lines))
for _, line := range lines {
matches := clawhubSearchLinePattern.FindStringSubmatch(strings.TrimSpace(line))
if len(matches) != 4 {
continue
}
items = append(items, dto.AgentSkillSearchItem{
Slug: matches[1],
Name: matches[2],
Score: matches[3],
Source: "clawhub",
})
}
return items
}
func buildOpenclawSkillInstallCommand(source, slug string) string {
switch source {
case "clawhub":
return fmt.Sprintf(
"mkdir -p %s && clawhub --workdir /home/node/.openclaw --dir skills install %q",
openclawManagedSkillsDir,
slug,
)
default:
return fmt.Sprintf(
"mkdir -p %s && skillhub --dir %s install %q",
openclawManagedSkillsDir,
openclawManagedSkillsDir,
slug,
)
}
}
func getOpenclawSkillKey(containerName, name string) (string, error) {
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills info %q --json 2>&1",
30*time.Second,
containerName,
name,
)
if err != nil {
return "", err
}
return parseOpenclawSkillKey(name, output)
}
func parseOpenclawSkillKey(name, output string) (string, error) {
payloadBytes, err := extractEmbeddedJSON(output)
if err != nil {
return "", err
}
var payload openclawSkillInfo
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return "", err
}
if payload.SkillKey == "" {
return "", fmt.Errorf("skill %s does not have a skillKey", name)
}
return payload.SkillKey, nil
}
func extractEmbeddedJSON(output string) ([]byte, error) {
trimmed := strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, ""))
if trimmed == "" {
return nil, nil
}
for i := 0; i < len(trimmed); i++ {
if trimmed[i] != '{' && trimmed[i] != '[' {
continue
}
decoder := json.NewDecoder(strings.NewReader(trimmed[i:]))
var raw json.RawMessage
if err := decoder.Decode(&raw); err == nil {
return raw, nil
}
}
return nil, fmt.Errorf("json payload not found")
}
func setOpenclawSkillEnabled(conf map[string]interface{}, skillKey string, enabled bool) {
skills := ensureChildMap(conf, "skills")
entries := ensureChildMap(skills, "entries")
entry := ensureChildMap(entries, skillKey)
entry["enabled"] = enabled
}
File diff suppressed because it is too large Load Diff
+16 -31
View File
@@ -3,6 +3,12 @@ package service
import (
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"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"
@@ -18,11 +24,6 @@ import (
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"math"
"sort"
"strconv"
"strings"
"time"
)
const (
@@ -423,25 +424,19 @@ func loadDiskUsage(alert dto.AlertDTO) {
}
if isAlertDue(newDate) {
if strings.Contains(alert.Project, "all") {
err = processAllDisks(alert)
_ = processAllDisks(alert)
} else {
err = processSingleDisk(alert)
_ = processSingleDisk(alert)
}
}
}
func loadPanelLogin(alert dto.AlertDTO) {
count, isAlert, err := alertUtil.CountRecentFailedLoginLogs(alert.Cycle, alert.Count)
alertType := alert.Type
quota := strconv.Itoa(count)
quotaType := strconv.Itoa(int(alert.Cycle))
if err != nil {
global.LOG.Errorf("Failed to count recent failed login logs: %v", err)
}
if isAlert {
alertType = "panelLogin"
quota = strconv.Itoa(count)
quotaType = "panelLogin"
params := []dto.Param{
{
Index: "1",
@@ -454,7 +449,7 @@ func loadPanelLogin(alert dto.AlertDTO) {
Value: "",
},
}
sendAlerts(alert, alertType, quota, quotaType, params)
sendAlerts(alert, "panelLogin", strconv.Itoa(count), "panelLogin", params)
}
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
@@ -463,15 +458,13 @@ func loadPanelLogin(alert dto.AlertDTO) {
global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err)
}
if len(records) > 0 {
quota = strings.Join(func() []string {
quota := strings.Join(func() []string {
var ips []string
for _, r := range records {
ips = append(ips, r.IP)
}
return ips
}(), "\n")
alertType = "panelIpLogin"
quotaType = "panelIpLogin"
params := []dto.Param{
{
Index: "1",
@@ -484,22 +477,16 @@ func loadPanelLogin(alert dto.AlertDTO) {
Value: " IP ",
},
}
sendAlerts(alert, alertType, quota, quotaType, params)
sendAlerts(alert, "panelIpLogin", quota, "panelIpLogin", params)
}
}
func loadSSHLogin(alert dto.AlertDTO) {
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
alertType := alert.Type
quota := strconv.Itoa(count)
quotaType := strconv.Itoa(int(alert.Cycle))
if err != nil {
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
}
if isAlert {
alertType = "sshLogin"
quota = strconv.Itoa(count)
quotaType = "sshLogin"
params := []dto.Param{
{
Index: "1",
@@ -512,7 +499,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
Value: "",
},
}
sendAlerts(alert, alertType, quota, quotaType, params)
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
}
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
@@ -520,9 +507,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
}
if len(records) > 0 {
quota = strings.Join(records, "\n")
alertType = "sshIpLogin"
quotaType = "sshIpLogin"
quota := strings.Join(records, "\n")
params := []dto.Param{
{
Index: "1",
@@ -535,7 +520,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
Value: " IP ",
},
}
sendAlerts(alert, alertType, quota, quotaType, params)
sendAlerts(alert, "sshIpLogin", quota, "sshIpLogin", params)
}
}
@@ -894,7 +879,7 @@ func processAllDisks(alert dto.AlertDTO) error {
if err != nil {
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
errMsgs = append(errMsgs, errMsg)
global.LOG.Errorf(errMsg)
global.LOG.Errorf("%s", errMsg)
continue
}
}
@@ -907,7 +892,7 @@ func processAllDisks(alert dto.AlertDTO) error {
func processSingleDisk(alert dto.AlertDTO) error {
err := checkAndCreateDiskAlert(alert, alert.Project)
if err != nil {
global.LOG.Errorf(err.Error())
global.LOG.Errorf("%s", err.Error())
return err
}
return nil
+13
View File
@@ -57,6 +57,10 @@ type IAppService interface {
GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
}
type appInstallHooks struct {
AfterCopyData func(appInstall *model.AppInstall) error
}
func NewIAppService() IAppService {
return &AppService{}
}
@@ -340,6 +344,10 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
}
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
return a.installWithHooks(req, executeScript, nil)
}
func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript bool, hooks *appInstallHooks) (appInstall *model.AppInstall, err error) {
if err = docker.CreateDefaultDockerNetwork(); err != nil {
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
return
@@ -537,6 +545,11 @@ func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (a
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
if hooks != nil && hooks.AfterCopyData != nil {
if err = hooks.AfterCopyData(appInstall); err != nil {
return err
}
}
if executeScript {
if err = runScript(t, appInstall, "init"); err != nil {
return err
+15 -6
View File
@@ -337,12 +337,11 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if err != nil {
return err
}
changePort := false
oldInstalled := installed
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
if ok {
portN := int(math.Ceil(port.(float64)))
if portN != installed.HttpPort {
changePort = true
httpPort, err := checkPort("PANEL_APP_PORT_HTTP", req.Params)
if err != nil {
return err
@@ -428,9 +427,17 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
installed.Status = constant.StatusRunning
_ = appInstallRepo.Save(context.Background(), &installed)
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
website, _ := websiteRepo.GetFirst(websiteRepo.WithAppInstallId(installed.ID))
if changePort && website.ID != 0 && website.Status == constant.StatusRunning {
go func() {
if proxyChanged && website.ID != 0 && currentProxyErr == nil {
website.Proxy = currentProxy
if err := websiteRepo.SaveWithoutCtx(&website); err != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
}
}
if proxyChanged && website.ID != 0 && website.Status == constant.StatusRunning && currentProxyErr == nil {
go func(website model.Website, proxy string) {
nginxInstall, err := getNginxFull(&website)
if err != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
@@ -443,7 +450,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
return
}
server := servers[0]
proxy := fmt.Sprintf("http://127.0.0.1:%d", installed.HttpPort)
server.UpdateRootProxy([]string{proxy})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
@@ -454,7 +460,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
return
}
}()
}(website, currentProxy)
}
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
}
return nil
}
+9 -6
View File
@@ -33,7 +33,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
composeV2 "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
@@ -363,7 +362,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
if err != nil {
return err
}
images, err := composeV2.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err
}
@@ -676,6 +675,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
@@ -752,7 +752,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := composeV2.GetImagesFromDockerCompose(content, composeContent)
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
@@ -802,6 +802,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
var newCompose string
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
return err
}
if req.DockerCompose == "" {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
@@ -911,7 +914,7 @@ func getContainerNames(install model.AppInstall) ([]string, error) {
if err != nil {
return nil, err
}
project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true)
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true)
if err != nil {
return nil, err
}
@@ -1133,7 +1136,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
}
func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
client, err := composeV2.NewDockerClient()
client, err := docker.NewDockerClient()
if err != nil {
return false, err
}
@@ -1169,7 +1172,7 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error
if err != nil {
return err
}
images, err := composeV2.GetImagesFromDockerCompose(envByte, []byte(appInstall.DockerCompose))
images, err := docker.GetImagesFromDockerCompose(envByte, []byte(appInstall.DockerCompose))
if err != nil {
return err
}
+5
View File
@@ -57,6 +57,11 @@ type IBackupService interface {
AppBackup(db dto.CommonBackup) (*model.BackupRecord, error)
AppRecover(req dto.CommonRecover) error
ContainerBackup(req dto.CommonBackup) error
ContainerRecover(req dto.CommonRecover) error
ComposeBackup(req dto.CommonBackup) error
ComposeRecover(req dto.CommonRecover) error
}
func NewIBackupService() IBackupService {
+707
View File
@@ -0,0 +1,707 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"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/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
type composeBackupFile struct {
OriginalPath string `json:"originalPath"`
FileName string `json:"fileName"`
RelativePath string `json:"relativePath,omitempty"`
BackupPath string `json:"backupPath"`
}
type composeBackupMeta struct {
ComposeName string `json:"composeName"`
ComposePath string `json:"composePath"`
CreatedAt string `json:"createdAt"`
Files []composeBackupFile `json:"files"`
Containers []string `json:"containers"`
}
type composeBackupContext struct {
req dto.CommonBackup
composeName string
composePath string
composeFiles []string
composeDir string
fileOp files.FileOp
dockerClient *client.Client
stopped bool
backupDir string
fileName string
filePath string
tmpDir string
meta composeBackupMeta
}
type composeRecoverContext struct {
req dto.CommonRecover
fileOp files.FileOp
tmpDir string
meta composeBackupMeta
composeName string
targetDir string
composePath string
enqueued bool
}
func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
fileName := req.FileName
if fileName == "" {
fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow)
}
if !strings.HasSuffix(fileName, ".tar.gz") {
fileName += ".tar.gz"
}
itemDir := fmt.Sprintf("compose/%s", req.Name)
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
record := &model.BackupRecord{
Type: req.Type,
Name: req.Name,
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 compose backup record failed, err: %v", err)
return err
}
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
}
func (u *BackupService) ComposeRecover(req dto.CommonRecover) error {
return handleComposeRecover(req, nil)
}
func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, backupDir, fileName string) error {
composeCtx, err := newComposeBackupContext(req, backupDir, fileName)
if err != nil {
return err
}
containerNames, err := loadComposeContainerNames(composeCtx)
if err != nil {
composeCtx.close()
return err
}
backupTask := parentTask
if backupTask == nil {
backupTask, err = task.NewTaskWithOps(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
}
if req.StopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStop"), func(t *task.Task) error {
return stepStopComposeForBackup(composeCtx)
}, func(t *task.Task) {
_ = stepStartComposeAfterBackup(composeCtx)
}, 3, time.Hour)
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupPrepare"), func(t *task.Task) error { return stepPrepareComposeBackup(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupFiles"), func(t *task.Task) error { return stepBackupComposeFiles(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupContainers"), func(t *task.Task) error { return nil }, nil, 3, time.Hour)
for _, containerName := range containerNames {
backupFileName := fmt.Sprintf("%s.tar.gz", sanitizeComposeFileName(containerName))
backupFile := path.Join(composeCtx.tmpDir, "containers", backupFileName)
if err := handleContainerBackup(containerName, backupTask, 0, path.Dir(backupFile), path.Base(backupFile), "", "", false); err != nil {
return err
}
composeCtx.meta.Containers = append(composeCtx.meta.Containers, path.Join("containers", backupFileName))
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupMeta"), func(t *task.Task) error { return stepWriteComposeBackupMeta(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(task.GetTaskName(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error {
return stepPackComposeBackup(composeCtx)
}, nil, 3, time.Hour)
if req.StopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStart"), func(t *task.Task) error {
return stepStartComposeAfterBackup(composeCtx)
}, nil, 3, time.Hour)
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupCleanup"), func(t *task.Task) error {
composeCtx.close()
return nil
}, nil, 0, time.Hour)
if parentTask != nil {
return nil
}
go func() {
defer composeCtx.close()
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 loadComposeContainerNames(composeCtx *composeBackupContext) ([]string, error) {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
containers, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
if err != nil {
return nil, err
}
names := make([]string, 0, len(containers))
for _, item := range containers {
if len(item.Names) == 0 {
continue
}
names = append(names, strings.TrimPrefix(item.Names[0], "/"))
}
sort.Strings(names)
return names, nil
}
func handleComposeRecover(req dto.CommonRecover, parentTask *task.Task) error {
var recoverCtx *composeRecoverContext
recoverTask := parentTask
var err error
if recoverTask == nil {
if isImportRecover(req) {
taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Compose")
recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
} else {
taskName := req.Name
if taskName == "" {
taskName = "compose"
}
recoverTask, err = task.NewTaskWithOps(taskName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
}
}
timeout := loadRecoverTimeout(req.Timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverPrepare"), func(t *task.Task) error {
ctx, err := newComposeRecoverContext(req)
if err != nil {
return err
}
recoverCtx = ctx
if err := stepPrepareComposeRecover(recoverCtx); err != nil {
recoverCtx.close()
recoverCtx = nil
return err
}
return nil
}, func(t *task.Task) {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
}, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverExtract"), func(t *task.Task) error { return stepExtractComposeRecover(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverMeta"), func(t *task.Task) error {
if err := stepLoadComposeRecoverMeta(recoverCtx); err != nil {
return err
}
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogName", map[string]interface{}{
"name": recoverCtx.composeName,
}))
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogPath", map[string]interface{}{
"backupPath": recoverCtx.meta.ComposePath,
"targetDir": recoverCtx.targetDir,
}))
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogCount", map[string]interface{}{
"files": len(recoverCtx.meta.Files),
"containers": len(recoverCtx.meta.Containers),
}))
return nil
}, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverFiles"), func(t *task.Task) error { return stepRestoreComposeFiles(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverContainers"), func(t *task.Task) error {
if recoverCtx.enqueued {
return nil
}
containerItems := make([]string, 0, len(recoverCtx.meta.Containers))
for _, item := range recoverCtx.meta.Containers {
backupItem := item
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
if err != nil {
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
}
if !recoverCtx.fileOp.Stat(filePath) {
return fmt.Errorf("container backup file not found: %s", backupItem)
}
containerItems = append(containerItems, backupItem)
}
for _, backupItem := range containerItems {
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
if err != nil {
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
}
containerLabel := strings.TrimSuffix(path.Base(backupItem), ".tar.gz")
containerReq := recoverCtx.req
containerReq.Type = "container"
containerReq.Name = containerLabel
containerReq.DetailName = ""
containerReq.File = filePath
containerReq.Secret = ""
if err := handleContainerRecover(containerReq, recoverTask); err != nil {
return err
}
}
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverRecord"), func(t *task.Task) error {
return stepSaveComposeRecord(recoverCtx)
}, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverCleanup"), func(t *task.Task) error {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
return nil
}, nil, 0, timeout)
recoverCtx.enqueued = true
return nil
}, nil, 3, timeout)
if parentTask != nil {
return nil
}
go func() {
_ = recoverTask.Execute()
}()
return nil
}
func newComposeBackupContext(req dto.CommonBackup, backupDir, fileName string) (*composeBackupContext, error) {
if req.Name == "" {
return nil, fmt.Errorf("compose name is required")
}
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return nil, err
}
composePath, composeFiles, err := loadComposePathAndFiles(req.Name, dockerClient)
if err != nil {
_ = dockerClient.Close()
return nil, err
}
filePath := path.Join(backupDir, fileName)
tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz"))
ctx := &composeBackupContext{
req: req,
composeName: req.Name,
composePath: composePath,
composeFiles: composeFiles,
composeDir: path.Dir(composeFiles[0]),
fileOp: files.NewFileOp(),
dockerClient: dockerClient,
backupDir: backupDir,
fileName: fileName,
filePath: filePath,
tmpDir: tmpDir,
meta: composeBackupMeta{
ComposeName: req.Name,
ComposePath: composePath,
CreatedAt: time.Now().Format(constant.DateTimeLayout),
Files: make([]composeBackupFile, 0),
Containers: make([]string, 0),
},
}
return ctx, nil
}
func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (string, []string, error) {
composeRecord, _ := composeRepo.GetRecord(repo.WithByName(composeName))
if composeRecord.ID == 0 {
composeRecord, _ = composeRepo.GetRecord(repo.WithByName(strings.ToLower(composeName)))
}
composePath := composeRecord.Path
if composePath == "" {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel))
list, err := dockerClient.ContainerList(context.Background(), options)
if err != nil {
return "", nil, err
}
if len(list) == 0 {
return "", nil, fmt.Errorf("compose %s not found", composeName)
}
var targetContainer *container.Summary
for i := range list {
if strings.EqualFold(list[i].Labels[composeProjectLabel], composeName) {
targetContainer = &list[i]
break
}
}
if targetContainer == nil {
return "", nil, fmt.Errorf("compose %s not found", composeName)
}
config := targetContainer.Labels[composeConfigLabel]
workdir := targetContainer.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composePath = config
} else {
composePath = workdir
}
}
composeFiles := normalizeComposeFiles(composePath)
if len(composeFiles) == 0 {
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
}
return composePath, composeFiles, nil
}
func normalizeComposeFiles(composePath string) []string {
items := strings.Split(composePath, ",")
result := make([]string, 0)
seen := make(map[string]struct{})
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
stat, err := os.Stat(item)
if err == nil && stat.IsDir() {
item = path.Join(item, "docker-compose.yml")
}
if _, err := os.Stat(item); err != nil {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
sort.Strings(result)
return result
}
func (c *composeBackupContext) close() {
if c.dockerClient != nil {
_ = c.dockerClient.Close()
c.dockerClient = nil
}
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
c.tmpDir = ""
}
}
func stepPrepareComposeBackup(composeCtx *composeBackupContext) error {
if err := os.MkdirAll(composeCtx.backupDir, os.ModePerm); err != nil {
return fmt.Errorf("mkdir %s failed, err: %v", composeCtx.backupDir, err)
}
_ = os.RemoveAll(composeCtx.tmpDir)
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "compose_files"), os.ModePerm); err != nil {
return err
}
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "containers"), os.ModePerm); err != nil {
return err
}
return nil
}
func stepStopComposeForBackup(composeCtx *composeBackupContext) error {
if composeCtx.stopped {
return nil
}
options := container.ListOptions{All: false}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
runningList, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
if err != nil {
return err
}
if len(runningList) == 0 {
return nil
}
if stdout, err := compose.Operate(composeCtx.composePath, "stop"); err != nil {
return fmt.Errorf("docker-compose stop failed, std: %s, err: %v", stdout, err)
}
composeCtx.stopped = true
return nil
}
func stepStartComposeAfterBackup(composeCtx *composeBackupContext) error {
if !composeCtx.stopped {
return nil
}
if stdout, err := compose.Up(composeCtx.composePath); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
}
composeCtx.stopped = false
return nil
}
func stepBackupComposeFiles(composeCtx *composeBackupContext) error {
for i, filePath := range composeCtx.composeFiles {
backupName := fmt.Sprintf("%02d_%s", i, path.Base(filePath))
backupPath := path.Join(composeCtx.tmpDir, "compose_files", backupName)
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
if err := composeCtx.fileOp.SaveFile(backupPath, string(content), fs.ModePerm); err != nil {
return err
}
relativePath := path.Base(filePath)
if composeCtx.composeDir != "" {
rel, relErr := filepath.Rel(composeCtx.composeDir, filePath)
if relErr == nil {
rel = filepath.ToSlash(rel)
if rel != "" && rel != "." && !strings.HasPrefix(rel, "../") {
relativePath = rel
}
}
}
composeCtx.meta.Files = append(composeCtx.meta.Files, composeBackupFile{
OriginalPath: filePath,
FileName: path.Base(filePath),
RelativePath: relativePath,
BackupPath: path.Join("compose_files", backupName),
})
}
if len(composeCtx.composeFiles) != 0 {
envPath := path.Join(path.Dir(composeCtx.composeFiles[0]), ".env")
if composeCtx.fileOp.Stat(envPath) {
envContent, err := os.ReadFile(envPath)
if err != nil {
return err
}
if err := composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_files", ".env"), string(envContent), fs.ModePerm); err != nil {
return err
}
}
}
return nil
}
func stepWriteComposeBackupMeta(composeCtx *composeBackupContext) error {
metaBytes, err := json.MarshalIndent(composeCtx.meta, "", " ")
if err != nil {
return err
}
return composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_meta.json"), string(metaBytes), fs.ModePerm)
}
func stepPackComposeBackup(composeCtx *composeBackupContext) error {
return composeCtx.fileOp.TarGzCompressPro(true, composeCtx.tmpDir, composeCtx.filePath, composeCtx.req.Secret, "")
}
func newComposeRecoverContext(req dto.CommonRecover) (*composeRecoverContext, error) {
tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz"))
ctx := &composeRecoverContext{
req: req,
fileOp: files.NewFileOp(),
tmpDir: tmpDir,
meta: composeBackupMeta{
Files: make([]composeBackupFile, 0),
Containers: make([]string, 0),
},
}
return ctx, nil
}
func (c *composeRecoverContext) close() {
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
}
}
func stepPrepareComposeRecover(recoverCtx *composeRecoverContext) error {
if !recoverCtx.fileOp.Stat(recoverCtx.req.File) {
return buserr.WithName("ErrFileNotFound", recoverCtx.req.File)
}
_ = os.RemoveAll(recoverCtx.tmpDir)
return nil
}
func stepExtractComposeRecover(recoverCtx *composeRecoverContext) error {
return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret)
}
func stepLoadComposeRecoverMeta(recoverCtx *composeRecoverContext) error {
metaPath := path.Join(recoverCtx.tmpDir, "compose_meta.json")
if !recoverCtx.fileOp.Stat(metaPath) {
return fmt.Errorf("compose_meta.json not found in backup file")
}
metaBytes, err := os.ReadFile(metaPath)
if err != nil {
return err
}
if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil {
return fmt.Errorf("unmarshal compose_meta.json failed, err: %v", err)
}
recoverCtx.composeName = strings.TrimSpace(recoverCtx.req.Name)
if recoverCtx.composeName == "" {
recoverCtx.composeName = strings.TrimSpace(recoverCtx.meta.ComposeName)
}
if recoverCtx.composeName == "" {
return fmt.Errorf("compose name not found in recover request or backup file")
}
recoverCtx.targetDir = resolveComposeRecoverTargetDir(recoverCtx.meta, recoverCtx.composeName)
return nil
}
func resolveComposeRecoverTargetDir(meta composeBackupMeta, composeName string) string {
composePath := strings.TrimSpace(meta.ComposePath)
if composePath != "" {
items := strings.Split(composePath, ",")
for _, item := range items {
p := strings.TrimSpace(item)
if p == "" {
continue
}
ext := strings.ToLower(path.Ext(p))
if ext == ".yml" || ext == ".yaml" {
return path.Dir(p)
}
return p
}
}
return path.Join(global.Dir.DataDir, "docker/compose", composeName)
}
func safeJoinWithinBase(baseDir, name string) (string, error) {
base := filepath.Clean(baseDir)
candidate := strings.TrimSpace(name)
candidate = strings.ReplaceAll(candidate, "\\", "/")
candidate = filepath.Clean(filepath.FromSlash(candidate))
if candidate == "" || candidate == "." {
return "", fmt.Errorf("invalid path: empty")
}
if filepath.IsAbs(candidate) {
return "", fmt.Errorf("invalid path %q: absolute path is not allowed", name)
}
if candidate == ".." || strings.HasPrefix(candidate, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
}
resolved := filepath.Clean(filepath.Join(base, candidate))
rel, err := filepath.Rel(base, resolved)
if err != nil {
return "", fmt.Errorf("resolve path %q failed, err: %v", name, err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
}
return resolved, nil
}
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if recoverCtx.targetDir != "" {
_ = os.RemoveAll(recoverCtx.targetDir)
}
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
return err
}
restored := make([]string, 0, len(recoverCtx.meta.Files))
for _, item := range recoverCtx.meta.Files {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
}
if !recoverCtx.fileOp.Stat(backupPath) {
continue
}
targetName := item.FileName
if item.RelativePath != "" {
targetName = item.RelativePath
}
if targetName == "" {
targetName = path.Base(item.OriginalPath)
}
if targetName == "" {
targetName = "docker-compose.yml"
}
targetPath, err := safeJoinWithinBase(recoverCtx.targetDir, targetName)
if err != nil {
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
}
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
return err
}
content, err := os.ReadFile(backupPath)
if err != nil {
return err
}
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
return err
}
restored = append(restored, targetPath)
}
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
if recoverCtx.fileOp.Stat(envPath) {
envContent, err := os.ReadFile(envPath)
if err != nil {
return err
}
if err := recoverCtx.fileOp.SaveFile(path.Join(recoverCtx.targetDir, ".env"), string(envContent), fs.ModePerm); err != nil {
return err
}
}
if len(restored) == 0 {
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
if !recoverCtx.fileOp.Stat(defaultPath) {
return fmt.Errorf("compose file not found in backup data")
}
restored = append(restored, defaultPath)
}
sort.Strings(restored)
recoverCtx.composePath = strings.Join(restored, ",")
return nil
}
func stepSaveComposeRecord(recoverCtx *composeRecoverContext) error {
if recoverCtx.composePath == "" {
recoverCtx.composePath = path.Join(recoverCtx.targetDir, "docker-compose.yml")
}
recordName := strings.ToLower(recoverCtx.composeName)
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
if record.ID == 0 {
return composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: recoverCtx.composePath})
}
return composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": recoverCtx.composePath})
}
func sanitizeComposeFileName(in string) string {
name := strings.TrimSpace(in)
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, ":", "_")
if name == "" {
return "container"
}
return name
}
+914
View File
@@ -0,0 +1,914 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path"
"sort"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
)
type containerBackupMeta struct {
ContainerName string `json:"containerName"`
ContainerID string `json:"containerID"`
CreatedAt string `json:"createdAt"`
Image string `json:"image"`
HostConfig *container.HostConfig `json:"hostConfig,omitempty"`
Config *container.Config `json:"config,omitempty"`
Mounts []containerMountBackup `json:"mounts"`
}
type containerMountBackup struct {
Type string `json:"type"`
Name string `json:"name,omitempty"`
Source string `json:"source,omitempty"`
Destination string `json:"destination"`
Mode string `json:"mode,omitempty"`
RW bool `json:"rw"`
Propagation string `json:"propagation,omitempty"`
BackupPath string `json:"backupPath,omitempty"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
}
type containerBackupContext struct {
containerName string
backupDir string
fileName string
secret string
filePath string
tmpDir string
mountRoot string
wasRunning bool
stopped bool
fileOp files.FileOp
inspectInfo container.InspectResponse
meta containerBackupMeta
}
type containerRecoverContext struct {
req dto.CommonRecover
targetName string
fileOp files.FileOp
client *client.Client
tmpDir string
meta containerBackupMeta
inspectInfo container.InspectResponse
shouldStart bool
createdContainerID string
}
func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
fileName := req.FileName
if fileName == "" {
fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow)
}
if !strings.HasSuffix(fileName, ".tar.gz") {
fileName += ".tar.gz"
}
itemDir := fmt.Sprintf("container/%s", req.Name)
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
record := &model.BackupRecord{
Type: req.Type,
Name: req.Name,
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 := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
}
func (u *BackupService) ContainerRecover(req dto.CommonRecover) error {
return handleContainerRecover(req, nil)
}
func handleContainerBackup(containerName string, parentTask *task.Task, recordID uint, backupDir, fileName, taskID, secret string, stopBefore bool) error {
var (
err error
backupTask *task.Task
)
backupCtx, err := newContainerBackupContext(containerName, backupDir, fileName, secret)
if err != nil {
return err
}
backupTask = parentTask
if backupTask == nil {
backupTask, err = task.NewTaskWithOps(containerName, task.TaskBackup, task.TaskScopeBackup, taskID, 1)
if err != nil {
return err
}
}
if stopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStop"), func(t *task.Task) error {
return stepStopContainerForBackup(backupCtx)
}, func(t *task.Task) {
_ = stepStartContainerAfterBackup(backupCtx)
}, 3, time.Hour)
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupPrepare"), func(t *task.Task) error {
t.Logf("------------------ %s ------------------", containerName)
return stepPrepareContainerBackup(backupCtx)
}, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupInspect"), func(t *task.Task) error { return stepBackupContainerInspect(backupCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMounts"), func(t *task.Task) error { return stepBackupContainerMounts(backupCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMeta"), func(t *task.Task) error { return stepWriteContainerMeta(backupCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(task.GetTaskName(containerName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return stepPackContainerBackup(backupCtx) }, nil, 3, time.Hour)
if stopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStart"), func(t *task.Task) error {
return stepStartContainerAfterBackup(backupCtx)
}, nil, 3, time.Hour)
}
if parentTask != nil {
return nil
}
go func() {
defer backupCtx.close()
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 handleContainerRecover(req dto.CommonRecover, parentTask *task.Task) error {
var (
err error
recoverTask *task.Task
recoverCtx *containerRecoverContext
)
recoverTask = parentTask
if recoverTask == nil {
if isImportRecover(req) {
taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Container")
recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
} else {
recoverTask, err = task.NewTaskWithOps("container", task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
}
}
timeout := loadRecoverTimeout(req.Timeout)
logName := strings.TrimSpace(req.Name)
if logName == "" && req.File != "" {
logName = strings.TrimSuffix(path.Base(req.File), ".tar.gz")
}
if logName == "" {
logName = "container"
}
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverPrepare"), func(t *task.Task) error {
ctx, err := newContainerRecoverContext(req)
if err != nil {
return err
}
recoverCtx = ctx
t.Logf("------------------ %s ------------------", logName)
if err := stepPrepareContainerRecover(recoverCtx); err != nil {
recoverCtx.close()
recoverCtx = nil
return err
}
return nil
}, func(t *task.Task) {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
}, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverExtract"), func(t *task.Task) error { return stepExtractContainerRecover(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverParse"), func(t *task.Task) error { return stepLoadContainerRecoverData(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCreate"), func(t *task.Task) error { return stepRecreateContainer(recoverCtx, t) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverMounts"), func(t *task.Task) error { return stepRestoreContainerMounts(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverStart"), func(t *task.Task) error { return stepStartRecoveredContainer(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCleanup"), func(t *task.Task) error {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
return nil
}, nil, 0, timeout)
if parentTask != nil {
return nil
}
go func() {
_ = recoverTask.Execute()
}()
return nil
}
func loadRecoverTimeout(timeout int) time.Duration {
switch timeout {
case -1:
return 0
case 0:
return 3 * time.Hour
default:
return time.Duration(timeout) * time.Second
}
}
func isImportRecover(req dto.CommonRecover) bool {
return req.BackupRecordID == 0
}
func newContainerBackupContext(containerName, backupDir, fileName, secret string) (*containerBackupContext, error) {
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return nil, err
}
defer func() {
_ = dockerClient.Close()
}()
inspectInfo, err := dockerClient.ContainerInspect(context.Background(), containerName)
if err != nil {
return nil, err
}
filePath := path.Join(backupDir, fileName)
tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz"))
backupCtx := &containerBackupContext{
containerName: containerName,
backupDir: backupDir,
fileName: fileName,
secret: secret,
filePath: filePath,
tmpDir: tmpDir,
mountRoot: path.Join(tmpDir, "mounts"),
wasRunning: inspectInfo.State != nil && inspectInfo.State.Running,
fileOp: files.NewFileOp(),
inspectInfo: inspectInfo,
meta: containerBackupMeta{
ContainerName: containerName,
ContainerID: inspectInfo.ID,
CreatedAt: time.Now().Format(constant.DateTimeLayout),
Image: inspectInfo.Config.Image,
HostConfig: inspectInfo.HostConfig,
Config: inspectInfo.Config,
Mounts: make([]containerMountBackup, 0),
},
}
return backupCtx, nil
}
func newContainerRecoverContext(req dto.CommonRecover) (*containerRecoverContext, error) {
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return nil, err
}
tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz"))
ctx := &containerRecoverContext{
req: req,
targetName: req.Name,
fileOp: files.NewFileOp(),
client: dockerClient,
tmpDir: tmpDir,
meta: containerBackupMeta{
Mounts: make([]containerMountBackup, 0),
},
}
return ctx, nil
}
func (c *containerBackupContext) close() {
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
}
}
func (c *containerRecoverContext) close() {
if c.client != nil {
_ = c.client.Close()
}
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
}
}
func stepPrepareContainerBackup(backupCtx *containerBackupContext) error {
if err := os.MkdirAll(backupCtx.backupDir, os.ModePerm); err != nil {
return fmt.Errorf("mkdir %s failed, err: %v", backupCtx.backupDir, err)
}
_ = os.RemoveAll(backupCtx.tmpDir)
if err := os.MkdirAll(backupCtx.mountRoot, os.ModePerm); err != nil {
return err
}
return nil
}
func stepStopContainerForBackup(backupCtx *containerBackupContext) error {
if !backupCtx.wasRunning || backupCtx.stopped {
return nil
}
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return err
}
defer func() {
_ = dockerClient.Close()
}()
if err := dockerClient.ContainerStop(context.Background(), backupCtx.inspectInfo.ID, container.StopOptions{}); err != nil {
return err
}
backupCtx.stopped = true
return nil
}
func stepStartContainerAfterBackup(backupCtx *containerBackupContext) error {
if !backupCtx.stopped {
return nil
}
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return err
}
defer func() {
_ = dockerClient.Close()
}()
if err := dockerClient.ContainerStart(context.Background(), backupCtx.inspectInfo.ID, container.StartOptions{}); err != nil {
return err
}
backupCtx.stopped = false
return nil
}
func stepBackupContainerInspect(backupCtx *containerBackupContext) error {
inspectBytes, err := json.MarshalIndent(backupCtx.inspectInfo, "", " ")
if err != nil {
return err
}
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "inspect.json"), string(inspectBytes), fs.ModePerm); err != nil {
return err
}
if backupCtx.inspectInfo.NetworkSettings != nil {
networkBytes, err := json.MarshalIndent(backupCtx.inspectInfo.NetworkSettings, "", " ")
if err != nil {
return err
}
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "network.json"), string(networkBytes), fs.ModePerm); err != nil {
return err
}
}
return nil
}
func stepBackupContainerMounts(backupCtx *containerBackupContext) error {
var (
dockerClient *client.Client
clientErr error
)
ensureClient := func() (*client.Client, error) {
if dockerClient != nil || clientErr != nil {
return dockerClient, clientErr
}
dockerClient, clientErr = dockerUtils.NewDockerClient()
return dockerClient, clientErr
}
defer func() {
if dockerClient != nil {
_ = dockerClient.Close()
}
}()
for i, item := range backupCtx.inspectInfo.Mounts {
mountMeta := containerMountBackup{
Type: string(item.Type),
Name: item.Name,
Source: item.Source,
Destination: item.Destination,
Mode: item.Mode,
RW: item.RW,
Propagation: string(item.Propagation),
Status: "skipped",
}
mountDirName := fmt.Sprintf("%02d_%s", i, sanitizeContainerMountName(item.Destination))
mountDir := path.Join(backupCtx.mountRoot, mountDirName)
mountMeta.BackupPath = path.Join("mounts", mountDirName, "data")
switch item.Type {
case mount.TypeBind, mount.TypeVolume:
if item.Source == "" {
mountMeta.Message = "empty source"
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
continue
}
sourceInfo, statErr := os.Stat(item.Source)
if statErr != nil {
mountMeta.Message = statErr.Error()
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
continue
}
dataDir := path.Join(mountDir, "data")
if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
return err
}
if sourceInfo.IsDir() {
if err := backupCtx.fileOp.CopyDirWithNewName(item.Source, dataDir, "."); err != nil {
return err
}
} else {
if err := backupCtx.fileOp.CopyFile(item.Source, dataDir); err != nil {
return err
}
}
if item.Type == mount.TypeVolume && item.Name != "" {
cli, err := ensureClient()
if err != nil {
return err
}
volumeInfo, volumeErr := cli.VolumeInspect(context.Background(), item.Name)
if volumeErr == nil {
volumeBytes, volumeMarshalErr := json.MarshalIndent(volumeInfo, "", " ")
if volumeMarshalErr == nil {
_ = backupCtx.fileOp.SaveFile(path.Join(mountDir, "volume.json"), string(volumeBytes), fs.ModePerm)
}
}
}
mountMeta.Status = "backed_up"
default:
mountMeta.Message = "mount type not supported for data backup"
}
backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta)
}
return nil
}
func stepWriteContainerMeta(backupCtx *containerBackupContext) error {
metaBytes, err := json.MarshalIndent(backupCtx.meta, "", " ")
if err != nil {
return err
}
if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "meta.json"), string(metaBytes), fs.ModePerm); err != nil {
return err
}
return nil
}
func stepPackContainerBackup(backupCtx *containerBackupContext) error {
if err := backupCtx.fileOp.TarGzCompressPro(true, backupCtx.tmpDir, backupCtx.filePath, backupCtx.secret, ""); err != nil {
return err
}
return nil
}
func stepPrepareContainerRecover(recoverCtx *containerRecoverContext) error {
if !recoverCtx.fileOp.Stat(recoverCtx.req.File) {
return buserr.WithName("ErrFileNotFound", recoverCtx.req.File)
}
_ = os.RemoveAll(recoverCtx.tmpDir)
return nil
}
func stepExtractContainerRecover(recoverCtx *containerRecoverContext) error {
return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret)
}
func stepLoadContainerRecoverData(recoverCtx *containerRecoverContext) error {
if err := loadContainerRecoverMeta(recoverCtx); err != nil {
return err
}
if err := loadContainerRecoverInspect(recoverCtx); err != nil {
return err
}
if recoverCtx.targetName == "" {
recoverCtx.targetName = strings.TrimPrefix(recoverCtx.inspectInfo.Name, "/")
}
if recoverCtx.targetName == "" {
recoverCtx.targetName = recoverCtx.meta.ContainerName
}
if recoverCtx.targetName == "" {
return fmt.Errorf("container name not found in recover request or backup file")
}
if recoverCtx.inspectInfo.Config == nil {
recoverCtx.inspectInfo.Config = recoverCtx.meta.Config
}
if recoverCtx.inspectInfo.HostConfig == nil {
recoverCtx.inspectInfo.HostConfig = recoverCtx.meta.HostConfig
}
if recoverCtx.inspectInfo.Config == nil {
return fmt.Errorf("container config not found in backup file")
}
if recoverCtx.inspectInfo.HostConfig == nil {
recoverCtx.inspectInfo.HostConfig = &container.HostConfig{}
}
recoverCtx.shouldStart = recoverCtx.inspectInfo.State != nil && recoverCtx.inspectInfo.State.Running
return nil
}
func loadContainerRecoverMeta(recoverCtx *containerRecoverContext) error {
metaPath := path.Join(recoverCtx.tmpDir, "meta.json")
if !recoverCtx.fileOp.Stat(metaPath) {
return nil
}
metaBytes, err := os.ReadFile(metaPath)
if err != nil {
return err
}
if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil {
return fmt.Errorf("unmarshal meta.json failed, err: %v", err)
}
return nil
}
func loadContainerRecoverInspect(recoverCtx *containerRecoverContext) error {
inspectPath := path.Join(recoverCtx.tmpDir, "inspect.json")
if !recoverCtx.fileOp.Stat(inspectPath) {
return fmt.Errorf("inspect.json not found in backup file")
}
inspectBytes, err := os.ReadFile(inspectPath)
if err != nil {
return err
}
if err := json.Unmarshal(inspectBytes, &recoverCtx.inspectInfo); err != nil {
return fmt.Errorf("unmarshal inspect.json failed, err: %v", err)
}
return nil
}
func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.Task) error {
ctx := context.Background()
if err := ensureContainerRecoverNetworks(recoverCtx); err != nil {
return err
}
if err := ensureContainerRecoverVolumes(recoverCtx); err != nil {
return err
}
config := cloneContainerConfig(recoverCtx.inspectInfo.Config)
hostConfig := cloneContainerHostConfig(recoverCtx.inspectInfo.HostConfig)
if config.Image == "" {
config.Image = recoverCtx.meta.Image
}
if config.Image == "" {
return fmt.Errorf("container image not found in backup file")
}
if !checkImageExist(recoverCtx.client, config.Image) {
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
return err
}
}
if _, err := recoverCtx.client.ContainerInspect(ctx, recoverCtx.targetName); err == nil {
if err := recoverCtx.client.ContainerRemove(ctx, recoverCtx.targetName, container.RemoveOptions{Force: true, RemoveVolumes: false}); err != nil {
return err
}
} else if !client.IsErrNotFound(err) {
return err
}
networkConfig, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
removeBridgeDriverIPAM(recoverCtx.client, networkConfig, extraNetworks)
createRes, err := recoverCtx.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, recoverCtx.targetName)
if err != nil {
return err
}
recoverCtx.createdContainerID = createRes.ID
extraNames := make([]string, 0, len(extraNetworks))
for name := range extraNetworks {
extraNames = append(extraNames, name)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := recoverCtx.client.NetworkConnect(ctx, item, recoverCtx.createdContainerID, extraNetworks[item]); err != nil {
return err
}
}
return nil
}
func removeBridgeDriverIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if primary != nil {
removeBridgeDriverIPAMFromEndpoints(cli, primary.EndpointsConfig)
}
removeBridgeDriverIPAMFromEndpoints(cli, extras)
}
func removeBridgeDriverIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
if err != nil {
continue
}
if info.Driver == "bridge" {
endpoint.IPAMConfig = nil
}
}
}
func ensureContainerRecoverNetworks(recoverCtx *containerRecoverContext) error {
if recoverCtx.inspectInfo.NetworkSettings == nil {
return nil
}
for netName := range recoverCtx.inspectInfo.NetworkSettings.Networks {
if netName == "" || netName == "bridge" || netName == "host" || netName == "none" {
continue
}
if _, err := recoverCtx.client.NetworkInspect(context.Background(), netName, network.InspectOptions{}); err != nil {
if !client.IsErrNotFound(err) {
return err
}
if _, err := recoverCtx.client.NetworkCreate(context.Background(), netName, network.CreateOptions{Driver: "bridge"}); err != nil {
return err
}
}
}
return nil
}
func ensureContainerRecoverVolumes(recoverCtx *containerRecoverContext) error {
for _, item := range recoverCtx.meta.Mounts {
if item.Type != string(mount.TypeVolume) || item.Name == "" {
continue
}
if _, err := recoverCtx.client.VolumeInspect(context.Background(), item.Name); err == nil {
continue
} else if !client.IsErrNotFound(err) {
return err
}
createOptions := volume.CreateOptions{Name: item.Name}
if item.BackupPath != "" {
volumeMetaPath := path.Join(recoverCtx.tmpDir, path.Dir(item.BackupPath), "volume.json")
if recoverCtx.fileOp.Stat(volumeMetaPath) {
volumeBytes, readErr := os.ReadFile(volumeMetaPath)
if readErr != nil {
return readErr
}
var volumeInfo volume.Volume
if unmarshalErr := json.Unmarshal(volumeBytes, &volumeInfo); unmarshalErr != nil {
return unmarshalErr
}
if volumeInfo.Driver != "" {
createOptions.Driver = volumeInfo.Driver
}
if len(volumeInfo.Options) != 0 {
createOptions.DriverOpts = volumeInfo.Options
}
if len(volumeInfo.Labels) != 0 {
createOptions.Labels = volumeInfo.Labels
}
}
}
if _, err := recoverCtx.client.VolumeCreate(context.Background(), createOptions); err != nil {
return err
}
}
return nil
}
func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettings, hostConfig *container.HostConfig) (*network.NetworkingConfig, map[string]*network.EndpointSettings) {
extraNetworks := make(map[string]*network.EndpointSettings)
if hostConfig != nil {
networkMode := string(hostConfig.NetworkMode)
if networkMode == "host" || networkMode == "none" {
return nil, extraNetworks
}
}
if networkSettings == nil || len(networkSettings.Networks) == 0 {
return nil, extraNetworks
}
primaryName := ""
if hostConfig != nil {
networkMode := string(hostConfig.NetworkMode)
if networkMode != "" && networkMode != "default" && networkMode != "bridge" {
if _, ok := networkSettings.Networks[networkMode]; ok {
primaryName = networkMode
}
}
}
if primaryName == "" {
if _, ok := networkSettings.Networks["bridge"]; ok {
primaryName = "bridge"
} else {
names := make([]string, 0, len(networkSettings.Networks))
for name := range networkSettings.Networks {
names = append(names, name)
}
sort.Strings(names)
if len(names) > 0 {
primaryName = names[0]
}
}
}
config := &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}
for name, endpoint := range networkSettings.Networks {
if name == "host" || name == "none" {
continue
}
endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
if endpoint.IPAMConfig != nil {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address,
}
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address,
}
}
if name == primaryName {
config.EndpointsConfig[name] = endpointSetting
} else {
extraNetworks[name] = endpointSetting
}
}
if len(config.EndpointsConfig) == 0 {
return nil, extraNetworks
}
return config, extraNetworks
}
func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil {
return &container.Config{}
}
item := *config
if len(config.Env) != 0 {
item.Env = append([]string(nil), config.Env...)
}
if len(config.Cmd) != 0 {
item.Cmd = append([]string(nil), config.Cmd...)
}
if len(config.Entrypoint) != 0 {
item.Entrypoint = append([]string(nil), config.Entrypoint...)
}
if len(config.Labels) != 0 {
labels := make(map[string]string, len(config.Labels))
for key, val := range config.Labels {
labels[key] = val
}
item.Labels = labels
}
if len(config.Volumes) != 0 {
volumes := make(map[string]struct{}, len(config.Volumes))
for key, val := range config.Volumes {
volumes[key] = val
}
item.Volumes = volumes
}
return &item
}
func cloneContainerHostConfig(hostConfig *container.HostConfig) *container.HostConfig {
if hostConfig == nil {
return &container.HostConfig{}
}
item := *hostConfig
if len(hostConfig.Binds) != 0 {
item.Binds = append([]string(nil), hostConfig.Binds...)
}
if len(hostConfig.DNS) != 0 {
item.DNS = append([]string(nil), hostConfig.DNS...)
}
if len(hostConfig.ExtraHosts) != 0 {
item.ExtraHosts = append([]string(nil), hostConfig.ExtraHosts...)
}
if len(hostConfig.Mounts) != 0 {
item.Mounts = append([]mount.Mount(nil), hostConfig.Mounts...)
}
return &item
}
func stepRestoreContainerMounts(recoverCtx *containerRecoverContext) error {
currentContainer := recoverCtx.createdContainerID
if currentContainer == "" {
currentContainer = recoverCtx.targetName
}
currentInspect, err := recoverCtx.client.ContainerInspect(context.Background(), currentContainer)
if err != nil {
return err
}
currentMounts := make(map[string]container.MountPoint, len(currentInspect.Mounts))
for _, item := range currentInspect.Mounts {
currentMounts[item.Destination] = item
}
for _, item := range recoverCtx.meta.Mounts {
if item.Status != "backed_up" || item.BackupPath == "" || !item.RW {
continue
}
backupPath := path.Join(recoverCtx.tmpDir, item.BackupPath)
if !recoverCtx.fileOp.Stat(backupPath) {
continue
}
sourcePath := item.Source
if currentMount, ok := currentMounts[item.Destination]; ok {
if currentMount.Source != "" {
sourcePath = currentMount.Source
}
if item.Type == string(mount.TypeVolume) && item.Name == "" {
item.Name = currentMount.Name
}
}
if sourcePath == "" && item.Type == string(mount.TypeVolume) && item.Name != "" {
volumeInfo, volumeErr := recoverCtx.client.VolumeInspect(context.Background(), item.Name)
if volumeErr != nil {
return volumeErr
}
sourcePath = volumeInfo.Mountpoint
}
if sourcePath == "" {
continue
}
if err := restoreContainerMountData(recoverCtx.fileOp, backupPath, sourcePath); err != nil {
return err
}
}
return nil
}
func restoreContainerMountData(fileOp files.FileOp, backupPath, sourcePath string) error {
if sourcePath == "/" {
return fmt.Errorf("invalid mount source path /")
}
entries, err := os.ReadDir(backupPath)
if err != nil {
return err
}
if len(entries) == 1 && !entries[0].IsDir() && entries[0].Name() == path.Base(sourcePath) {
if err := os.MkdirAll(path.Dir(sourcePath), os.ModePerm); err != nil {
return err
}
_ = os.RemoveAll(sourcePath)
if err := fileOp.CopyFile(path.Join(backupPath, entries[0].Name()), path.Dir(sourcePath)); err != nil {
return err
}
return nil
}
_ = os.RemoveAll(sourcePath)
if err := os.MkdirAll(sourcePath, os.ModePerm); err != nil {
return err
}
if err := fileOp.CopyDirWithNewName(backupPath, sourcePath, "."); err != nil {
return err
}
return nil
}
func stepStartRecoveredContainer(recoverCtx *containerRecoverContext) error {
if !recoverCtx.shouldStart {
return nil
}
containerID := recoverCtx.createdContainerID
if containerID == "" {
containerID = recoverCtx.targetName
}
return recoverCtx.client.ContainerStart(context.Background(), containerID, container.StartOptions{})
}
func sanitizeContainerMountName(in string) string {
name := strings.TrimSpace(in)
name = strings.Trim(name, "/")
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, ":", "_")
if name == "" {
return "root"
}
return name
}
+459
View File
@@ -1,7 +1,9 @@
package service
import (
"archive/tar"
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -11,6 +13,7 @@ import (
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strconv"
@@ -40,6 +43,7 @@ import (
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/gin-gonic/gin"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -89,6 +93,12 @@ type IContainerService interface {
Prune(req dto.ContainerPrune) error
LoadUsers(req dto.OperationWithName) []string
ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error)
UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error
GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error)
GetContainerFileSize(req dto.ContainerFileReq) (int64, error)
DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error
DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error)
StreamLogs(ctx *gin.Context, params dto.StreamLog)
}
@@ -1166,6 +1176,455 @@ func (u *ContainerService) LoadUsers(req dto.OperationWithName) []string {
return users
}
func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error) {
if len(req.Path) == 0 {
req.Path = "/"
}
cli, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer cli.Close()
ctx := context.Background()
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
return nil, normalizeContainerFileError(err)
}
isDir := stat.Mode.IsDir()
isLink := stat.Mode&os.ModeSymlink != 0
if isLink && !isDir {
linkDir, linkErr := isContainerDir(cli, req.ContainerID, req.Path)
if linkErr == nil {
isDir = linkDir
}
}
if !isDir {
return []dto.ContainerFileInfo{toContainerFileInfo(req.Path, stat, isDir)}, nil
}
output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
if err != nil {
return nil, normalizeContainerFileError(err)
}
lines := strings.Split(strings.TrimSpace(output), "\n")
files := make([]dto.ContainerFileInfo, 0, len(lines))
for _, line := range lines {
name := strings.TrimSpace(line)
if len(name) == 0 || name == "." || name == ".." {
continue
}
childPath := req.Path
if childPath == "/" {
childPath = "/" + name
} else {
childPath = strings.TrimSuffix(childPath, "/") + "/" + name
}
childStat, statErr := cli.ContainerStatPath(ctx, req.ContainerID, childPath)
if statErr != nil {
continue
}
childIsDir := childStat.Mode.IsDir()
if childStat.Mode&os.ModeSymlink != 0 && !childIsDir {
linkDir, linkErr := isContainerDir(cli, req.ContainerID, childPath)
if linkErr == nil {
childIsDir = linkDir
}
}
files = append(files, toContainerFileInfo(childPath, childStat, childIsDir))
}
sort.Slice(files, func(i, j int) bool {
if files[i].IsDir != files[j].IsDir {
return files[i].IsDir
}
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
})
return files, nil
}
func (u *ContainerService) DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error {
for _, item := range req.Paths {
if strings.TrimSpace(item) == "/" {
return buserr.New("ErrPathNotDelete")
}
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
command := []string{"rm", "-rf", "--"}
command = append(command, req.Paths...)
_, err = runContainerCommand(cli, req.ContainerID, command)
return err
}
func (u *ContainerService) UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error {
if len(req.Path) == 0 {
req.Path = "/"
}
safeName := path.Base(fileName)
if safeName == "." || safeName == "/" || len(safeName) == 0 {
return buserr.New("ErrInvalidChar")
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
ctx := context.Background()
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
if _, mkErr := runContainerCommand(cli, req.ContainerID, []string{"mkdir", "-p", "--", req.Path}); mkErr != nil {
return mkErr
}
stat, err = cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
return err
}
}
if !stat.Mode.IsDir() {
return fmt.Errorf("path %s is not directory", req.Path)
}
pipeReader, pipeWriter := io.Pipe()
writeErr := make(chan error, 1)
go func() {
tw := tar.NewWriter(pipeWriter)
header := &tar.Header{
Name: safeName,
Mode: 0644,
Size: fileSize,
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
_ = tw.Close()
_ = pipeWriter.CloseWithError(err)
writeErr <- err
return
}
if _, err := io.Copy(tw, file); err != nil {
_ = tw.Close()
_ = pipeWriter.CloseWithError(err)
writeErr <- err
return
}
if err := tw.Close(); err != nil {
_ = pipeWriter.CloseWithError(err)
writeErr <- err
return
}
_ = pipeWriter.Close()
writeErr <- nil
}()
err = cli.CopyToContainer(ctx, req.ContainerID, req.Path, pipeReader, container.CopyToContainerOptions{
CopyUIDGID: true,
})
if err != nil {
_ = pipeReader.CloseWithError(err)
_ = pipeWriter.CloseWithError(err)
<-writeErr
return err
}
if err := <-writeErr; err != nil {
return err
}
return nil
}
func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error) {
if len(req.Path) == 0 {
return nil, buserr.New("ErrInvalidChar")
}
cli, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer cli.Close()
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return nil, normalizeContainerFileError(err)
}
if stat.Mode.IsDir() {
return nil, fmt.Errorf("path %s is directory", req.Path)
}
content := &dto.ContainerFileContent{Size: stat.Size}
headBytes, err := runContainerCommandRaw(cli, req.ContainerID, []string{"head", "-c", "4096", "--", req.Path})
if err != nil {
return nil, err
}
if bytes.IndexByte(headBytes, 0) >= 0 {
content.IsBinary = true
return content, nil
}
const inlinePreviewMax = 512 * 1024
if stat.Size <= inlinePreviewMax {
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"cat", "--", req.Path})
if err != nil {
return nil, err
}
content.Content = string(raw)
return content, nil
}
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"tail", "-n", "300", "--", req.Path})
if err != nil {
return nil, err
}
content.Content = string(raw)
content.Truncated = true
return content, nil
}
func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64, error) {
if len(req.Path) == 0 {
return 0, buserr.New("ErrInvalidChar")
}
cli, err := docker.NewDockerClient()
if err != nil {
return 0, err
}
defer cli.Close()
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return 0, normalizeContainerFileError(err)
}
if !stat.Mode.IsDir() {
return stat.Size, nil
}
output, err := runContainerCommand(cli, req.ContainerID, []string{"du", "-sb", "--", req.Path})
if err != nil {
return 0, err
}
parts := strings.Fields(output)
if len(parts) == 0 {
return 0, fmt.Errorf("invalid du output")
}
size, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, err
}
return size, nil
}
func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error) {
if len(req.Path) == 0 {
req.Path = "/"
}
cli, err := docker.NewDockerClient()
if err != nil {
return nil, "", "", err
}
ctx := context.Background()
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
_ = cli.Close()
return nil, "", "", normalizeContainerFileError(err)
}
fileName := stat.Name
if len(fileName) == 0 {
fileName = "container-file"
}
if stat.Mode.IsDir() {
if _, err := runContainerCommand(cli, req.ContainerID, []string{"sh", "-c", "command -v tar >/dev/null 2>&1"}); err != nil {
_ = cli.Close()
return nil, "", "", fmt.Errorf("tar command not found in container")
}
targetPath := path.Clean(req.Path)
parentPath := path.Dir(targetPath)
targetName := path.Base(targetPath)
if parentPath == "." || parentPath == "" {
parentPath = "/"
}
tarStream, err := runContainerCommandStream(cli, req.ContainerID, []string{
"tar", "-czf", "-", "-C", parentPath, "--", targetName,
})
if err != nil {
_ = cli.Close()
return nil, "", "", err
}
if !strings.HasSuffix(fileName, ".tar.gz") {
fileName += ".tar.gz"
}
return &closeHookReader{
ReadCloser: tarStream,
onClose: cli.Close,
}, fileName, "application/gzip", nil
}
fileStream, err := runContainerCommandStream(cli, req.ContainerID, []string{"cat", "--", req.Path})
if err != nil {
_ = cli.Close()
return nil, "", "", err
}
return &closeHookReader{
ReadCloser: fileStream,
onClose: cli.Close,
}, fileName, "application/octet-stream", nil
}
func normalizeContainerFileError(err error) error {
if err == nil {
return nil
}
message := strings.ToLower(err.Error())
if strings.Contains(message, "no such file or directory") || strings.Contains(message, "not found") {
return buserr.New("ErrPathNotFound")
}
return err
}
func runContainerCommand(cli *client.Client, containerID string, command []string) (string, error) {
raw, err := runContainerCommandRaw(cli, containerID, command)
if err != nil {
return "", err
}
return strings.TrimSpace(string(raw)), nil
}
type closeHookReader struct {
io.ReadCloser
onClose func() error
}
func (r *closeHookReader) Close() error {
var closeErr error
if r.ReadCloser != nil {
closeErr = r.ReadCloser.Close()
}
if r.onClose != nil {
if err := r.onClose(); err != nil && closeErr == nil {
closeErr = err
}
}
return closeErr
}
func runContainerCommandRaw(cli *client.Client, containerID string, command []string) ([]byte, error) {
ctx := context.Background()
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
Cmd: command,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return nil, err
}
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
if err != nil {
return nil, err
}
defer hijack.Close()
raw, err := io.ReadAll(hijack.Reader)
if err != nil {
return nil, err
}
var stdout bytes.Buffer
var stderr bytes.Buffer
if _, err := stdcopy.StdCopy(&stdout, &stderr, bytes.NewReader(raw)); err != nil {
return nil, err
}
output := strings.TrimSpace(stdout.String())
errorOutput := strings.TrimSpace(stderr.String())
info, err := cli.ContainerExecInspect(ctx, resp.ID)
if err != nil {
return nil, err
}
if info.ExitCode != 0 {
if len(errorOutput) != 0 {
return nil, fmt.Errorf("%s", errorOutput)
}
if len(output) == 0 {
return nil, fmt.Errorf("container command failed with exit code %d", info.ExitCode)
}
return nil, fmt.Errorf("%s", output)
}
return stdout.Bytes(), nil
}
func runContainerCommandStream(cli *client.Client, containerID string, command []string) (io.ReadCloser, error) {
ctx := context.Background()
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
Cmd: command,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return nil, err
}
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
if err != nil {
return nil, err
}
pipeReader, pipeWriter := io.Pipe()
go func() {
defer hijack.Close()
var stderr bytes.Buffer
_, copyErr := stdcopy.StdCopy(pipeWriter, &stderr, hijack.Reader)
if copyErr != nil {
_ = pipeWriter.CloseWithError(copyErr)
return
}
info, inspectErr := cli.ContainerExecInspect(ctx, resp.ID)
if inspectErr != nil {
_ = pipeWriter.CloseWithError(inspectErr)
return
}
if info.ExitCode != 0 {
msg := strings.TrimSpace(stderr.String())
if len(msg) == 0 {
msg = fmt.Sprintf("container command failed with exit code %d", info.ExitCode)
}
_ = pipeWriter.CloseWithError(fmt.Errorf("%s", msg))
return
}
_ = pipeWriter.Close()
}()
return pipeReader, nil
}
func toContainerFileInfo(filePath string, stat container.PathStat, isDir bool) dto.ContainerFileInfo {
name := stat.Name
if len(name) == 0 {
items := strings.Split(strings.TrimSuffix(filePath, "/"), "/")
name = items[len(items)-1]
}
isLink := stat.Mode&os.ModeSymlink != 0
return dto.ContainerFileInfo{
Name: name,
Path: filePath,
IsDir: isDir,
IsLink: isLink,
LinkTo: stat.LinkTarget,
Size: stat.Size,
Mode: stat.Mode.String(),
ModTime: stat.Mtime.Format(constant.DateTimeLayout),
}
}
func isContainerDir(cli *client.Client, containerID, targetPath string) (bool, error) {
_, err := runContainerCommand(cli, containerID, []string{
"sh", "-c", "[ -d \"$1\" ]", "sh", targetPath,
})
if err != nil {
return false, err
}
return true, nil
}
func stringsToMap(list []string) map[string]string {
var labelMap = make(map[string]string)
for _, label := range list {
+29 -2
View File
@@ -135,6 +135,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap {
value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
records = append(records, value)
}
if len(req.Info) != 0 {
@@ -164,6 +165,29 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return int64(total), listItem, nil
}
func composeFileExists(workdir, configFile string) bool {
workdir = strings.TrimSpace(workdir)
configFile = strings.TrimSpace(configFile)
if configFile == "" {
return false
}
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) && workdir != "" {
file = filepath.Join(workdir, file)
}
file = filepath.Clean(file)
info, err := os.Stat(file)
if err == nil && !info.IsDir() {
return true
}
}
return false
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if cmd.CheckIllegal(req.Path) {
return false, buserr.New("ErrCmdIllegal")
@@ -383,8 +407,11 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
}
func removeContainerForCompose(composeName, composePath string) error {
if stdout, err := compose.Operate(composePath, "down"); err != nil {
return errors.New(stdout)
if _, err := os.Stat(composePath); err == nil {
if stdout, err := compose.Operate(composePath, "down"); err != nil {
return errors.New(stdout)
}
return nil
}
var options container.ListOptions
options.All = true
+11 -3
View File
@@ -284,7 +284,7 @@ func (u *DashboardService) LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher,
return data, err
}
showList, err := launcherRepo.ListName()
showList, _ := launcherRepo.ListName()
defaultList, err := appRepo.GetTopRecommend()
if err != nil {
return data, nil
@@ -361,6 +361,9 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
_ = copier.Copy(&item, &quick)
list = append(list, item)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Recommend < list[j].Recommend
})
return list
}
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
@@ -612,6 +615,9 @@ func loadQuickJump(base *dto.DashboardBase) {
website, _ := websiteRepo.GetBy()
base.WebsiteNumber = len(website)
agents, _ := agentRepo.List()
base.AgentNumber = len(agents)
postgresqlDbs, _ := postgresqlRepo.List()
mysqlDbs, _ := mysqlRepo.List()
base.DatabaseNumber = len(mysqlDbs) + len(postgresqlDbs)
@@ -625,6 +631,8 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
switch quicks[i].Name {
case "Agent":
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
case "Website":
quicks[i].Detail = fmt.Sprintf("%d", base.WebsiteNumber)
case "Database":
@@ -638,7 +646,7 @@ func loadQuickJump(base *dto.DashboardBase) {
_ = copier.Copy(&item, quicks[i])
base.QuickJumps = append(base.QuickJumps, item)
}
sort.Slice(quicks, func(i, j int) bool {
return quicks[i].Recommend < quicks[j].Recommend
sort.Slice(base.QuickJumps, func(i, j int) bool {
return base.QuickJumps[i].Recommend < base.QuickJumps[j].Recommend
})
}
+6 -5
View File
@@ -12,11 +12,12 @@ var (
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
+1 -1
View File
@@ -691,7 +691,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
logFileRes *dto.LogFileRes
)
if stat.Size() > files.MaxReadFileSize {
lines, err = files.TailFromEnd(logFilePath, req.PageSize)
lines, _ = files.TailFromEnd(logFilePath, req.PageSize)
isEndOfFile = true
scope = "tail"
} else {
@@ -4,13 +4,12 @@ import (
"encoding/base64"
"fmt"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/ssh"
"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/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
@@ -75,11 +74,7 @@ func (u *HostService) TestByInfo(req dto.HostConnTest) bool {
}
func (u *HostService) TestLocalConn(id uint) bool {
var (
host model.Host
err error
)
host, err = hostRepo.Get(repo.WithByID(id))
host, err := hostRepo.Get(repo.WithByID(id))
if err != nil {
return false
}
@@ -113,12 +108,11 @@ func (u *HostService) TestLocalConn(id uint) bool {
return false
}
defer client.Close()
return true
}
func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interface{}, error) {
var options []global.DBOption
var options []repo.DBOption
if len(req.Info) != 0 {
options = append(options, hostRepo.WithByInfo(req.Info))
}
@@ -198,8 +192,7 @@ func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, e
func (u *HostService) GetHostByID(id uint) (*dto.HostInfo, error) {
var item dto.HostInfo
var host model.Host
host, _ = hostRepo.Get(repo.WithByID(id))
host, _ := hostRepo.Get(repo.WithByID(id))
if host.ID == 0 {
return nil, buserr.New("ErrRecordNotFound")
}
@@ -336,7 +329,6 @@ func GetHostInfo(id uint) (*model.Host, error) {
return nil, err
}
}
if len(host.PassPhrase) != 0 {
host.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase)
if err != nil {
+3 -3
View File
@@ -289,11 +289,11 @@ func (s *IptablesService) LoadChainStatus(req dto.OperationWithName) dto.Iptable
}
switch req.Name {
case iptables.Chain1PanelBasic:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
case iptables.Chain1PanelInput:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
case iptables.Chain1PanelOutput:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
}
return data
}
+1 -2
View File
@@ -18,7 +18,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/subosito/gotenv"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
@@ -241,7 +240,7 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
return err
}
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
cmdMgr := cmd2.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
if err = cmdMgr.RunBashCf("docker compose -f %s build", nginxInstall.GetComposePath()); err != nil {
return err
}
+62
View File
@@ -3,6 +3,8 @@ package service
import (
"encoding/base64"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -12,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
"github.com/jinzhu/copier"
)
@@ -19,7 +22,9 @@ type SettingService struct{}
type ISettingService interface {
GetSettingInfo() (*dto.SettingInfo, error)
GetTerminalAIInfo() (*dto.TerminalAIInfo, error)
Update(key, value string) error
UpdateTerminalAI(req dto.TerminalAIInfo) error
TestConnByInfo(req dto.SSHConnData) bool
SaveConnInfo(req dto.SSHConnData) error
@@ -57,10 +62,67 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
return &info, err
}
func (u *SettingService) GetTerminalAIInfo() (*dto.TerminalAIInfo, error) {
info := &dto.TerminalAIInfo{
AIStatus: constant.StatusDisable,
AIAccountID: "",
AIPrefix: constant.DefaultTerminalAIPrefix,
AIRiskCommands: "[]",
AIRiskCommandsDefault: constant.DefaultTerminalAIRiskCommands,
}
if value, err := settingRepo.GetValueByKey("AIStatus"); err == nil && value != "" {
info.AIStatus = value
}
if value, err := settingRepo.GetValueByKey("AIAccountID"); err == nil {
info.AIAccountID = value
}
if value, err := settingRepo.GetValueByKey("AIPrefix"); err == nil {
info.AIPrefix = value
}
if value, err := settingRepo.GetValueByKey("AIRiskCommands"); err == nil && value != "" {
info.AIRiskCommands = value
}
return info, nil
}
func (u *SettingService) Update(key, value string) error {
return settingRepo.UpdateOrCreate(key, value)
}
func (u *SettingService) UpdateTerminalAI(req dto.TerminalAIInfo) error {
if strings.EqualFold(strings.TrimSpace(req.AIStatus), constant.StatusEnable) {
accountID, err := strconv.ParseUint(strings.TrimSpace(req.AIAccountID), 10, 64)
if err != nil || accountID == 0 {
return buserr.New("ErrAgentAccountIDRequired")
}
currentStatus, _ := settingRepo.GetValueByKey("AIStatus")
currentAccountID, _ := settingRepo.GetValueByKey("AIAccountID")
needValidate := !strings.EqualFold(strings.TrimSpace(currentStatus), constant.StatusEnable) ||
strings.TrimSpace(currentAccountID) != strings.TrimSpace(req.AIAccountID)
if needValidate {
if err := terminalai.ValidateTerminalAccount(uint(accountID)); err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
}
}
if err := settingRepo.UpdateOrCreate("AIStatus", req.AIStatus); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIAccountID", req.AIAccountID); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIPrefix", req.AIPrefix); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIRiskCommands", req.AIRiskCommands); err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
return nil
}
func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
+15 -20
View File
@@ -389,7 +389,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
appInstall = install
website.AppInstallID = install.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
website.Proxy, err = getAppInstallProxyPass(appInstall)
if err != nil {
return err
}
} else {
var install model.AppInstall
install, err = appInstallRepo.GetFirst(repo.WithByID(create.AppInstallID))
@@ -399,7 +402,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
configApp := func(t *task.Task) error {
appInstall = &install
website.AppInstallID = appInstall.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
website.Proxy, err = getAppInstallProxyPass(appInstall)
if err != nil {
return err
}
return nil
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigApp"), configApp, nil)
@@ -1488,23 +1494,11 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
newBlock.AppendDirectives(ifDir)
}
if website.Type == constant.Deployment {
newBlock.AppendDirectives(
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"Host", "$host"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Real-IP", "$remote_addr"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Forwarded-For", "$proxy_add_x_forwarded_for"},
},
&components.Directive{
Name: "proxy_pass",
Parameters: []string{fmt.Sprintf("http://%s", website.Proxy)},
})
proxyDirectives := getRootProxyDirectives(website.Proxy)
if len(proxyDirectives) == 0 {
return errors.New("failed to build deployment proxy directives")
}
newBlock.AppendDirectives(proxyDirectives...)
}
newDirective.Block = newBlock
index := -1
@@ -1750,7 +1744,7 @@ func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias)
nginxInclude := getWebsiteRedirectInclude(website)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return
}
@@ -2328,6 +2322,7 @@ func (w WebsiteService) ExecComposer(req request.ExecComposerReq) error {
} else {
command = req.ExtCommand
}
command = strings.TrimSpace(command)
resourceName := fmt.Sprintf("composer %s", command)
composerTask, err := task.NewTaskWithOps(resourceName, task.TaskExec, req.Command, req.TaskID, website.ID)
if err != nil {
+12 -10
View File
@@ -114,7 +114,14 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
err = errors.New("invalid proxy config, no location found")
return
}
location.UpdateDirective("proxy_pass", []string{req.ProxyPass})
applyLocationProxyPass(location, req.ProxyPass, &req.SNI, req.ProxySSLName)
if isHTTPSProxyPass(req.ProxyPass) && req.SSLVerify {
location.UpdateDirective("proxy_ssl_verify", []string{"on"})
location.UpdateDirective("proxy_ssl_trusted_certificate", []string{"/etc/ssl/certs/ca-certificates.crt"})
} else {
location.RemoveDirective("proxy_ssl_verify", []string{})
location.RemoveDirective("proxy_ssl_trusted_certificate", []string{})
}
location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost})
location.ChangePath(req.Modifier, req.Match)
// Server Cache Settings
@@ -143,15 +150,7 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
} else {
location.RemoveSubFilter()
}
// SSL Settings
if req.SNI {
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
if req.ProxySSLName != "" {
location.UpdateDirective("proxy_ssl_name", []string{req.ProxySSLName})
}
} else {
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
}
// Explicit SNI configuration still takes precedence over the automatic HTTPS upstream default.
// CORS Settings
if req.Cors {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"})
@@ -337,6 +336,9 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
if directive.GetName() == "proxy_ssl_name" && len(directive.GetParameters()) > 0 {
proxyConfig.ProxySSLName = directive.GetParameters()[0]
}
if directive.GetName() == "proxy_ssl_verify" {
proxyConfig.SSLVerify = len(directive.GetParameters()) > 0 && directive.GetParameters()[0] == "on"
}
}
proxyConfig.Cors = location.Cors
proxyConfig.AllowCredentials = location.AllowCredentials
+89 -7
View File
@@ -47,6 +47,82 @@ func handleChineseDomain(domain string) (string, error) {
return domain, nil
}
func isHTTPSProxyPass(proxyPass string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(proxyPass)), "https://")
}
func normalizeProxyPass(proxyPass string) string {
proxyPass = strings.TrimSpace(proxyPass)
if proxyPass == "" {
return ""
}
if strings.Contains(proxyPass, "://") || strings.HasPrefix(proxyPass, "unix:") {
return proxyPass
}
return fmt.Sprintf("http://%s", proxyPass)
}
func getAppInstallProxyPass(appInstall *model.AppInstall) (string, error) {
if appInstall == nil {
return "", errors.New("app install is nil")
}
if appInstall.HttpPort > 0 {
return fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort), nil
}
if appInstall.HttpsPort > 0 {
return fmt.Sprintf("https://127.0.0.1:%d", appInstall.HttpsPort), nil
}
return "", fmt.Errorf("app %s has no available http or https port", appInstall.Name)
}
func getAppInstallProxyPassOrEmpty(appInstall *model.AppInstall) string {
proxyPass, err := getAppInstallProxyPass(appInstall)
if err != nil {
return ""
}
return proxyPass
}
func hasAppInstallProxyPassChanged(before *model.AppInstall, after *model.AppInstall) bool {
return getAppInstallProxyPassOrEmpty(before) != getAppInstallProxyPassOrEmpty(after)
}
func getRootProxyDirectives(proxyPass string) []components.IDirective {
server := &components.Server{}
server.UpdateRootProxy([]string{normalizeProxyPass(proxyPass)})
locations := server.FindDirectives("location")
if len(locations) == 0 || locations[0].GetBlock() == nil {
return nil
}
return append([]components.IDirective(nil), locations[0].GetBlock().GetDirectives()...)
}
func getWebsiteRedirectInclude(website model.Website) string {
return fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias)
}
func applyLocationProxyPass(location *components.Location, proxyPass string, sni *bool, proxySSLName string) {
location.UpdateDirective("proxy_pass", []string{proxyPass})
enableSNI := isHTTPSProxyPass(proxyPass)
if sni != nil {
enableSNI = enableSNI && *sni
}
if enableSNI {
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
} else {
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
}
sslName := "$proxy_host"
if proxySSLName != "" {
sslName = proxySSLName
}
location.UpdateDirective("proxy_ssl_name", []string{sslName})
}
func createIndexFile(website *model.Website, runtime *model.Runtime) error {
var (
indexPath string
@@ -122,7 +198,7 @@ func createProxyFile(website *model.Website) error {
return errors.New("error")
}
location.ChangePath("^~", "/")
location.UpdateDirective("proxy_pass", []string{website.Proxy})
applyLocationProxyPass(location, website.Proxy, nil, "")
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
@@ -257,7 +333,10 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
rootIndex := path.Join("/www/sites", website.Alias, "index")
switch website.Type {
case constant.Deployment:
proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort)
proxy, err := getAppInstallProxyPass(appInstall)
if err != nil {
return err
}
server.UpdateRootProxy([]string{proxy})
case constant.Static:
server.UpdateRoot(rootIndex)
@@ -279,7 +358,7 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
server.UpdatePHPProxy([]string{website.Proxy}, "")
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
server.UpdateRootProxy([]string{fmt.Sprintf("http://%s", website.Proxy)})
server.UpdateRootProxy([]string{normalizeProxyPass(website.Proxy)})
}
case constant.Subsite:
parentWebsite, err := websiteRepo.GetFirst(repo.WithByID(website.ParentWebsiteID))
@@ -977,7 +1056,7 @@ func opWebsite(website *model.Website, operate string) error {
server.RemoveDirective("include", []string{proxyInclude})
rewriteInclude := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias)
server.RemoveDirective("include", []string{rewriteInclude})
redirectInclude := fmt.Sprintf("/www/sites/%s/redirect/%s.conf", website.Alias, website.Alias)
redirectInclude := getWebsiteRedirectInclude(*website)
server.RemoveDirective("include", []string{redirectInclude})
switch website.Type {
@@ -1009,7 +1088,7 @@ func opWebsite(website *model.Website, operate string) error {
if fileOp.Stat(absoluteRewritePath) {
server.UpdateDirective("include", []string{rewriteInclude})
}
redirectInclude := fmt.Sprintf("/www/sites/%s/redirect/%s.conf", website.Alias, website.Alias)
redirectInclude := getWebsiteRedirectInclude(*website)
absoluteRedirectPath := GetSitePath(*website, SiteRedirectDir)
if fileOp.Stat(absoluteRedirectPath) {
server.UpdateDirective("include", []string{redirectInclude})
@@ -1025,7 +1104,10 @@ func opWebsite(website *model.Website, operate string) error {
if err != nil {
return err
}
proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort)
proxy, err := getAppInstallProxyPass(&appInstall)
if err != nil {
return err
}
server.UpdateRootProxy([]string{proxy})
case constant.Static:
server.UpdateRoot(rootIndex)
@@ -1045,7 +1127,7 @@ func opWebsite(website *model.Website, operate string) error {
}
server.UpdatePHPProxy([]string{website.Proxy}, localPath)
} else {
proxy := fmt.Sprintf("http://%s", website.Proxy)
proxy := normalizeProxyPass(website.Proxy)
server.UpdateRootProxy([]string{proxy})
}
case constant.Subsite:
+2 -1
View File
@@ -275,7 +275,8 @@ func (t *Task) Execute() error {
}
var err error
t.Log(i18n.GetWithName("TaskStart", t.Name))
for _, subTask := range t.SubTasks {
for i := 0; i < len(t.SubTasks); i++ {
subTask := t.SubTasks[i]
t.Task.CurrentStep = subTask.StepAlias
t.updateTask(t.Task)
if err = subTask.Execute(); err == nil {
+30
View File
@@ -368,6 +368,36 @@
"formatZH": "docker 服务 [operation]",
"formatEN": "[operation] docker service"
},
"/containers/files/del": {
"bodyKeys": [
"containerID",
"paths"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除容器 [containerID] 文件 [paths]",
"formatEN": "Delete files [paths] in container [containerID]"
},
"/containers/files/download": {
"bodyKeys": [
"containerID",
"path"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "下载容器 [containerID] 文件 [path]",
"formatEN": "Download file [path] from container [containerID]"
},
"/containers/files/upload": {
"bodyKeys": [
"containerID",
"path"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "容器 [containerID] 上传文件到 [path]",
"formatEN": "Upload file to [path] in container [containerID]"
},
"/containers/image/build": {
"bodyKeys": [
"name"
@@ -2,7 +2,6 @@ package nginx_conf
import (
"embed"
_ "embed"
"io"
)
+5
View File
@@ -0,0 +1,5 @@
package constant
const DefaultTerminalAIPrefix = "@ai"
const DefaultTerminalAIRiskCommands = "[\"rm\",\"mkfs\",\"dd if=\",\"dd of=/dev/\",\"wipefs\",\"fdisk\",\"parted\",\"sfdisk\",\"shred\",\"curl | sh\",\"wget | sh\",\"chmod -R 777 /\",\"shutdown\",\"reboot\",\"poweroff\",\"init 0\",\":(){ :|:& };:\"]"
+46 -38
View File
@@ -1,14 +1,17 @@
module github.com/1Panel-dev/1Panel/agent
go 1.24.9
go 1.25.7
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.0
github.com/compose-spec/compose-go/v2 v2.9.1
github.com/creack/pty v1.1.24
github.com/docker/compose/v2 v2.40.2
github.com/docker/docker v28.5.1+incompatible
github.com/docker/cli v29.2.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/fsnotify/fsnotify v1.9.0
github.com/gin-gonic/gin v1.10.0
@@ -18,15 +21,15 @@ require (
github.com/go-playground/validator/v10 v10.26.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-resty/resty/v2 v2.17.0
github.com/go-sql-driver/mysql v1.8.1
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.6.0
github.com/jackc/pgx/v5 v5.7.5
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.1
github.com/klauspost/compress v1.18.2
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.69
github.com/minio/minio-go/v7 v7.0.74
@@ -36,12 +39,12 @@ require (
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/qiniu/go-sdk/v7 v7.21.1
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/sirupsen/logrus v1.9.3
github.com/sirupsen/logrus v1.9.4
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.19.0
github.com/subosito/gotenv v1.6.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
@@ -102,7 +105,7 @@ require (
github.com/connesc/cipherio v0.2.1 // 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.0 // indirect
github.com/containerd/containerd/v2 v2.2.1 // 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
@@ -112,17 +115,17 @@ require (
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.29.1 // indirect
github.com/docker/cli v28.5.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/buildx v0.31.1 // 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/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/color v1.18.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/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
@@ -145,7 +148,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // 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
@@ -155,8 +158,8 @@ require (
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
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
@@ -167,18 +170,19 @@ require (
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/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f // indirect
github.com/mattn/go-colorable v0.1.13 // 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/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.25.1 // indirect
github.com/moby/buildkit v0.27.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // 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/patternmatcher v0.6.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -188,7 +192,7 @@ 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/morikuni/aec v1.0.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -206,13 +210,15 @@ require (
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // 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/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/sigstore v1.10.0 // indirect
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
@@ -222,7 +228,7 @@ require (
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
@@ -232,34 +238,36 @@ require (
github.com/volcengine/volc-sdk-golang v1.0.230 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // 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.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.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.5.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.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-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // 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/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+100 -19
View File
@@ -38,7 +38,6 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -117,6 +116,7 @@ github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7
github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
@@ -215,16 +215,21 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM=
github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs=
github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY=
github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8=
github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE=
github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=
github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -237,13 +242,17 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg=
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM=
github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE=
github.com/containerd/nydus-snapshotter v0.15.10 h1:hphjuKOqSHLGznNJiAvmsOWkdu4qFXjf4DzGrWSuIsM=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4=
github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
@@ -255,7 +264,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
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=
@@ -270,16 +278,28 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA=
github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI=
github.com/docker/buildx v0.30.1 h1:3vthfaTQOLt5QfN2nl7rKuPLUvx69nL5ZikFIXp//c8=
github.com/docker/buildx v0.30.1/go.mod h1:8nwT0V6UNYNo9rXq6WO/BQd9KrJ0JYcY/QX6x0y1Oro=
github.com/docker/buildx v0.31.1 h1:zbvbrb9nxBNVV8nnI33f2F+4aAZBA1gY+AmeBFflMqY=
github.com/docker/buildx v0.31.1/go.mod h1:SD+jYLnt3S4SXqohVtV+8z+dihnOgwMJ8t+bLQvsaCk=
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/compose/v2 v2.40.2 h1:h2bDBJkOuqmj93XvT2oI0ArPQonE0lGtWiILXdiXvbA=
github.com/docker/compose/v2 v2.40.2/go.mod h1:CbSJpKGw20LInVsPjglZ8z7Squ3OBQOD7Ux5nkjGfIU=
github.com/docker/compose/v2 v2.40.3 h1:XeYkQu1svDtyfZPv5nTwFryQ25ZJMkIlc4pz9HalMPI=
github.com/docker/compose/v2 v2.40.3/go.mod h1:iNY1tvoHTyN3C3QHCuWAgj3OjR2T6mGkk/qxfbBF/4M=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -316,6 +336,8 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
@@ -333,6 +355,8 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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=
@@ -379,15 +403,12 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
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.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
@@ -397,6 +418,8 @@ github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2m
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
@@ -407,7 +430,6 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ=
github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -489,6 +511,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -515,6 +538,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
@@ -556,7 +581,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 h1:uia+R3K1izQRGpxTV+bS4q3/ueMW7ProAMWqM6OlqOU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
@@ -567,10 +591,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
@@ -615,6 +645,8 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -631,7 +663,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -639,7 +670,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
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/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
@@ -648,14 +678,14 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f h1:B0OD7nYl2FPQEVrw8g2uyc1lGEzNbvrKh7fspGZcbvY=
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
@@ -702,12 +732,22 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8=
github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
github.com/moby/buildkit v0.26.3 h1:D+ruZVAk/3ipRq5XRxBH9/DIFpRjSlTtMbghT5gQP9g=
github.com/moby/buildkit v0.26.3/go.mod h1:4T4wJzQS4kYWIfFRjsbJry4QoxDBjK+UGOEOs1izL7w=
github.com/moby/buildkit v0.27.0 h1:1gtNaMcVE0XXCZrybC32L79A7Ga1JeB7V3PfpCt1bDc=
github.com/moby/buildkit v0.27.0/go.mod h1:4STUkNc5t1nf03HS+01UmI2X6FdfOI3XaKt9QNoTsms=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@@ -735,6 +775,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -787,8 +829,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
@@ -808,7 +852,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -853,22 +896,21 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.21.1 h1:D/IjVOlg5pTw0jeDjqTo6H5QM73Obb1AYfPOHmIFN+Q=
github.com/qiniu/go-sdk/v7 v7.21.1/go.mod h1:8EM2awITynlem2VML2dXGHkMYP2UyECsGLOdp6yMpco=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/qiniu/go-sdk/v7 v7.26.4 h1:D/k6cPbhjKvSx9hVBQSh97GuPu1ZtM3OLPCclSpusjo=
github.com/qiniu/go-sdk/v7 v7.26.4/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
@@ -886,18 +928,26 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sigstore/sigstore v1.10.0 h1:lQrmdzqlR8p9SCfWIpFoGUqdXEzJSZT2X+lTXOMPaQI=
github.com/sigstore/sigstore v1.10.0/go.mod h1:Ygq+L/y9Bm3YnjpJTlQrOk/gXyrjkpn3/AEJpmk1n9Y=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 h1:94NLPmq4bxvdmslzcG670IOkrlS98CGpmob8cjpFHuI=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7/go.mod h1:4r/PNX0G7uzkLpc3PSdYs5E2k4bWEJNXTK6kwAyw9TM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -907,12 +957,15 @@ github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIK
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
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.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=
@@ -964,6 +1017,8 @@ github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK5
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
@@ -983,6 +1038,7 @@ github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNW
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/volcengine/volc-sdk-golang v1.0.230 h1:84/MOF0zUPtAHt3e1+MsFq5qrnQRC+e3XzTUwIOzZxw=
github.com/volcengine/volc-sdk-golang v1.0.230/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -1008,6 +1064,8 @@ go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsX
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1020,20 +1078,33 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -1045,6 +1116,8 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -1058,6 +1131,7 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
@@ -1082,7 +1156,6 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -1113,6 +1186,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
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-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1143,6 +1218,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1477,6 +1554,8 @@ google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOs
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1514,6 +1593,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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=
+23 -3
View File
@@ -27,13 +27,18 @@ var langFiles = map[string]string{
}
func GetMsgWithMap(key string, maps map[string]interface{}) string {
return GetMsgWithMapAndLang("", key, maps)
}
func GetMsgWithMapAndLang(lang string, key string, maps map[string]interface{}) string {
var content string
localizer := newLocalizer(lang)
if maps == nil {
content, _ = global.I18n.Localize(&i18n.LocalizeConfig{
content, _ = localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
})
} else {
content, _ = global.I18n.Localize(&i18n.LocalizeConfig{
content, _ = localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: maps,
})
@@ -78,7 +83,11 @@ func GetErrMsg(key string, maps map[string]interface{}) string {
}
func GetMsgByKey(key string) string {
content, _ := global.I18n.Localize(&i18n.LocalizeConfig{
return GetMsgByKeyAndLang("", key)
}
func GetMsgByKeyAndLang(lang string, key string) string {
content, _ := newLocalizer(lang).Localize(&i18n.LocalizeConfig{
MessageID: key,
})
return content
@@ -151,6 +160,17 @@ func Init() {
global.I18n = i18n.NewLocalizer(bundle, lang)
}
func newLocalizer(lang string) *i18n.Localizer {
lang = strings.TrimSpace(lang)
if lang == "" {
if global.I18n != nil {
return global.I18n
}
lang = GetLanguageFromDB()
}
return i18n.NewLocalizer(bundle, lang)
}
func GetLanguageFromDB() string {
if global.DB == nil {
return "en"
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Bad request: {{ .detail }}'
ErrTokenParse: 'Token error: {{ .detail }}'
ErrInitialPassword: 'Original password is incorrect'
ErrInternalServer: 'Internal server error: {{ .detail }}'
ErrWgetRemoteFailed: 'Remote download failed or URL is unreachable: {{ .detail }}'
ErrWgetInvalidContentType: 'Remote URL did not return a file (possibly an error page), {{ .detail }}'
ErrRecordExist: 'Record already exists'
ErrRecordNotFound: 'Record not found'
ErrStructTransform: 'Type conversion failed: {{ .err }}'
@@ -44,7 +46,10 @@ ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified'
ErrAgentProviderMismatch: 'Agent provider mismatch'
ErrAgentAccountModelsRequired: 'Please configure at least one model'
ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation'
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
ErrAgentBaseURLRequired: 'Base URL is required'
ErrAgentApiKeyRequired: 'API key is required'
ErrAgentComposeRequired: 'Compose content is required'
@@ -232,6 +237,36 @@ BuildCache: 'Build Cache'
Volume: 'Volume'
Network: "Network"
PruneStart: 'Cleaning in progress'
ContainerBackupPrepare: 'Prepare container backup workspace'
ContainerBackupStop: 'Stop container before backup'
ContainerBackupInspect: 'Backup container inspect and network'
ContainerBackupMounts: 'Backup container mounts'
ContainerBackupMeta: 'Write container backup metadata'
ContainerBackupStart: 'Start container after backup'
ContainerRecoverPrepare: 'Prepare container restore workspace'
ContainerRecoverExtract: 'Extract container backup archive'
ContainerRecoverParse: 'Load container backup metadata'
ContainerRecoverCreate: 'Recreate container'
ContainerRecoverMounts: 'Restore container mounts'
ContainerRecoverStart: 'Start restored container'
ContainerRecoverCleanup: 'Cleanup container restore workspace'
ComposeBackupPrepare: 'Prepare compose backup workspace'
ComposeBackupStop: 'Stop compose before backup'
ComposeBackupFiles: 'Backup compose files'
ComposeBackupContainers: 'Backup compose containers'
ComposeBackupMeta: 'Write compose backup metadata'
ComposeBackupStart: 'Start compose after backup'
ComposeBackupCleanup: 'Cleanup compose backup workspace'
ComposeRecoverPrepare: 'Prepare compose restore workspace'
ComposeRecoverExtract: 'Extract compose backup archive'
ComposeRecoverMeta: 'Load compose backup metadata'
ComposeRecoverMetaLogName: 'Compose restore meta: name={{ .name }}'
ComposeRecoverMetaLogPath: 'Compose restore meta: backupPath={{ .backupPath }} targetDir={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Compose restore meta: files={{ .files }} containers={{ .containers }}'
ComposeRecoverFiles: 'Restore compose files'
ComposeRecoverContainers: 'Restore compose containers'
ComposeRecoverRecord: 'Save compose record'
ComposeRecoverCleanup: 'Cleanup compose restore workspace'
#runtime
ErrFileNotExist: 'Source file not found: {{ .detail }}'
@@ -532,3 +567,9 @@ PartitionDiskErr: 'Partition failed: {{ .err }}'
FormatDiskErr: 'Format failed: {{ .err }}'
MountDiskErr: 'Mount failed: {{ .err }}'
XfsNotFound: 'xfs not found; install xfsprogs first'
# terminal
TerminalAIBlockedRiskyCommand: 'blocked risky command: {{ .command }}'
TerminalAIThinking: 'AI is thinking...'
TerminalAIReadyToExecute: 'Thinking complete, press Enter to execute (duration: {{ .duration }}, tokens: {{ .tokens }})'
TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Solicitud inválida: {{ .detail }}'
ErrTokenParse: 'Token inválido: {{ .detail }}'
ErrInitialPassword: 'Contraseña original incorrecta'
ErrInternalServer: 'Error interno: {{ .detail }}'
ErrWgetRemoteFailed: 'La descarga remota falló o la URL no es accesible: {{ .detail }}'
ErrWgetInvalidContentType: 'La URL remota no devolvió un archivo (posiblemente una página de error), {{ .detail }}'
ErrRecordExist: 'Registro ya existe'
ErrRecordNotFound: 'Registro no encontrado'
ErrStructTransform: 'Error de conversión: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
ErrAgentProviderMismatch: 'Proveedor de agente no coincide'
ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta'
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base requerida'
ErrAgentApiKeyRequired: 'Clave API requerida'
ErrAgentComposeRequired: 'Contenido compose requerido'
@@ -209,6 +214,36 @@ BuildCache: 'Caché de compilación'
Volume: 'Volumen de almacenamiento'
Network: 'Red'
PruneStart: 'Limpiando en progreso espere...'
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
ContainerBackupStop: 'Detener contenedor antes del respaldo'
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
ContainerBackupMounts: 'Respaldar montajes del contenedor'
ContainerBackupMeta: 'Escribir metadatos del respaldo del contenedor'
ContainerBackupStart: 'Iniciar contenedor después del respaldo'
ContainerRecoverPrepare: 'Preparar espacio de restauración del contenedor'
ContainerRecoverExtract: 'Extraer archivo de respaldo del contenedor'
ContainerRecoverParse: 'Cargar metadatos del respaldo del contenedor'
ContainerRecoverCreate: 'Recrear contenedor'
ContainerRecoverMounts: 'Restaurar montajes del contenedor'
ContainerRecoverStart: 'Iniciar contenedor restaurado'
ContainerRecoverCleanup: 'Limpiar espacio de restauracion del contenedor'
ComposeBackupPrepare: 'Preparar espacio de respaldo del compose'
ComposeBackupStop: 'Detener compose antes del respaldo'
ComposeBackupFiles: 'Respaldar archivos de compose'
ComposeBackupContainers: 'Respaldar contenedores del compose'
ComposeBackupMeta: 'Escribir metadatos del respaldo de compose'
ComposeBackupStart: 'Iniciar compose después del respaldo'
ComposeBackupCleanup: 'Limpiar espacio de respaldo del compose'
ComposeRecoverPrepare: 'Preparar espacio de restauración del compose'
ComposeRecoverExtract: 'Extraer archivo de respaldo de compose'
ComposeRecoverMeta: 'Cargar metadatos del respaldo de compose'
ComposeRecoverMetaLogName: 'Metadatos de restauracion de compose: nombre={{ .name }}'
ComposeRecoverMetaLogPath: 'Metadatos de restauracion de compose: rutaRespaldo={{ .backupPath }} rutaDestino={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Metadatos de restauracion de compose: archivos={{ .files }} contenedores={{ .containers }}'
ComposeRecoverFiles: 'Restaurar archivos de compose'
ComposeRecoverContainers: 'Restaurar contenedores del compose'
ComposeRecoverRecord: 'Guardar registro de compose'
ComposeRecoverCleanup: 'Limpiar espacio de restauracion de compose'
ErrFileNotExist: 'El archivo {{ .detail }} no existe, verifique la integridad'
ErrImageBuildErr: 'Fallo al construir la imagen'
ErrImageExist: 'La imagen ya existe Modifique el nombre de la imagen.'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'Error al particionar, {{ .err }}'
FormatDiskErr: 'Error al formatear disco, {{ .err }}'
MountDiskErr: 'Error al montar disco, {{ .err }}'
XfsNotFound: 'No se detectó el sistema de archivos xfs instale xfsprogs primero'
# terminal
TerminalAIBlockedRiskyCommand: 'Comando de riesgo bloqueado: {{ .command }}'
TerminalAIThinking: 'La IA está pensando...'
TerminalAIReadyToExecute: 'Pensamiento completado, pulsa Enter para ejecutar (duración: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '不正なリクエスト: {{ .detail }}'
ErrTokenParse: 'トークンエラー: {{ .detail }}'
ErrInitialPassword: '元のパスワードが違います'
ErrInternalServer: '内部エラー: {{ .detail }}'
ErrWgetRemoteFailed: 'リモートのダウンロード URL が無効か、到達できません:{{ .detail }}'
ErrWgetInvalidContentType: 'リモート URL がファイルではなくエラーページ等を返しました、{{ .detail }}'
ErrRecordExist: 'レコードが既に存在します'
ErrRecordNotFound: 'レコードが見つかりません'
ErrStructTransform: '型変換エラー: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です'
ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
ErrAgentBaseURLRequired: 'ベースURLが必要です'
ErrAgentApiKeyRequired: 'APIキーが必要です'
ErrAgentComposeRequired: 'Composeが必要です'
@@ -209,6 +214,36 @@ BuildCache: 'ビルドキャッシュ'
Volume: 'ストレージボリューム'
Network: 'ネットワーク'
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
ContainerBackupStop: 'バックアップ前にコンテナを停止'
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
ContainerBackupMounts: 'コンテナのマウントデータをバックアップ'
ContainerBackupMeta: 'コンテナバックアップのメタデータを書き込み'
ContainerBackupStart: 'バックアップ後にコンテナを起動'
ContainerRecoverPrepare: 'コンテナ復元作業領域を準備'
ContainerRecoverExtract: 'コンテナバックアップを展開'
ContainerRecoverParse: 'コンテナバックアップメタデータを読み込み'
ContainerRecoverCreate: 'コンテナを再作成'
ContainerRecoverMounts: 'コンテナマウントを復元'
ContainerRecoverStart: '復元済みコンテナを起動'
ContainerRecoverCleanup: 'コンテナ復元作業領域をクリーンアップ'
ComposeBackupPrepare: 'Compose バックアップ作業領域を準備'
ComposeBackupStop: 'バックアップ前に Compose を停止'
ComposeBackupFiles: 'Compose ファイルをバックアップ'
ComposeBackupContainers: 'Compose コンテナをバックアップ'
ComposeBackupMeta: 'Compose バックアップのメタデータを書き込み'
ComposeBackupStart: 'バックアップ後に Compose を起動'
ComposeBackupCleanup: 'Compose バックアップ作業領域をクリーンアップ'
ComposeRecoverPrepare: 'Compose 復元作業領域を準備'
ComposeRecoverExtract: 'Compose バックアップを展開'
ComposeRecoverMeta: 'Compose バックアップメタデータを読み込み'
ComposeRecoverMetaLogName: 'Compose 復元メタ情報: 名前={{ .name }}'
ComposeRecoverMetaLogPath: 'Compose 復元メタ情報: バックアップパス={{ .backupPath }} 復元先={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Compose 復元メタ情報: ファイル={{ .files }} コンテナ={{ .containers }}'
ComposeRecoverFiles: 'Compose ファイルを復元'
ComposeRecoverContainers: 'Compose コンテナを復元'
ComposeRecoverRecord: 'Compose 記録を保存'
ComposeRecoverCleanup: 'Compose 復元作業領域をクリーンアップ'
ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。'
ErrImageBuildErr: 'イメージのビルドに失敗しました'
ErrImageExist: 'イメージはすでに存在します!イメージ名を変更してください。'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'パーティションに失敗しました、{{ .err }}'
FormatDiskErr: 'ディスクのフォーマットに失敗しました、{{ .err }}'
MountDiskErr: 'ディスクのマウントに失敗しました、{{ .err }}'
XfsNotFound: 'xfs ファイルシステムが検出されませんでした、最初に xfsprogs をインストールしてください'
# terminal
TerminalAIBlockedRiskyCommand: '危険なコマンドをブロックしました: {{ .command }}'
TerminalAIThinking: 'AI が考えています...'
TerminalAIReadyToExecute: '思考が完了しました。Enter で実行してください(所要時間 {{ .duration }}、token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI リクエストに失敗しました: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '잘못된 요청: {{ .detail }}'
ErrTokenParse: '토큰 오류: {{ .detail }}'
ErrInitialPassword: '원래 비밀번호가 틀립니다'
ErrInternalServer: '서버 오류: {{ .detail }}'
ErrWgetRemoteFailed: '원격 다운로드 주소가 잘못되었거나 접근할 수 없습니다: {{ .detail }}'
ErrWgetInvalidContentType: '원격 주소가 파일이 아닌 콘텐츠를 반환했습니다 (오류 페이지일 수 있음), {{ .detail }}'
ErrRecordExist: '레코드가 이미 있습니다'
ErrRecordNotFound: '레코드를 찾을 수 없습니다'
ErrStructTransform: '형 변환 실패: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
ErrAgentProviderMismatch: '에이전트 공급자 불일치'
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
ErrAgentBaseURLRequired: '기본 URL 필요'
ErrAgentApiKeyRequired: 'API 키 필요'
ErrAgentComposeRequired: 'Compose 내용 필요'
@@ -209,6 +214,36 @@ BuildCache: '빌드 캐시'
Volume: '스토리지 볼륨'
Network: '네트워크'
PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
ContainerBackupStop: '백업 전에 컨테이너 중지'
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
ContainerBackupMounts: '컨테이너 마운트 데이터 백업'
ContainerBackupMeta: '컨테이너 백업 메타데이터 작성'
ContainerBackupStart: '백업 후 컨테이너 시작'
ContainerRecoverPrepare: '컨테이너 복원 작업 공간 준비'
ContainerRecoverExtract: '컨테이너 백업 파일 압축 해제'
ContainerRecoverParse: '컨테이너 백업 메타데이터 로드'
ContainerRecoverCreate: '컨테이너 재생성'
ContainerRecoverMounts: '컨테이너 마운트 데이터 복원'
ContainerRecoverStart: '복원된 컨테이너 시작'
ContainerRecoverCleanup: '컨테이너 복원 작업 공간 정리'
ComposeBackupPrepare: 'Compose 백업 작업 공간 준비'
ComposeBackupStop: '백업 전에 Compose 중지'
ComposeBackupFiles: 'Compose 파일 백업'
ComposeBackupContainers: 'Compose 컨테이너 백업'
ComposeBackupMeta: 'Compose 백업 메타데이터 작성'
ComposeBackupStart: '백업 후 Compose 시작'
ComposeBackupCleanup: 'Compose 백업 작업 공간 정리'
ComposeRecoverPrepare: 'Compose 복원 작업 공간 준비'
ComposeRecoverExtract: 'Compose 백업 파일 압축 해제'
ComposeRecoverMeta: 'Compose 백업 메타데이터 로드'
ComposeRecoverMetaLogName: 'Compose 복원 메타: 이름={{ .name }}'
ComposeRecoverMetaLogPath: 'Compose 복원 메타: 백업경로={{ .backupPath }} 대상경로={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Compose 복원 메타: 파일={{ .files }} 컨테이너={{ .containers }}'
ComposeRecoverFiles: 'Compose 파일 복원'
ComposeRecoverContainers: 'Compose 컨테이너 복원'
ComposeRecoverRecord: 'Compose 기록 저장'
ComposeRecoverCleanup: 'Compose 복원 작업 공간 정리'
ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다 소스 파일의 무결성을 확인하세요'
ErrImageBuildErr: '이미지 빌드 실패'
ErrImageExist: '이미지가 이미 존재합니다 이미지 이름을 수정하세요.'
@@ -474,3 +509,9 @@ PartitionDiskErr: '파티션 분할에 실패했습니다, {{ .err }}'
FormatDiskErr: '디스크 포맷에 실패했습니다, {{ .err }}'
MountDiskErr: '디스크 마운트에 실패했습니다, {{ .err }}'
XfsNotFound: 'xfs 파일 시스템이 감지되지 않았습니다, 먼저 xfsprogs 를 설치하세요'
# terminal
TerminalAIBlockedRiskyCommand: '위험한 명령이 차단되었습니다: {{ .command }}'
TerminalAIThinking: 'AI가 생각 중입니다...'
TerminalAIReadyToExecute: '생각이 완료되었습니다. Enter를 눌러 실행하세요 (소요 시간 {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI 요청 실패: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Permintaan tidak sah: {{ .detail }}'
ErrTokenParse: 'Token tidak sah: {{ .detail }}'
ErrInitialPassword: 'Kata laluan asal salah'
ErrInternalServer: 'Ralat pelayan: {{ .detail }}'
ErrWgetRemoteFailed: 'Muat turun jauh gagal atau URL tidak boleh diakses: {{ .detail }}'
ErrWgetInvalidContentType: 'URL jauh tidak mengembalikan fail (mungkin halaman ralat), {{ .detail }}'
ErrRecordExist: 'Rekod sudah wujud'
ErrRecordNotFound: 'Rekod tidak ditemui'
ErrStructTransform: 'Ralat penukaran: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
ErrAgentProviderMismatch: 'Penyedia ejen tidak sepadan'
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun'
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
ErrAgentBaseURLRequired: 'URL asas diperlukan'
ErrAgentApiKeyRequired: 'Kunci API diperlukan'
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
@@ -209,6 +214,36 @@ BuildCache: 'Cache binaan'
Volume: 'Jilid storan'
Network: 'Rangkaian'
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
ContainerBackupMounts: 'Sandarkan mount kontena'
ContainerBackupMeta: 'Tulis metadata sandaran kontena'
ContainerBackupStart: 'Mulakan kontena selepas sandaran'
ContainerRecoverPrepare: 'Sediakan ruang kerja pemulihan kontena'
ContainerRecoverExtract: 'Ekstrak arkib sandaran kontena'
ContainerRecoverParse: 'Muat metadata sandaran kontena'
ContainerRecoverCreate: 'Cipta semula kontena'
ContainerRecoverMounts: 'Pulihkan mount kontena'
ContainerRecoverStart: 'Mulakan kontena dipulihkan'
ContainerRecoverCleanup: 'Bersihkan ruang kerja pemulihan kontena'
ComposeBackupPrepare: 'Sediakan ruang kerja sandaran compose'
ComposeBackupStop: 'Hentikan compose sebelum sandaran'
ComposeBackupFiles: 'Sandarkan fail compose'
ComposeBackupContainers: 'Sandarkan kontena compose'
ComposeBackupMeta: 'Tulis metadata sandaran compose'
ComposeBackupStart: 'Mulakan compose selepas sandaran'
ComposeBackupCleanup: 'Bersihkan ruang kerja sandaran compose'
ComposeRecoverPrepare: 'Sediakan ruang kerja pemulihan compose'
ComposeRecoverExtract: 'Ekstrak arkib sandaran compose'
ComposeRecoverMeta: 'Muat metadata sandaran compose'
ComposeRecoverMetaLogName: 'Metadata pemulihan compose: nama={{ .name }}'
ComposeRecoverMetaLogPath: 'Metadata pemulihan compose: laluanSandaran={{ .backupPath }} laluanSasaran={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Metadata pemulihan compose: fail={{ .files }} kontena={{ .containers }}'
ComposeRecoverFiles: 'Pulihkan fail compose'
ComposeRecoverContainers: 'Pulihkan kontena compose'
ComposeRecoverRecord: 'Simpan rekod compose'
ComposeRecoverCleanup: 'Bersihkan ruang kerja pemulihan compose'
ErrFileNotExist: 'Fail {{ .detail }} tidak wujud Sila semak integriti fail sumber'
ErrImageBuildErr: 'Pembinaan imej gagal'
ErrImageExist: 'Imej sudah wujud Sila ubah nama imej.'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'Gagal membahagikan, {{ .err }}'
FormatDiskErr: 'Gagal memformat cakera, {{ .err }}'
MountDiskErr: 'Gagal mengkaitkan cakera, {{ .err }}'
XfsNotFound: 'Sistem fail xfs tidak dikesan, sila pasang xfsprogs terlebih dahulu'
# terminal
TerminalAIBlockedRiskyCommand: 'Perintah berisiko telah disekat: {{ .command }}'
TerminalAIThinking: 'AI sedang berfikir...'
TerminalAIReadyToExecute: 'Pemikiran selesai, tekan Enter untuk jalankan (tempoh: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Permintaan AI gagal: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Requisição inválida: {{ .detail }}'
ErrTokenParse: 'Erro de token: {{ .detail }}'
ErrInitialPassword: 'Senha original incorreta'
ErrInternalServer: 'Erro interno: {{ .detail }}'
ErrWgetRemoteFailed: 'Falha no download remoto ou URL inacessível: {{ .detail }}'
ErrWgetInvalidContentType: 'A URL remota não retornou um arquivo (possivelmente uma página de erro), {{ .detail }}'
ErrRecordExist: 'Registro já existe'
ErrRecordNotFound: 'Registro não encontrado'
ErrStructTransform: 'Erro de conversão: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
ErrAgentProviderMismatch: 'Provedor de agente diferente'
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta'
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base obrigatória'
ErrAgentApiKeyRequired: 'Chave API obrigatória'
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
@@ -209,6 +214,36 @@ BuildCache: 'Cache de construção'
Volume: 'Volume de armazenamento'
Network: 'Rede'
PruneStart: 'Limpando em andamento aguarde...'
ContainerBackupPrepare: 'Preparar área de backup do contêiner'
ContainerBackupStop: 'Parar contêiner antes do backup'
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
ContainerBackupMounts: 'Fazer backup dos mounts do contêiner'
ContainerBackupMeta: 'Escrever metadados do backup do contêiner'
ContainerBackupStart: 'Iniciar contêiner após o backup'
ContainerRecoverPrepare: 'Preparar área de restauração do contêiner'
ContainerRecoverExtract: 'Extrair arquivo de backup do contêiner'
ContainerRecoverParse: 'Carregar metadados do backup do contêiner'
ContainerRecoverCreate: 'Recriar contêiner'
ContainerRecoverMounts: 'Restaurar mounts do contêiner'
ContainerRecoverStart: 'Iniciar contêiner restaurado'
ContainerRecoverCleanup: 'Limpar área de restauração do contêiner'
ComposeBackupPrepare: 'Preparar área de backup do compose'
ComposeBackupStop: 'Parar compose antes do backup'
ComposeBackupFiles: 'Fazer backup dos arquivos do compose'
ComposeBackupContainers: 'Fazer backup dos contêineres do compose'
ComposeBackupMeta: 'Escrever metadados do backup do compose'
ComposeBackupStart: 'Iniciar compose após o backup'
ComposeBackupCleanup: 'Limpar área de backup do compose'
ComposeRecoverPrepare: 'Preparar área de restauração do compose'
ComposeRecoverExtract: 'Extrair arquivo de backup do compose'
ComposeRecoverMeta: 'Carregar metadados do backup do compose'
ComposeRecoverMetaLogName: 'Metadados da restauracao do compose: nome={{ .name }}'
ComposeRecoverMetaLogPath: 'Metadados da restauracao do compose: caminhoBackup={{ .backupPath }} caminhoDestino={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Metadados da restauracao do compose: arquivos={{ .files }} containers={{ .containers }}'
ComposeRecoverFiles: 'Restaurar arquivos do compose'
ComposeRecoverContainers: 'Restaurar contêineres do compose'
ComposeRecoverRecord: 'Salvar registro do compose'
ComposeRecoverCleanup: 'Limpar área de restauração do compose'
ErrFileNotExist: 'O arquivo {{ .detail }} não existe Verifique a integridade do arquivo de origem'
ErrImageBuildErr: 'Falha na criação da imagem'
ErrImageExist: 'A imagem já existe, modifique o nome da imagem.'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'Falha ao particionar, {{ .err }}'
FormatDiskErr: 'Falha ao formatar disco, {{ .err }}'
MountDiskErr: 'Falha ao montar disco, {{ .err }}'
XfsNotFound: 'Sistema de arquivos xfs não detectado instale xfsprogs primeiro'
# terminal
TerminalAIBlockedRiskyCommand: 'Comando de risco bloqueado: {{ .command }}'
TerminalAIThinking: 'A IA está pensando...'
TerminalAIReadyToExecute: 'Pensamento concluído, pressione Enter para executar (duração: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Falha na solicitação de AI: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Неверный запрос: {{ .detail }}'
ErrTokenParse: 'Ошибка токена: {{ .detail }}'
ErrInitialPassword: 'Исходный пароль неверен'
ErrInternalServer: 'Внутренняя ошибка: {{ .detail }}'
ErrWgetRemoteFailed: 'Неверный или недоступный URL удалённой загрузки: {{ .detail }}'
ErrWgetInvalidContentType: 'Удалённый сервер вернул не файл (возможно, страницу ошибки), {{ .detail }}'
ErrRecordExist: 'Запись уже существует'
ErrRecordNotFound: 'Запись не найдена'
ErrStructTransform: 'Ошибка преобразования: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступн
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
ErrAgentProviderMismatch: 'Провайдер не совпадает'
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
ErrAgentBaseURLRequired: 'Нужен базовый URL'
ErrAgentApiKeyRequired: 'Нужен API-ключ'
ErrAgentComposeRequired: 'Нужен compose'
@@ -209,6 +214,36 @@ BuildCache: 'Кэш сборки'
Volume: 'Том хранилища'
Network: 'Сеть'
PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
ContainerBackupMounts: 'Сделать резервную копию точек монтирования контейнера'
ContainerBackupMeta: 'Записать метаданные резервной копии контейнера'
ContainerBackupStart: 'Запустить контейнер после резервного копирования'
ContainerRecoverPrepare: 'Подготовить рабочую область восстановления контейнера'
ContainerRecoverExtract: 'Распаковать архив резервной копии контейнера'
ContainerRecoverParse: 'Загрузить метаданные резервной копии контейнера'
ContainerRecoverCreate: 'Пересоздать контейнер'
ContainerRecoverMounts: 'Восстановить точки монтирования контейнера'
ContainerRecoverStart: 'Запустить восстановленный контейнер'
ContainerRecoverCleanup: 'Очистить рабочую область восстановления контейнера'
ComposeBackupPrepare: 'Подготовить рабочую область резервной копии compose'
ComposeBackupStop: 'Остановить compose перед резервным копированием'
ComposeBackupFiles: 'Сделать резервную копию файлов compose'
ComposeBackupContainers: 'Сделать резервную копию контейнеров compose'
ComposeBackupMeta: 'Записать метаданные резервной копии compose'
ComposeBackupStart: 'Запустить compose после резервного копирования'
ComposeBackupCleanup: 'Очистить рабочую область резервного копирования compose'
ComposeRecoverPrepare: 'Подготовить рабочую область восстановления compose'
ComposeRecoverExtract: 'Распаковать архив резервной копии compose'
ComposeRecoverMeta: 'Загрузить метаданные резервной копии compose'
ComposeRecoverMetaLogName: 'Метаданные восстановления compose: имя={{ .name }}'
ComposeRecoverMetaLogPath: 'Метаданные восстановления compose: путьБэкапа={{ .backupPath }} путьНазначения={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Метаданные восстановления compose: файлов={{ .files }} контейнеров={{ .containers }}'
ComposeRecoverFiles: 'Восстановить файлы compose'
ComposeRecoverContainers: 'Восстановить контейнеры compose'
ComposeRecoverRecord: 'Сохранить запись compose'
ComposeRecoverCleanup: 'Очистить рабочую область восстановления compose'
ErrFileNotExist: 'Файл {{ .detail }} не существует Проверьте целостность исходного файла'
ErrImageBuildErr: 'Сборка образа не удалась'
ErrImageExist: 'Изображение уже существует Пожалуйста, измените имя изображения.'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'Не удалось разделить, {{ .err }}'
FormatDiskErr: 'Не удалось отформатировать диск, {{ .err }}'
MountDiskErr: 'Не удалось подключить диск, {{ .err }}'
XfsNotFound: 'Файловая система xfs не обнаружена, сначала установите xfsprogs'
# terminal
TerminalAIBlockedRiskyCommand: 'Опасная команда заблокирована: {{ .command }}'
TerminalAIThinking: 'AI думает...'
TerminalAIReadyToExecute: 'Обдумывание завершено, нажмите Enter для выполнения (время: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'Ошибка запроса AI: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'İstek geçersiz: {{ .detail }}'
ErrTokenParse: 'Token hatası: {{ .detail }}'
ErrInitialPassword: 'Orijinal şifre yanlış'
ErrInternalServer: 'Sunucu hatası: {{ .detail }}'
ErrWgetRemoteFailed: 'Uzak indirme başarısız veya URL erişilemiyor: {{ .detail }}'
ErrWgetInvalidContentType: 'Uzak URL bir dosya değil, muhtemelen bir hata sayfası döndürdü, {{ .detail }}'
ErrRecordExist: 'Kayıt zaten var'
ErrRecordNotFound: 'Kayıt bulunamadı'
ErrStructTransform: 'Dönüşüm hatası: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
ErrAgentProviderMismatch: 'Ajans sağlayıcısı eşleşmiyor'
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir'
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}'
ErrAgentBaseURLRequired: 'Temel URL gerekli'
ErrAgentApiKeyRequired: 'API anahtarı gerekli'
ErrAgentComposeRequired: 'Compose içeriği gerekli'
@@ -209,6 +214,36 @@ BuildCache: 'Derleme önbelleği'
Volume: 'Depolama hacmi'
Network: 'Ağ'
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
ContainerBackupMounts: 'Konteyner mount verilerini yedekle'
ContainerBackupMeta: 'Konteyner yedekleme meta verisini yaz'
ContainerBackupStart: 'Yedeklemeden sonra konteyneri başlat'
ContainerRecoverPrepare: 'Konteyner geri yükleme çalışma alanını hazırla'
ContainerRecoverExtract: 'Konteyner yedek arşivini çıkar'
ContainerRecoverParse: 'Konteyner yedek meta verisini yükle'
ContainerRecoverCreate: 'Konteyneri yeniden oluştur'
ContainerRecoverMounts: 'Konteyner mount verilerini geri yükle'
ContainerRecoverStart: 'Geri yüklenen konteyneri başlat'
ContainerRecoverCleanup: 'Konteyner geri yükleme çalışma alanını temizle'
ComposeBackupPrepare: 'Compose yedekleme çalışma alanını hazırla'
ComposeBackupStop: 'Yedeklemeden önce compose’u durdur'
ComposeBackupFiles: 'Compose dosyalarını yedekle'
ComposeBackupContainers: 'Compose konteynerlerini yedekle'
ComposeBackupMeta: 'Compose yedek meta verisini yaz'
ComposeBackupStart: 'Yedeklemeden sonra compose’u başlat'
ComposeBackupCleanup: 'Compose yedekleme çalışma alanını temizle'
ComposeRecoverPrepare: 'Compose geri yükleme çalışma alanını hazırla'
ComposeRecoverExtract: 'Compose yedek arşivini çıkar'
ComposeRecoverMeta: 'Compose yedek meta verisini yükle'
ComposeRecoverMetaLogName: 'Compose geri yükleme meta: ad={{ .name }}'
ComposeRecoverMetaLogPath: 'Compose geri yükleme meta: yedekYolu={{ .backupPath }} hedefYol={{ .targetDir }}'
ComposeRecoverMetaLogCount: 'Compose geri yükleme meta: dosya={{ .files }} konteyner={{ .containers }}'
ComposeRecoverFiles: 'Compose dosyalarını geri yükle'
ComposeRecoverContainers: 'Compose konteynerlerini geri yükle'
ComposeRecoverRecord: 'Compose kaydını sakla'
ComposeRecoverCleanup: 'Compose geri yükleme çalışma alanını temizle'
ErrFileNotExist: '{{ .detail }} dosyası mevcut değil Lütfen kaynak dosyanın bütünlüğünü kontrol edin'
ErrImageBuildErr: 'Image yapı başarısız'
ErrImageExist: 'Görüntü zaten var Lütfen görüntü adını değiştirin.'
@@ -474,3 +509,9 @@ PartitionDiskErr: 'Bölümleme başarısız, {{ .err }}'
FormatDiskErr: 'Disk biçimlendirme başarısız, {{ .err }}'
MountDiskErr: 'Disk bağlama başarısız, {{ .err }}'
XfsNotFound: 'XFS dosya sistemi algılanmadı, lütfen önce xfsprogs''i yükleyin'
# terminal
TerminalAIBlockedRiskyCommand: 'Riskli komut engellendi: {{ .command }}'
TerminalAIThinking: 'AI dusunuyor...'
TerminalAIReadyToExecute: 'Dusunme tamamlandi, calistirmak icin Enter tusuna basin (sure: {{ .duration }}, token: {{ .tokens }})'
TerminalAIRequestFailed: 'AI istegi basarisiz oldu: {{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '請求參數錯誤: {{ .detail }}'
ErrTokenParse: 'Token 產生錯誤: {{ .detail }}'
ErrInitialPassword: '原密碼錯誤'
ErrInternalServer: '服務內部錯誤: {{ .detail }}'
ErrWgetRemoteFailed: '遠端下載網址無效或無法存取:{{ .detail }}'
ErrWgetInvalidContentType: '遠端網址未回傳檔案內容(可能是錯誤頁),{{ .detail }}'
ErrRecordExist: '記錄已存在'
ErrRecordNotFound: '記錄未能找到'
ErrStructTransform: '型別轉換失敗: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }}
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
ErrAgentProviderMismatch: '帳號提供商不符,請重試'
ErrAgentAccountModelsRequired: '請至少配置一個模型'
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
@@ -209,6 +214,36 @@ BuildCache: '構建快取'
Volume: '磁碟區'
Network: '網路'
PruneStart: '清理中,請稍候...'
ContainerBackupPrepare: '準備容器備份工作目錄'
ContainerBackupStop: '備份前停止容器'
ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
ContainerBackupMounts: '備份容器掛載資料'
ContainerBackupMeta: '寫入容器備份中繼資訊'
ContainerBackupStart: '備份後啟動容器'
ContainerRecoverPrepare: '準備容器還原工作目錄'
ContainerRecoverExtract: '解壓容器備份檔案'
ContainerRecoverParse: '載入容器備份中繼資訊'
ContainerRecoverCreate: '重建容器'
ContainerRecoverMounts: '還原容器掛載資料'
ContainerRecoverStart: '啟動已還原容器'
ContainerRecoverCleanup: '清理容器還原工作目錄'
ComposeBackupPrepare: '準備編排備份工作目錄'
ComposeBackupStop: '備份前停止編排'
ComposeBackupFiles: '備份編排檔案'
ComposeBackupContainers: '備份編排容器'
ComposeBackupMeta: '寫入編排備份中繼資訊'
ComposeBackupStart: '備份後啟動編排'
ComposeBackupCleanup: '清理編排備份工作目錄'
ComposeRecoverPrepare: '準備編排還原工作目錄'
ComposeRecoverExtract: '解壓編排備份檔案'
ComposeRecoverMeta: '載入編排備份中繼資訊'
ComposeRecoverMetaLogName: '編排還原中繼資訊:名稱={{ .name }}'
ComposeRecoverMetaLogPath: '編排還原中繼資訊:備份路徑={{ .backupPath }} 目標路徑={{ .targetDir }}'
ComposeRecoverMetaLogCount: '編排還原中繼資訊:檔案數={{ .files }} 容器數={{ .containers }}'
ComposeRecoverFiles: '還原編排檔案'
ComposeRecoverContainers: '還原編排容器'
ComposeRecoverRecord: '保存編排記錄'
ComposeRecoverCleanup: '清理編排還原工作目錄'
ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!'
ErrImageBuildErr: '映像build 失敗'
ErrImageExist: '映像已存在!請修改映像名稱。'
@@ -474,3 +509,9 @@ PartitionDiskErr: '分區失敗,{{ .err }}'
FormatDiskErr: '格式化磁碟失敗,{{ .err }}'
MountDiskErr: '掛載磁碟失敗,{{ .err }}'
XfsNotFound: '未偵測到 xfs 檔案系統,請先安裝 xfsprogs'
# terminal
TerminalAIBlockedRiskyCommand: '已攔截風險命令:{{ .command }}'
TerminalAIThinking: 'AI 正在思考...'
TerminalAIReadyToExecute: '思考完成,請按 Enter 執行(耗時 {{ .duration }},token:{{ .tokens }})'
TerminalAIRequestFailed: 'AI 請求失敗:{{ .err }}'
+42 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: "参数错误: {{ .detail }}"
ErrTokenParse: "Token 错误: {{ .detail }}"
ErrInitialPassword: "原密码错误"
ErrInternalServer: "服务错误: {{ .detail }}"
ErrWgetRemoteFailed: "远程下载地址无效或无法访问:{{ .detail }}"
ErrWgetInvalidContentType: "远程地址返回的不是文件内容(可能是错误页),{{ .detail }}"
ErrRecordExist: "记录已存在"
ErrRecordNotFound: "记录不存在"
ErrStructTransform: "类型转换失败: {{ .err }}"
@@ -44,7 +46,10 @@ ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证"
ErrAgentProviderMismatch: "账号提供商不匹配"
ErrAgentAccountModelsRequired: "请至少配置一个模型"
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
ErrAgentBaseURLRequired: "Base URL 不能为空"
ErrAgentApiKeyRequired: "API Key 不能为空"
ErrAgentComposeRequired: "自定义编排内容不能为空"
@@ -232,6 +237,36 @@ BuildCache: "构建缓存"
Volume: "存储卷"
Network: "网络"
PruneStart: "清理中,请稍候..."
ContainerBackupPrepare: "准备容器备份工作目录"
ContainerBackupStop: "备份前停止容器"
ContainerBackupInspect: "备份容器 Inspect 与网络信息"
ContainerBackupMounts: "备份容器挂载数据"
ContainerBackupMeta: "写入容器备份元信息"
ContainerBackupStart: "备份后启动容器"
ContainerRecoverPrepare: "准备容器恢复工作目录"
ContainerRecoverExtract: "解压容器备份文件"
ContainerRecoverParse: "加载容器备份元信息"
ContainerRecoverCreate: "重建容器"
ContainerRecoverMounts: "恢复容器挂载数据"
ContainerRecoverStart: "启动已恢复容器"
ContainerRecoverCleanup: "清理容器恢复工作目录"
ComposeBackupPrepare: "准备编排备份工作目录"
ComposeBackupStop: "备份前停止编排"
ComposeBackupFiles: "备份编排文件"
ComposeBackupContainers: "备份编排容器"
ComposeBackupMeta: "写入编排备份元信息"
ComposeBackupStart: "备份后启动编排"
ComposeBackupCleanup: "清理编排备份工作目录"
ComposeRecoverPrepare: "准备编排恢复工作目录"
ComposeRecoverExtract: "解压编排备份文件"
ComposeRecoverMeta: "加载编排备份元信息"
ComposeRecoverMetaLogName: "编排恢复元信息:名称={{ .name }}"
ComposeRecoverMetaLogPath: "编排恢复元信息:备份路径={{ .backupPath }} 目标路径={{ .targetDir }}"
ComposeRecoverMetaLogCount: "编排恢复元信息:文件数={{ .files }} 容器数={{ .containers }}"
ComposeRecoverFiles: "恢复编排文件"
ComposeRecoverContainers: "恢复编排容器"
ComposeRecoverRecord: "保存编排记录"
ComposeRecoverCleanup: "清理编排恢复工作目录"
#runtime
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
@@ -532,3 +567,9 @@ PartitionDiskErr: "分区失败,{{ .err }}"
FormatDiskErr: "格式化磁盘失败,{{ .err }}"
MountDiskErr: "挂载磁盘失败,{{ .err }}"
XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs"
# terminal
TerminalAIBlockedRiskyCommand: "已拦截风险命令:{{ .command }}"
TerminalAIThinking: "AI 正在思考..."
TerminalAIReadyToExecute: "思考完成,请回车执行(耗时 {{ .duration }},token:{{ .tokens }})"
TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
+6 -1
View File
@@ -43,6 +43,7 @@ func InitAgentDB() {
migrations.InitLocalSSHShow,
migrations.InitRecordStatus,
migrations.AddShowNameForQuickJump,
migrations.AddAgentQuickJump,
migrations.AddTimeoutForClam,
migrations.UpdateCronjobSpec,
migrations.UpdateWebsiteSSLAddColumn,
@@ -64,7 +65,6 @@ func InitAgentDB() {
migrations.AddCronjobArgs,
migrations.AddWebsiteAcmeAccountColumn,
migrations.AddAgentTables,
migrations.MigrateOpenclawAgents,
migrations.AddAgentCustomModelFields,
migrations.AddAppInstallSortOrder,
migrations.AddAgentAccountRememberAPIKey,
@@ -72,6 +72,11 @@ func InitAgentDB() {
migrations.AddAgentTypeForAgents,
migrations.NormalizeAgentAccountVerifiedStatus,
migrations.NormalizeOllamaAccountAPIType,
migrations.InitAgentAccountModelPool,
migrations.AddHostTable,
migrations.AddAITerminalSettings,
migrations.UpdateAgentQuickJumpTitle,
migrations.FixOpenclaw20260323HTTPPort,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+225 -108
View File
@@ -52,6 +52,7 @@ var AddTable = &gormigrate.Migration{
&model.DatabasePostgresql{},
&model.Favorite{},
&model.Firewall{},
&model.Host{},
&model.Ftp{},
&model.ImageRepo{},
&model.ScriptLibrary{},
@@ -589,6 +590,67 @@ var AddShowNameForQuickJump = &gormigrate.Migration{
},
}
var AddAgentQuickJump = &gormigrate.Migration{
ID: "20260312-add-agent-quick-jump",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.QuickJump{}); err != nil {
return err
}
var quicks []model.QuickJump
if err := tx.Find(&quicks).Error; err != nil {
return err
}
var (
cronjob *model.QuickJump
database *model.QuickJump
showList []*model.QuickJump
)
for i := range quicks {
switch quicks[i].Name {
case "Cronjob":
cronjob = &quicks[i]
case "Database":
database = &quicks[i]
}
if quicks[i].IsShow {
showList = append(showList, &quicks[i])
}
}
showCount := len(showList)
updatedIDs := make(map[uint]struct{})
kickedCronjob := false
if showCount >= 4 && cronjob != nil && cronjob.IsShow {
cronjob.IsShow = false
updatedIDs[cronjob.ID] = struct{}{}
kickedCronjob = true
}
if !kickedCronjob && showCount >= 4 && database != nil && database.IsShow {
database.IsShow = false
updatedIDs[database.ID] = struct{}{}
}
for _, item := range quicks {
if _, ok := updatedIDs[item.ID]; !ok {
continue
}
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Update("is_show", item.IsShow).Error; err != nil {
return err
}
}
return tx.Create(&model.QuickJump{
Name: "Agent",
Title: "aiTools.agents.agent",
Recommend: 1,
IsShow: true,
Router: "/ai/agents/agent",
}).Error
},
}
var AddTimeoutForClam = &gormigrate.Migration{
ID: "20250922-add-timeout-for-clam",
Migrate: func(tx *gorm.DB) error {
@@ -835,114 +897,6 @@ var AddAgentTables = &gormigrate.Migration{
},
}
var MigrateOpenclawAgents = &gormigrate.Migration{
ID: "20260207-migrate-openclaw-agents",
Migrate: func(tx *gorm.DB) error {
var installs []model.AppInstall
if err := tx.Preload("App").Find(&installs).Error; err != nil {
return err
}
for _, install := range installs {
appKey := install.App.Key
if appKey == "" || install.App.Resource == "" {
var app model.App
if err := tx.First(&app, install.AppId).Error; err == nil {
install.App = app
appKey = app.Key
}
}
if appKey != constant.AppOpenclaw {
continue
}
var count int64
if err := tx.Model(&model.Agent{}).Where("app_install_id = ?", install.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
envMap := map[string]interface{}{}
if strings.TrimSpace(install.Env) != "" {
_ = json.Unmarshal([]byte(install.Env), &envMap)
}
configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json")
cfgMeta := migrationutils.OpenclawMeta{}
if fileData, err := os.ReadFile(configPath); err == nil {
cfgMeta = migrationutils.ParseOpenclawMeta(fileData)
}
provider := strings.ToLower(migrationutils.GetEnvStr(envMap, "PROVIDER"))
if provider == "" {
provider = strings.ToLower(cfgMeta.Provider)
}
if provider == "" {
continue
}
provider = migrationutils.NormalizeOpenclawProvider(provider, cfgMeta.BaseURL)
modelName := migrationutils.GetEnvStr(envMap, "MODEL")
if modelName == "" {
modelName = cfgMeta.Model
}
baseURL := migrationutils.GetEnvStr(envMap, "BASE_URL")
if baseURL == "" {
baseURL = cfgMeta.BaseURL
}
apiKey := migrationutils.GetEnvStr(envMap, "API_KEY")
if apiKey == "" {
apiKey = cfgMeta.APIKey
}
token := migrationutils.GetEnvStr(envMap, "OPENCLAW_GATEWAY_TOKEN")
if token == "" {
token = cfgMeta.Token
}
if provider != "ollama" {
if baseURL == "" {
if defaultURL, ok := migrationutils.DefaultBaseURL(provider); ok {
baseURL = defaultURL
}
}
}
if provider == "ollama" && baseURL == "" {
continue
}
var account model.AgentAccount
err := tx.Where("provider = ? AND api_key = ? AND base_url = ?", provider, apiKey, baseURL).First(&account).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
account = model.AgentAccount{
Provider: provider,
Name: install.Name,
APIKey: apiKey,
BaseURL: baseURL,
Verified: apiKey != "" || provider == "ollama",
}
if err := tx.Create(&account).Error; err != nil {
return err
}
} else {
return err
}
}
agent := model.Agent{
Name: install.Name,
Provider: provider,
Model: modelName,
BaseURL: baseURL,
APIKey: apiKey,
Token: token,
Status: install.Status,
Message: install.Message,
AppInstallID: install.ID,
AccountID: account.ID,
ConfigPath: configPath,
}
if err := tx.Create(&agent).Error; err != nil {
return err
}
}
return nil
},
}
var AddAgentCustomModelFields = &gormigrate.Migration{
ID: "20260224-add-agent-custom-model-fields",
Migrate: func(tx *gorm.DB) error {
@@ -1034,3 +988,166 @@ var NormalizeOllamaAccountAPIType = &gormigrate.Migration{
Update("api_type", "openai-responses").Error
},
}
var InitAgentAccountModelPool = &gormigrate.Migration{
ID: "20260319-init-agent-account-model-pool",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.AgentAccountModel{}); err != nil {
return err
}
return migrationutils.MigrateAgentAccountModelPool(tx)
},
}
var AddHostTable = &gormigrate.Migration{
ID: "20260318-add-host-table",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Host{}); err != nil {
return err
}
if global.CoreDB == nil || !global.CoreDB.Migrator().HasTable("hosts") {
if err := tx.Create(&model.Group{Name: "Default", Type: "host", IsDefault: true}).Error; err != nil {
return err
}
return nil
}
var encryptSetting model.Setting
if err := global.CoreDB.Where("key = ?", "EncryptKey").First(&encryptSetting).Error; err != nil {
global.LOG.Errorf("failed to get encrypt key from core db, err: %v", err)
return nil
}
coreEncryptKey := strings.TrimSpace(encryptSetting.Value)
if coreEncryptKey == "" {
global.LOG.Error("encrypt key from core db is empty")
return nil
}
groupIDMap := make(map[uint]uint)
defaultGroupID := uint(0)
var coreGroups []model.Group
if err := global.CoreDB.Where("type = ?", "host").Order("id asc").Find(&coreGroups).Error; err != nil {
return err
}
for _, coreGroup := range coreGroups {
agentGroup := model.Group{
Name: coreGroup.Name,
Type: "host",
IsDefault: coreGroup.IsDefault,
}
if agentGroup.IsDefault {
defaultGroupID = coreGroup.ID
}
if err := tx.Create(&agentGroup).Error; err != nil {
global.LOG.Errorf("failed to create group, group id: %v, err: %v", coreGroup.ID, err)
continue
}
groupIDMap[coreGroup.ID] = agentGroup.ID
}
var coreHosts []model.Host
if err := global.CoreDB.Order("id asc").Find(&coreHosts).Error; err != nil {
return err
}
for _, coreHost := range coreHosts {
password, err := encrypt.StringDecryptWithKey(coreHost.Password, coreEncryptKey)
if err != nil {
global.LOG.Errorf("failed to decrypt host password, host id: %v, err: %v", coreHost.ID, err)
continue
}
privateKey, err := encrypt.StringDecryptWithKey(coreHost.PrivateKey, coreEncryptKey)
if err != nil {
global.LOG.Errorf("failed to decrypt host private key, host id: %v, err: %v", coreHost.ID, err)
continue
}
passPhrase, err := encrypt.StringDecryptWithKey(coreHost.PassPhrase, coreEncryptKey)
if err != nil {
global.LOG.Errorf("failed to decrypt host pass phrase, host id: %v, err: %v", coreHost.ID, err)
continue
}
encryptedPassword, err := encrypt.StringEncrypt(password)
if err != nil {
global.LOG.Errorf("failed to encrypt host password, host id: %v, err: %v", coreHost.ID, err)
continue
}
encryptedPrivateKey, err := encrypt.StringEncrypt(privateKey)
if err != nil {
global.LOG.Errorf("failed to encrypt host private key, host id: %v, err: %v", coreHost.ID, err)
continue
}
encryptedPassPhrase, err := encrypt.StringEncrypt(passPhrase)
if err != nil {
global.LOG.Errorf("failed to encrypt host pass phrase, host id: %v, err: %v", coreHost.ID, err)
continue
}
groupID := defaultGroupID
if mappedGroupID, ok := groupIDMap[coreHost.GroupID]; ok && mappedGroupID != 0 {
groupID = mappedGroupID
}
host := model.Host{
GroupID: groupID,
Name: coreHost.Name,
Addr: coreHost.Addr,
Port: coreHost.Port,
User: coreHost.User,
AuthMode: coreHost.AuthMode,
Password: encryptedPassword,
PrivateKey: encryptedPrivateKey,
PassPhrase: encryptedPassPhrase,
RememberPassword: coreHost.RememberPassword,
Description: coreHost.Description,
}
if err := tx.Create(&host).Error; err != nil {
global.LOG.Errorf("failed to create host, host id: %v, err: %v", coreHost.ID, err)
continue
}
}
return nil
},
}
var AddAITerminalSettings = &gormigrate.Migration{
ID: "20260318-add-ai-terminal-settings",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "AIStatus", Value: constant.StatusDisable}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "AIAccountID", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "AIPrefix", Value: constant.DefaultTerminalAIPrefix}).Error; err != nil {
return err
}
return tx.Create(&model.Setting{
Key: "AIRiskCommands",
Value: constant.DefaultTerminalAIRiskCommands,
}).Error
},
}
var UpdateAgentQuickJumpTitle = &gormigrate.Migration{
ID: "20260324-update-agent-quick-jump-title",
Migrate: func(tx *gorm.DB) error {
return tx.Model(&model.QuickJump{}).
Where("title = ?", "aiTools.agents.agents").
Update("title", "aiTools.agents.agent").Error
},
}
var FixOpenclaw20260323HTTPPort = &gormigrate.Migration{
ID: "20260325-fix-openclaw-20260323-http-port",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(
`UPDATE app_installs
SET http_port = https_port,
https_port = 0
WHERE version = ?
AND https_port > 0
AND app_id IN (SELECT id FROM apps WHERE key = ?)`,
"2026.3.24",
constant.AppOpenclaw,
).Error
},
}
@@ -0,0 +1,134 @@
package utils
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/app/service"
"gorm.io/gorm"
)
type legacyAgentAccountModelPoolSource struct {
model.AgentAccount
LegacyModel string `gorm:"column:model"`
LegacyModels string `gorm:"column:models"`
}
func MigrateAgentAccountModelPool(tx *gorm.DB) error {
var accounts []legacyAgentAccountModelPoolSource
if err := tx.Table(model.AgentAccount{}.TableName()).Find(&accounts).Error; err != nil {
return err
}
for _, account := range accounts {
count := int64(0)
if err := tx.Model(&model.AgentAccountModel{}).Where("account_id = ?", account.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
models, err := buildMigratedAgentAccountModels(tx, &account)
if err != nil {
return err
}
if len(models) == 0 {
continue
}
for index, item := range models {
inputPayload, err := json.Marshal(item.Input)
if err != nil {
return err
}
record := &model.AgentAccountModel{
AccountID: account.ID,
Model: item.ID,
Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
}
if err := tx.Create(record).Error; err != nil {
return err
}
}
}
return nil
}
func buildMigratedAgentAccountModels(tx *gorm.DB, account *legacyAgentAccountModelPoolSource) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, nil
}
baseAccount := account.AgentAccount
requested := make([]dto.AgentAccountModel, 0)
if strings.TrimSpace(account.LegacyModels) != "" {
if err := json.Unmarshal([]byte(account.LegacyModels), &requested); err != nil {
return nil, err
}
}
seen := make(map[string]struct{}, len(requested))
for _, item := range requested {
target := strings.TrimSpace(item.ID)
if target == "" {
continue
}
seen[target] = struct{}{}
}
appendModel := func(modelID string) {
target := strings.TrimSpace(modelID)
if target == "" {
return
}
if _, ok := seen[target]; ok {
return
}
seen[target] = struct{}{}
requested = append(requested, dto.AgentAccountModel{ID: target})
}
appendModel(account.LegacyModel)
if account.ID > 0 {
var agents []model.Agent
if err := tx.Where("account_id = ?", account.ID).Find(&agents).Error; err != nil {
return nil, err
}
for _, agent := range agents {
appendModel(agent.Model)
}
}
models, err := service.MergeCatalogAgentAccountModelsForMigration(&baseAccount, requested)
if err != nil {
if strings.TrimSpace(err.Error()) == "model is required" {
return nil, nil
}
return nil, err
}
applyLegacyAgentAccountModelDefaults(account.Provider, models)
return models, nil
}
func applyLegacyAgentAccountModelDefaults(provider string, models []dto.AgentAccountModel) {
if provider != "custom" && provider != "ollama" && provider != "vllm" {
return
}
meta, ok := providercatalog.Get(provider)
if !ok {
return
}
for i := range models {
if strings.TrimSpace(models[i].Name) == "" {
models[i].Name = strings.TrimSpace(models[i].ID)
}
if models[i].ContextWindow <= 0 {
models[i].ContextWindow = meta.Default.ContextWindow
}
if models[i].MaxTokens <= 0 {
models[i].MaxTokens = meta.Default.MaxTokens
}
}
}
@@ -1,164 +0,0 @@
package utils
import (
"encoding/json"
"fmt"
"strings"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
)
type OpenclawMeta struct {
Provider string
Model string
BaseURL string
APIKey string
Token string
}
func GetEnvStr(envMap map[string]interface{}, key string) string {
if envMap == nil {
return ""
}
if value, ok := envMap[key]; ok {
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
default:
return strings.TrimSpace(fmt.Sprintf("%v", v))
}
}
return ""
}
func ParseOpenclawMeta(fileData []byte) OpenclawMeta {
meta := OpenclawMeta{}
content := map[string]interface{}{}
if err := json.Unmarshal(fileData, &content); err != nil {
return meta
}
meta.Token = getNestedString(content, "gateway", "auth", "token")
meta.Model = getNestedString(content, "agents", "defaults", "model", "primary")
providerKey := ""
if parts := strings.SplitN(meta.Model, "/", 2); len(parts) == 2 {
providerKey = strings.TrimSpace(parts[0])
}
providers := getNestedMap(content, "models", "providers")
providerConfig := map[string]interface{}{}
if providerKey != "" {
if cfg, ok := providers[providerKey].(map[string]interface{}); ok {
providerConfig = cfg
}
}
if len(providerConfig) == 0 && len(providers) == 1 {
for key, value := range providers {
if cfg, ok := value.(map[string]interface{}); ok {
providerKey = key
providerConfig = cfg
break
}
}
}
meta.Provider = providerKey
meta.BaseURL = getString(providerConfig, "baseUrl")
meta.APIKey = getString(providerConfig, "apiKey")
if meta.Model == "" && providerKey != "" {
if modelID := getProviderFirstModelID(providerConfig); modelID != "" {
meta.Model = providerKey + "/" + modelID
}
}
return meta
}
func NormalizeOpenclawProvider(provider, baseURL string) string {
p := strings.ToLower(strings.TrimSpace(provider))
base := strings.ToLower(strings.TrimSpace(baseURL))
switch p {
case "minimax-portal":
return "minimax"
case "moonshot":
if strings.Contains(base, "moonshot.cn") {
return "kimi"
}
return "moonshot"
default:
return p
}
}
func DefaultBaseURL(provider string) (string, bool) {
return providercatalog.DefaultBaseURL(provider)
}
func getNestedMap(data map[string]interface{}, keys ...string) map[string]interface{} {
current := data
for _, key := range keys {
next, ok := current[key].(map[string]interface{})
if !ok {
return map[string]interface{}{}
}
current = next
}
return current
}
func getNestedString(data map[string]interface{}, keys ...string) string {
current := data
for i, key := range keys {
value, ok := current[key]
if !ok {
return ""
}
if i == len(keys)-1 {
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
default:
return strings.TrimSpace(fmt.Sprintf("%v", v))
}
}
next, ok := value.(map[string]interface{})
if !ok {
return ""
}
current = next
}
return ""
}
func getString(data map[string]interface{}, key string) string {
if data == nil {
return ""
}
value, ok := data[key]
if !ok {
return ""
}
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
default:
return strings.TrimSpace(fmt.Sprintf("%v", v))
}
}
func getProviderFirstModelID(data map[string]interface{}) string {
if data == nil {
return ""
}
rawModels, ok := data["models"].([]interface{})
if !ok || len(rawModels) == 0 {
return ""
}
first, ok := rawModels[0].(map[string]interface{})
if !ok {
return ""
}
return getString(first, "id")
}
+3 -1
View File
@@ -75,7 +75,9 @@ func OperationResolveMeta() gin.HandlerFunc {
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
bodyMap, _ = parseMultipart(body, c.Request.Header.Get("Content-Type"))
} else {
_ = json.Unmarshal(body, &bodyMap)
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
_ = decoder.Decode(&bodyMap)
}
for _, key := range meta.BodyKeys {
if value, ok := bodyMap[key]; ok {
+18 -3
View File
@@ -45,27 +45,42 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent)
aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken)
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview)
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
aiToolsRouter.POST("/agents/accounts/search", baseApi.PageAgentAccounts)
aiToolsRouter.POST("/agents/accounts/models", baseApi.GetAgentAccountModels)
aiToolsRouter.POST("/agents/accounts/models/create", baseApi.CreateAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/models/update", baseApi.UpdateAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/models/delete", baseApi.DeleteAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount)
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/approve", baseApi.ApproveAgentFeishuPairing)
aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig)
aiToolsRouter.POST("/agents/channel/discord/update", baseApi.UpdateAgentDiscordConfig)
aiToolsRouter.POST("/agents/channel/wecom/get", baseApi.GetAgentWecomConfig)
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/login", baseApi.LoginAgentWeixinChannel)
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
aiToolsRouter.POST("/agents/plugin/check", baseApi.CheckAgentPlugin)
aiToolsRouter.POST("/agents/browser/get", baseApi.GetAgentBrowserConfig)
aiToolsRouter.POST("/agents/browser/update", baseApi.UpdateAgentBrowserConfig)
aiToolsRouter.POST("/agents/security/get", baseApi.GetAgentSecurityConfig)
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
aiToolsRouter.POST("/agents/other/update", baseApi.UpdateAgentOtherConfig)
aiToolsRouter.POST("/agents/config-file/get", baseApi.GetAgentConfigFile)
aiToolsRouter.POST("/agents/config-file/update", baseApi.UpdateAgentConfigFile)
aiToolsRouter.POST("/agents/skills/list", baseApi.ListAgentSkills)
aiToolsRouter.POST("/agents/skills/search", baseApi.SearchAgentSkills)
aiToolsRouter.POST("/agents/skills/update", baseApi.UpdateAgentSkill)
aiToolsRouter.POST("/agents/skills/install", baseApi.InstallAgentSkill)
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
}
}
+6
View File
@@ -35,6 +35,12 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/prune", baseApi.ContainerPrune)
baRouter.POST("/users", baseApi.LoadContainerUsers)
baRouter.POST("/files/search", baseApi.ListContainerFiles)
baRouter.POST("/files/upload", baseApi.UploadContainerFile)
baRouter.POST("/files/content", baseApi.GetContainerFileContent)
baRouter.POST("/files/size", baseApi.GetContainerFileSize)
baRouter.POST("/files/del", baseApi.DeleteContainerFile)
baRouter.POST("/files/download", baseApi.DownloadContainerFile)
baRouter.GET("/repo", baseApi.ListRepo)
baRouter.POST("/repo/status", baseApi.CheckRepoStatus)
+1
View File
@@ -33,6 +33,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles)
fileRouter.POST("/rename", baseApi.ChangeFileName)
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/wget/stop", baseApi.StopWget)
fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
+10
View File
@@ -11,6 +11,16 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi
{
hostRouter.POST("", baseApi.CreateHost)
hostRouter.POST("/info", baseApi.GetHostByID)
hostRouter.POST("/del", baseApi.DeleteHost)
hostRouter.POST("/update", baseApi.UpdateHost)
hostRouter.POST("/update/group", baseApi.UpdateHostGroup)
hostRouter.POST("/search", baseApi.SearchHost)
hostRouter.POST("/tree", baseApi.HostTree)
hostRouter.POST("/test/byinfo", baseApi.TestByInfo)
hostRouter.POST("/test/byid", baseApi.TestByID)
hostRouter.POST("/firewall/base", baseApi.LoadFirewallBaseInfo)
hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
+2
View File
@@ -12,8 +12,10 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
settingRouter.POST("/search", baseApi.GetSettingInfo)
settingRouter.POST("/terminal/ai/search", baseApi.GetTerminalAISettingInfo)
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.POST("/terminal/ai/update", baseApi.UpdateTerminalAISetting)
settingRouter.GET("/get/:key", baseApi.GetSettingByKey)
settingRouter.POST("/description/save", baseApi.SaveDescription)
@@ -34,10 +34,6 @@ type authorizer struct {
defAuth Authenticator
}
type preemptiveAuthorizer struct {
auth Authenticator
}
type authShim struct {
factory AuthFactory
body io.Reader
+1 -1
View File
@@ -150,7 +150,7 @@ func LoadServiceName(keyword string) (string, error) {
}
processedName := loadProcessedName(client.Name(), keyword)
exist, err := client.IsExist(processedName)
exist, _ := client.IsExist(processedName)
if exist {
return processedName, nil
}
+98 -2
View File
@@ -12,11 +12,13 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
)
@@ -301,7 +303,11 @@ func (c Client) BuildImageWithProcessAndOptions(task *task.Task, tar io.ReadClos
}
func (c Client) PullImageWithProcess(task *task.Task, imageName string) error {
return c.PullImageWithProcessAndOptions(task, imageName, image.PullOptions{})
options := image.PullOptions{}
if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok {
options.RegistryAuth = authStr
}
return c.PullImageWithProcessAndOptions(task, imageName, options)
}
func logProcess(progress map[string]interface{}, task *task.Task) {
@@ -319,8 +325,98 @@ func PullImage(imageName string) error {
return err
}
defer cli.Close()
if _, err := cli.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil {
options := image.PullOptions{}
if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok {
options.RegistryAuth = authStr
}
if _, err := cli.ImagePull(context.Background(), imageName, options); err != nil {
return err
}
return nil
}
func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) {
registryHost, hasRegistry := extractRegistryHost(imageName)
cfg := config.LoadDefaultConfigFile(io.Discard)
if cfg == nil {
return "", false
}
candidates := make([]string, 0)
if hasRegistry {
candidates = append(candidates, registryHost, "https://"+registryHost, "http://"+registryHost)
}
if !hasRegistry || isDockerHubRegistry(registryHost) {
candidates = append(candidates,
"https://index.docker.io/v1/",
"index.docker.io",
"docker.io",
"registry-1.docker.io",
"https://registry-1.docker.io",
)
}
seen := make(map[string]struct{})
for _, key := range candidates {
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
auth, err := cfg.GetAuthConfig(key)
if err != nil {
continue
}
if auth.Username == "" && auth.Password == "" && auth.Auth == "" && auth.IdentityToken == "" && auth.RegistryToken == "" {
continue
}
authStr, err := registry.EncodeAuthConfig(registry.AuthConfig{
Username: auth.Username,
Password: auth.Password,
Auth: auth.Auth,
ServerAddress: auth.ServerAddress,
IdentityToken: auth.IdentityToken,
RegistryToken: auth.RegistryToken,
})
if err != nil {
return "", false
}
return authStr, true
}
return "", false
}
func isDockerHubRegistry(host string) bool {
switch normalizeRegistryHost(host) {
case "docker.io", "index.docker.io", "registry-1.docker.io":
return true
default:
return false
}
}
func extractRegistryHost(imageName string) (string, bool) {
parts := strings.Split(imageName, "/")
if len(parts) < 2 {
return "", false
}
first := parts[0]
if strings.Contains(first, ".") || strings.Contains(first, ":") || first == "localhost" {
return normalizeRegistryHost(first), true
}
return "", false
}
func normalizeRegistryHost(registryKey string) string {
key := strings.TrimSpace(registryKey)
if key == "" {
return ""
}
key = strings.TrimPrefix(key, "http://")
key = strings.TrimPrefix(key, "https://")
key = strings.Trim(key, "/")
if strings.Contains(key, "/") {
key = strings.SplitN(key, "/", 2)[0]
}
return strings.ToLower(key)
}
+187 -20
View File
@@ -17,6 +17,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -333,6 +334,17 @@ func (f FileOp) Rename(oldName string, newName string) error {
return f.Fs.Rename(oldName, newName)
}
type downloadTask struct {
resp *http.Response
file *os.File
dst string
}
var (
downloadMu sync.Mutex
downloadTasks = make(map[string]*downloadTask)
)
type WriteCounter struct {
Total uint64
Written uint64
@@ -382,37 +394,83 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
}
}
defer client.CloseIdleConnections()
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil
return buserr.WithDetail("ErrWgetRemoteFailed", err.Error(), err)
}
request.Header.Set("Accept-Encoding", "identity")
resp, err := client.Do(request)
if err != nil {
global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error())
return err
return buserr.WithDetail("ErrWgetRemoteFailed", err.Error(), err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64*1024))
_ = resp.Body.Close()
global.LOG.Errorf("wget remote returned non-success status %s for url %s", resp.Status, url)
return buserr.WithDetail("ErrWgetRemoteFailed", resp.StatusCode, nil)
}
ct := strings.ToLower(resp.Header.Get("Content-Type"))
dstExt := strings.ToLower(filepath.Ext(dst))
if (strings.Contains(ct, "text/html") || strings.Contains(ct, "text/xml")) &&
dstExt != ".html" && dstExt != ".htm" && dstExt != ".xml" && dstExt != ".svg" {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64*1024))
_ = resp.Body.Close()
detail := fmt.Sprintf("Content-Type: %s", ct)
global.LOG.Errorf("wget got html/xml response for non-html file %s, url %s, %s", dst, url, detail)
return buserr.WithDetail("ErrWgetInvalidContentType", detail, nil)
}
out, err := os.Create(dst)
if err != nil {
global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error())
resp.Body.Close()
return err
}
downloadMu.Lock()
downloadTasks[key] = &downloadTask{
resp: resp,
file: out,
dst: dst,
}
downloadMu.Unlock()
go func() {
defer func() {
out.Close()
resp.Body.Close()
downloadMu.Lock()
delete(downloadTasks, key)
downloadMu.Unlock()
}()
counter := &WriteCounter{}
counter.Key = key
if resp.ContentLength > 0 {
counter.Total = uint64(resp.ContentLength)
}
counter.Name = filepath.Base(dst)
if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
if _, err := io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error())
global.CACHE.Del(counter.Key)
return
}
out.Close()
resp.Body.Close()
value := global.CACHE.Get(counter.Key)
if value == "" {
return
}
process := &Process{}
_ = json.Unmarshal([]byte(value), process)
if err := json.Unmarshal([]byte(value), process); err != nil {
return
}
process.Percent = 100
process.Name = counter.Name
process.Total = process.Written
@@ -422,6 +480,25 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
return nil
}
func CancelDownload(key string) {
downloadMu.Lock()
task, ok := downloadTasks[key]
if !ok {
downloadMu.Unlock()
return
}
dst := task.dst
downloadMu.Unlock()
_ = task.file.Close()
_ = task.resp.Body.Close()
if dst != "" {
_ = os.Remove(dst)
}
global.CACHE.Del(key)
}
func (f FileOp) DownloadFile(url, dst string) error {
resp, err := req_helper.HandleGet(url)
if err != nil {
@@ -527,22 +604,22 @@ func (f FileOp) CopyAndReName(src, dst, name string, cover bool) error {
if name != "" && !cover {
dstPath = filepath.Join(dst, name)
}
return cmd.RunDefaultBashCf(`cp -rf '%s' '%s'`, src, dstPath)
return cmd.RunDefaultBashCf(`cp -rfp '%s' '%s'`, src, dstPath)
} else {
dstPath := filepath.Join(dst, name)
if cover {
dstPath = dst
}
return cmd.RunDefaultBashCf(`cp -f '%s' '%s'`, src, dstPath)
return cmd.RunDefaultBashCf(`cp -fp '%s' '%s'`, src, dstPath)
}
}
func (f FileOp) CopyDirWithNewName(src, dst, newName string) error {
if newName == "." || newName == "" {
return cmd.RunDefaultBashCf(`cp -rf '%s'/. '%s'`, src, dst)
return cmd.RunDefaultBashCf(`cp -rfp '%s'/. '%s'`, src, dst)
}
dstDir := filepath.Join(dst, newName)
return cmd.RunDefaultBashCf(`cp -rf '%s' '%s'`, src, dstDir)
return cmd.RunDefaultBashCf(`cp -rfp '%s' '%s'`, src, dstDir)
}
func (f FileOp) CopyDir(src, dst string) error {
@@ -554,7 +631,7 @@ func (f FileOp) CopyDir(src, dst string) error {
if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rf '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rfp '%s' '%s'`, src, dst+"/")
}
func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error {
@@ -567,7 +644,7 @@ func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error
return err
}
if len(excludeNames) == 0 {
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rf '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rfp '%s' '%s'`, src, dst+"/")
}
tmpFiles, err := os.ReadDir(src)
if err != nil {
@@ -600,7 +677,7 @@ func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error
func (f FileOp) CopyFile(src, dst string) error {
dst = filepath.Clean(dst) + string(filepath.Separator)
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -f '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -fp '%s' '%s'`, src, dst+"/")
}
func (f FileOp) GetDirSize(path string) (int64, error) {
@@ -797,13 +874,18 @@ func decodeGBK(input string) (string, error) {
func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType) error {
format := getFormat(cType)
if cType == Gz {
err := f.DecompressGzFile(srcFile, dst)
if err != nil {
return err
if err := f.tryDecompressTarGz(srcFile, dst, format); err == nil {
return nil
}
return nil
return f.DecompressGzFile(srcFile, dst)
}
type dirEntry struct {
path string
modTime time.Time
}
var dirs []dirEntry
handler := func(ctx context.Context, archFile archiver.File) error {
info := archFile.FileInfo
if isIgnoreFile(archFile.Name()) {
@@ -824,6 +906,7 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil {
return err
}
dirs = append(dirs, dirEntry{path: filePath, modTime: info.ModTime()})
return nil
} else {
parentDir := path.Dir(filePath)
@@ -846,14 +929,20 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
if _, err := io.Copy(fw, fr); err != nil {
return err
}
_ = os.Chtimes(filePath, info.ModTime(), info.ModTime())
return nil
}
input, err := f.Fs.Open(srcFile)
if err != nil {
return err
}
return format.Extract(context.Background(), input, nil, handler)
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
return err
}
for i := len(dirs) - 1; i >= 0; i-- {
_ = os.Chtimes(dirs[i].path, dirs[i].modTime, dirs[i].modTime)
}
return nil
}
func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error {
@@ -926,7 +1015,75 @@ func ZipFile(files []archiver.File, dst afero.File) error {
return nil
}
func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.CompressedArchive) error {
input, err := f.Fs.Open(srcFile)
if err != nil {
return err
}
defer input.Close()
type dirEntry struct {
path string
modTime time.Time
}
var dirs []dirEntry
extracted := false
handler := func(ctx context.Context, archFile archiver.File) error {
info := archFile.FileInfo
if isIgnoreFile(archFile.Name()) {
return nil
}
filePath := filepath.Join(dst, archFile.NameInArchive)
if info.IsDir() {
if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil {
return err
}
dirs = append(dirs, dirEntry{path: filePath, modTime: info.ModTime()})
} else {
parentDir := filepath.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
return err
}
}
fr, err := archFile.Open()
if err != nil {
return err
}
defer fr.Close()
fw, err := f.Fs.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer fw.Close()
if _, err := io.Copy(fw, fr); err != nil {
return err
}
_ = os.Chtimes(filePath, info.ModTime(), info.ModTime())
}
extracted = true
return nil
}
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
return err
}
if !extracted {
return fmt.Errorf("no files extracted as tar.gz")
}
for i := len(dirs) - 1; i >= 0; i-- {
_ = os.Chtimes(dirs[i].path, dirs[i].modTime, dirs[i].modTime)
}
return nil
}
func (f FileOp) DecompressGzFile(srcFile, dst string) error {
var archiveModTime time.Time
if st, err := f.Fs.Stat(srcFile); err == nil {
archiveModTime = st.ModTime()
}
in, err := f.Fs.Open(srcFile)
if err != nil {
return fmt.Errorf("open source file failed: %w", err)
@@ -939,7 +1096,13 @@ func (f FileOp) DecompressGzFile(srcFile, dst string) error {
}
defer gr.Close()
outName := strings.TrimSuffix(filepath.Base(srcFile), ".gz")
outName := ""
if gr.Name != "" {
outName = filepath.Base(gr.Name)
}
if outName == "" || outName == "." {
outName = strings.TrimSuffix(filepath.Base(srcFile), ".gz")
}
outPath := filepath.Join(dst, outName)
parentDir := filepath.Dir(outPath)
if !f.Stat(parentDir) {
@@ -958,6 +1121,10 @@ func (f FileOp) DecompressGzFile(srcFile, dst string) error {
return fmt.Errorf("copy content failed: %w", err)
}
if !archiveModTime.IsZero() {
_ = os.Chtimes(outPath, archiveModTime, archiveModTime)
}
return nil
}
+10
View File
@@ -3,6 +3,7 @@ package components
import (
"errors"
"fmt"
"strings"
)
type Server struct {
@@ -352,6 +353,7 @@ func (s *Server) UpdateRootLocation() {
}
func (s *Server) UpdateRootProxy(proxy []string) {
httpsProxy := len(proxy) > 0 && strings.HasPrefix(strings.ToLower(strings.TrimSpace(proxy[0])), "https://")
newDir := Directive{
Name: "location",
Parameters: []string{"/"},
@@ -392,6 +394,14 @@ func (s *Server) UpdateRootProxy(proxy []string) {
Name: "proxy_http_version",
Parameters: []string{"1.1"},
},
&Directive{
Name: "proxy_ssl_server_name",
Parameters: []string{map[bool]string{true: "on", false: "off"}[httpsProxy]},
},
&Directive{
Name: "proxy_ssl_name",
Parameters: []string{"$proxy_host"},
},
&Directive{
Name: "proxy_pass",
Parameters: proxy,
-1
View File
@@ -127,7 +127,6 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64, []float64) {
}
c.cachedTotalUsage = totalUsage
c.cachedPerCore = c.cachedPerCore
c.cachedDetailedPercent = detailedPercent
c.lastTotalStat = &curTotal
c.lastDetailStat = &curDetail
-5
View File
@@ -30,11 +30,6 @@ import (
var Orders = make(map[uint]*acme.Order)
type domainError struct {
Domain string
Error error
}
type zeroSSLRes struct {
Success bool `json:"success"`
EabKid string `json:"eab_kid"`
+823
View File
@@ -0,0 +1,823 @@
package ai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/global"
)
const (
defaultTimeout = 30 * time.Second
defaultUserAgent = "1panel-terminal-ai/1.0"
defaultAnthropicToken = 1024
)
type Client interface {
ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error)
}
type ClientConfig struct {
Provider string
BaseURL string
APIKey string
Model string
APIType string
MaxTokens int
Timeout time.Duration
HTTPClient *http.Client
}
type GeneratorConfig struct {
Provider string
BaseURL string
APIKey string
Model string
APIType string
MaxTokens int
}
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatCompletionRequest struct {
Messages []ChatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type ThinkingMode struct {
Type string `json:"type"`
}
type ChatCompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content string `json:"content"`
RawText string `json:"rawText"`
Usage ResponseUsage `json:"usage"`
}
type ResponseUsage struct {
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
TotalTokens int `json:"totalTokens"`
}
type terminalAIClient struct {
config ClientConfig
httpClient *http.Client
}
func NewClient(cfg ClientConfig) (Client, error) {
providerKey := strings.ToLower(strings.TrimSpace(cfg.Provider))
if providerKey == "" {
providerKey = "custom"
}
if strings.TrimSpace(cfg.Model) == "" {
return nil, fmt.Errorf("model is required")
}
if strings.TrimSpace(cfg.APIKey) == "" && providerKey != "ollama" {
return nil, fmt.Errorf("api key is required")
}
baseURL := normalizeBaseURL(providerKey, cfg.BaseURL)
if baseURL == "" {
return nil, fmt.Errorf("base url is required")
}
cfg.Provider = providerKey
cfg.BaseURL = baseURL
cfg.APIType = normalizeClientAPIType(providerKey, cfg.APIType)
if cfg.Timeout <= 0 {
cfg.Timeout = defaultTimeout
}
client := cfg.HTTPClient
if client == nil {
client = &http.Client{Timeout: cfg.Timeout}
}
return &terminalAIClient{
config: cfg,
httpClient: client,
}, nil
}
type ClientOption func(*ClientConfig)
func WithBaseURL(baseURL string) ClientOption {
return func(cfg *ClientConfig) {
cfg.BaseURL = baseURL
}
}
func WithTimeout(timeout time.Duration) ClientOption {
return func(cfg *ClientConfig) {
cfg.Timeout = timeout
}
}
func WithHTTPClient(client *http.Client) ClientOption {
return func(cfg *ClientConfig) {
cfg.HTTPClient = client
}
}
func (c *terminalAIClient) ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
if len(req.Messages) == 0 {
return nil, fmt.Errorf("messages are required")
}
switch c.config.APIType {
case "anthropic-messages":
return c.chatCompletionAnthropic(ctx, req)
case "gemini-generate-content":
return c.chatCompletionGemini(ctx, req)
case "openai-responses":
return c.chatCompletionOpenAIResponses(ctx, req)
default:
return c.chatCompletionOpenAI(ctx, req)
}
}
func (c *terminalAIClient) chatCompletionOpenAI(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := openAIChatCompletionRequest{
Model: normalizeModelID(c.config.Model),
Messages: req.Messages,
MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
Thinking: thinkingModeForModel(c.config.Model),
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildChatCompletionsURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp openAIChatCompletionResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractAssistantContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.PromptTokens,
CompletionTokens: completionResp.Usage.CompletionTokens,
TotalTokens: completionResp.Usage.TotalTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionOpenAIResponses(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := openAIResponsesRequest{
Model: normalizeModelID(c.config.Model),
Input: toResponsesInput(req.Messages),
MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildResponsesURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
if strings.TrimSpace(c.config.APIKey) != "" {
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp openAIResponsesResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractResponsesContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.InputTokens,
CompletionTokens: completionResp.Usage.OutputTokens,
TotalTokens: completionResp.Usage.TotalTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionAnthropic(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
system, messages := toAnthropicMessages(req.Messages)
payload := anthropicMessagesRequest{
Model: normalizeModelID(c.config.Model),
System: system,
Messages: messages,
MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens, defaultAnthropicToken),
Temperature: req.Temperature,
TopP: req.TopP,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildAnthropicMessagesURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("x-api-key", strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("anthropic-version", "2023-06-01")
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp anthropicMessagesResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractAnthropicContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.InputTokens,
CompletionTokens: completionResp.Usage.OutputTokens,
TotalTokens: completionResp.Usage.InputTokens + completionResp.Usage.OutputTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionGemini(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := geminiGenerateContentRequest{
Contents: toGeminiContents(req.Messages),
GenerationConfig: &geminiGenerationConfig{
MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
},
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildGeminiGenerateContentURL(c.config.BaseURL, normalizeModelID(c.config.Model)), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("x-goog-api-key", strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp geminiGenerateContentResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractGeminiContent(completionResp)
return &ChatCompletionResponse{
Model: firstNonEmptyString(c.config.Model, completionResp.ModelVersion),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.PromptTokenCount,
CompletionTokens: completionResp.Usage.CandidatesTokenCount,
TotalTokens: completionResp.Usage.TotalTokenCount,
},
}, nil
}
func (c *terminalAIClient) do(httpReq *http.Request) ([]byte, error) {
start := time.Now()
resp, err := c.httpClient.Do(httpReq)
if err != nil {
c.logRequestCost(httpReq, 0, time.Since(start), err)
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), err)
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= http.StatusBadRequest {
providerErr := parseProviderError(resp.StatusCode, respBody)
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), providerErr)
return nil, providerErr
}
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), nil)
return respBody, nil
}
func (c *terminalAIClient) logRequestCost(httpReq *http.Request, statusCode int, duration time.Duration, err error) {
if global.LOG == nil || httpReq == nil {
return
}
if err != nil {
global.LOG.Warnf("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s err=%v",
c.config.Provider,
c.config.Model,
c.config.APIType,
httpReq.Method,
httpReq.URL.String(),
statusCode,
duration,
err,
)
return
}
global.LOG.Infof("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s",
c.config.Provider,
c.config.Model,
c.config.APIType,
httpReq.Method,
httpReq.URL.String(),
statusCode,
duration,
)
}
type openAIChatCompletionRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Thinking *ThinkingMode `json:"thinking,omitempty"`
}
type openAIChatCompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []struct {
Message ChatMessage `json:"message"`
Text string `json:"text"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
type openAIResponsesRequest struct {
Model string `json:"model"`
Input []openAIResponsesInput `json:"input"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type openAIResponsesInput struct {
Role string `json:"role"`
Content []openAIResponsesInputPart `json:"content"`
}
type openAIResponsesInputPart struct {
Type string `json:"type"`
Text string `json:"text"`
}
type openAIResponsesResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Output []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
OutputText string `json:"output_text"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
type anthropicMessagesRequest struct {
Model string `json:"model"`
System string `json:"system,omitempty"`
Messages []anthropicMessage `json:"messages"`
MaxTokens int `json:"max_tokens"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type anthropicMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type anthropicMessagesResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
type geminiGenerateContentRequest struct {
Contents []geminiContent `json:"contents"`
GenerationConfig *geminiGenerationConfig `json:"generationConfig,omitempty"`
}
type geminiContent struct {
Role string `json:"role,omitempty"`
Parts []geminiContentPart `json:"parts"`
}
type geminiContentPart struct {
Text string `json:"text"`
}
type geminiGenerationConfig struct {
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"topP,omitempty"`
}
type geminiGenerateContentResponse struct {
Candidates []struct {
Content geminiContent `json:"content"`
} `json:"candidates"`
ModelVersion string `json:"modelVersion"`
Usage struct {
PromptTokenCount int `json:"promptTokenCount"`
CandidatesTokenCount int `json:"candidatesTokenCount"`
TotalTokenCount int `json:"totalTokenCount"`
} `json:"usageMetadata"`
}
type openAIErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
func normalizeBaseURL(provider, rawBaseURL string) string {
baseURL := strings.TrimSpace(rawBaseURL)
if baseURL == "" {
defaultBaseURL, ok := providercatalog.DefaultBaseURL(provider)
if ok {
baseURL = defaultBaseURL
}
}
return strings.TrimRight(baseURL, "/")
}
func buildChatCompletionsURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/chat/completions") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/chat/completions"
}
switch {
case strings.HasSuffix(parsed.Path, "/v1"):
parsed.Path += "/chat/completions"
case strings.HasSuffix(parsed.Path, "/v1beta"):
parsed.Path += "/chat/completions"
default:
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/chat/completions"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildResponsesURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/responses") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/responses"
}
if strings.HasSuffix(parsed.Path, "/v1") {
parsed.Path += "/responses"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/responses"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildAnthropicMessagesURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/messages") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/messages"
}
if strings.HasSuffix(parsed.Path, "/v1") {
parsed.Path += "/messages"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/messages"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildGeminiGenerateContentURL(baseURL, model string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
model = strings.TrimSpace(model)
if model == "" {
model = "gemini-3-flash-preview"
}
parsed, err := url.Parse(baseURL)
if err != nil {
return strings.TrimRight(baseURL, "/") + "/v1beta/models/" + model + ":generateContent"
}
if strings.Contains(parsed.Path, "/models/") && strings.HasSuffix(parsed.Path, ":generateContent") {
return parsed.String()
}
if strings.Contains(parsed.Path, "/v1beta") {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/models/" + model + ":generateContent"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1beta/models/" + model + ":generateContent"
}
return parsed.String()
}
func normalizeModelID(model string) string {
model = strings.TrimSpace(model)
if parts := strings.SplitN(model, "/", 2); len(parts) == 2 {
return parts[1]
}
return model
}
func thinkingModeForModel(model string) *ThinkingMode {
if strings.EqualFold(normalizeModelID(model), "kimi-k2.5") {
return &ThinkingMode{Type: "disabled"}
}
return nil
}
func normalizeClientAPIType(provider, apiType string) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "anthropic", "kimi-coding", "minimax":
return "anthropic-messages"
case "gemini":
return "gemini-generate-content"
case "ollama":
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "openai-completions" {
return trim
}
return "openai-responses"
default:
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "" {
return "openai-completions"
}
return trim
}
}
func extractAssistantContent(resp openAIChatCompletionResponse) string {
if len(resp.Choices) == 0 {
return ""
}
if content := strings.TrimSpace(resp.Choices[0].Message.Content); content != "" {
return content
}
return strings.TrimSpace(resp.Choices[0].Text)
}
func extractResponsesContent(resp openAIResponsesResponse) string {
if text := strings.TrimSpace(resp.OutputText); text != "" {
return text
}
for _, item := range resp.Output {
for _, content := range item.Content {
if strings.TrimSpace(content.Text) != "" {
return strings.TrimSpace(content.Text)
}
}
}
return ""
}
func extractAnthropicContent(resp anthropicMessagesResponse) string {
var parts []string
for _, item := range resp.Content {
if strings.TrimSpace(item.Text) == "" {
continue
}
parts = append(parts, strings.TrimSpace(item.Text))
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
func extractGeminiContent(resp geminiGenerateContentResponse) string {
if len(resp.Candidates) == 0 {
return ""
}
var parts []string
for _, part := range resp.Candidates[0].Content.Parts {
if strings.TrimSpace(part.Text) == "" {
continue
}
parts = append(parts, strings.TrimSpace(part.Text))
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
func toResponsesInput(messages []ChatMessage) []openAIResponsesInput {
result := make([]openAIResponsesInput, 0, len(messages))
for _, message := range messages {
if strings.TrimSpace(message.Content) == "" {
continue
}
result = append(result, openAIResponsesInput{
Role: normalizeRole(message.Role),
Content: []openAIResponsesInputPart{{
Type: "input_text",
Text: message.Content,
}},
})
}
return result
}
func toAnthropicMessages(messages []ChatMessage) (string, []anthropicMessage) {
var systemParts []string
result := make([]anthropicMessage, 0, len(messages))
for _, message := range messages {
content := strings.TrimSpace(message.Content)
if content == "" {
continue
}
role := normalizeRole(message.Role)
if role == "system" {
systemParts = append(systemParts, content)
continue
}
if role != "assistant" {
role = "user"
}
result = append(result, anthropicMessage{
Role: role,
Content: content,
})
}
if len(result) == 0 && len(systemParts) > 0 {
result = append(result, anthropicMessage{
Role: "user",
Content: "Generate one shell command only.",
})
}
return strings.Join(systemParts, "\n\n"), result
}
func toGeminiContents(messages []ChatMessage) []geminiContent {
result := make([]geminiContent, 0, len(messages))
for _, message := range messages {
content := strings.TrimSpace(message.Content)
if content == "" {
continue
}
role := normalizeRole(message.Role)
if role == "assistant" {
role = "model"
} else {
role = "user"
}
result = append(result, geminiContent{
Role: role,
Parts: []geminiContentPart{{
Text: content,
}},
})
}
return result
}
func normalizeRole(role string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "assistant", "model":
return "assistant"
case "system":
return "system"
default:
return "user"
}
}
func parseProviderError(statusCode int, body []byte) error {
var errResp openAIErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil && strings.TrimSpace(errResp.Error.Message) != "" {
if strings.TrimSpace(errResp.Error.Code) != "" {
return fmt.Errorf("provider returned %d: %s (%s)", statusCode, errResp.Error.Message, errResp.Error.Code)
}
return fmt.Errorf("provider returned %d: %s", statusCode, errResp.Error.Message)
}
var generic map[string]interface{}
if err := json.Unmarshal(body, &generic); err == nil {
if message := extractErrorMessage(generic); message != "" {
return fmt.Errorf("provider returned %d: %s", statusCode, message)
}
}
message := strings.TrimSpace(string(body))
if message == "" {
message = http.StatusText(statusCode)
}
if !utf8.ValidString(message) {
message = http.StatusText(statusCode)
}
return fmt.Errorf("provider returned %d: %s", statusCode, message)
}
func extractErrorMessage(value map[string]interface{}) string {
if errorValue, ok := value["error"]; ok {
switch typed := errorValue.(type) {
case string:
return strings.TrimSpace(typed)
case map[string]interface{}:
if msg, ok := typed["message"].(string); ok {
return strings.TrimSpace(msg)
}
if msg, ok := typed["status"].(string); ok {
return strings.TrimSpace(msg)
}
}
}
if msg, ok := value["message"].(string); ok {
return strings.TrimSpace(msg)
}
return ""
}
func firstPositive(values ...int) int {
for _, value := range values {
if value > 0 {
return value
}
}
return 0
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
@@ -0,0 +1,172 @@
package ai
import (
"context"
"fmt"
"strings"
)
type CommandGenerator struct {
client Client
}
type CommandGenerateRequest struct {
Input string
Shell string
WorkingDir string
OS string
RecentCommands []string
DirectoryHints []string
}
type CommandGenerateResponse struct {
Command string
Model string
Provider string
RawText string
Usage ResponseUsage
}
func NewCommandGeneratorFromConfig(cfg GeneratorConfig) (*CommandGenerator, error) {
client, err := NewClient(ClientConfig{
Provider: cfg.Provider,
BaseURL: cfg.BaseURL,
APIKey: cfg.APIKey,
Model: cfg.Model,
APIType: cfg.APIType,
MaxTokens: cfg.MaxTokens,
})
if err != nil {
return nil, err
}
return NewCommandGenerator(client)
}
func NewCommandGenerator(client Client) (*CommandGenerator, error) {
if client == nil {
return nil, fmt.Errorf("client is required")
}
return &CommandGenerator{client: client}, nil
}
func (g *CommandGenerator) Generate(ctx context.Context, req CommandGenerateRequest) (*CommandGenerateResponse, error) {
if strings.TrimSpace(req.Input) == "" {
return nil, fmt.Errorf("input is required")
}
resp, err := g.client.ChatCompletion(ctx, ChatCompletionRequest{
Messages: []ChatMessage{
{Role: "system", Content: buildCommandSystemPrompt()},
{Role: "user", Content: buildCommandUserPrompt(req)},
},
})
if err != nil {
return nil, err
}
command := sanitizeCommand(resp.Content)
if command == "" {
return nil, fmt.Errorf("model returned empty command")
}
return &CommandGenerateResponse{
Command: command,
Model: resp.Model,
Provider: providerNameFromModel(resp.Model),
RawText: resp.RawText,
Usage: resp.Usage,
}, nil
}
func buildCommandSystemPrompt() string {
return strings.Join([]string{
"You are a shell command generator.",
"Return exactly one command suitable for direct execution in the user's shell.",
"Do not include markdown, code fences, explanations, numbering, comments, or backticks.",
"If multiple commands are required, join them with shell operators in a single line.",
"Prefer safe, non-destructive commands unless the user explicitly asks for destructive behavior.",
"Preserve the user's language when filenames or arguments are ambiguous, but output only the command.",
}, "\n")
}
func buildCommandUserPrompt(req CommandGenerateRequest) string {
var sections []string
sections = append(sections, "Task:\n"+strings.TrimSpace(req.Input))
var env []string
if shell := strings.TrimSpace(req.Shell); shell != "" {
env = append(env, "Shell: "+shell)
}
if wd := strings.TrimSpace(req.WorkingDir); wd != "" {
env = append(env, "Working directory: "+wd)
}
if osName := strings.TrimSpace(req.OS); osName != "" {
env = append(env, "Operating system: "+osName)
}
if len(env) > 0 {
sections = append(sections, "Environment:\n"+strings.Join(env, "\n"))
}
if block := formatBulletBlock(req.DirectoryHints); block != "" {
sections = append(sections, "Directory hints:\n"+block)
}
if block := formatBulletBlock(req.RecentCommands); block != "" {
sections = append(sections, "Recent commands:\n"+block)
}
sections = append(sections, "Output requirement:\nReturn one shell command only.")
return strings.Join(sections, "\n\n")
}
func formatBulletBlock(values []string) string {
var lines []string
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
lines = append(lines, "- "+value)
}
return strings.Join(lines, "\n")
}
func sanitizeCommand(raw string) string {
command := strings.TrimSpace(raw)
if command == "" {
return ""
}
command = strings.TrimPrefix(command, "```sh")
command = strings.TrimPrefix(command, "```bash")
command = strings.TrimPrefix(command, "```zsh")
command = strings.TrimPrefix(command, "```shell")
command = strings.TrimPrefix(command, "```")
command = strings.TrimSuffix(command, "```")
command = strings.TrimSpace(command)
lines := strings.Split(command, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(strings.ToLower(line), "command:") {
line = strings.TrimSpace(line[len("command:"):])
}
return strings.Trim(line, "` ")
}
return ""
}
func providerNameFromModel(model string) string {
model = strings.TrimSpace(model)
if model == "" {
return ""
}
if parts := strings.SplitN(model, "/", 2); len(parts) == 2 {
return parts[0]
}
return ""
}
+200
View File
@@ -0,0 +1,200 @@
package ai
import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
var agentAccountRepo = repo.NewIAgentAccountRepo()
var agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
var terminalRuntimeVersion atomic.Uint64
type TerminalRuntimeSettings struct {
AccountID uint
Prefix string
RiskCommands []string
}
func CurrentTerminalRuntimeVersion() uint64 {
return terminalRuntimeVersion.Load()
}
func InvalidateTerminalRuntimeCache() {
terminalRuntimeVersion.Add(1)
}
func ResolveGeneratorConfig(accountID uint) (GeneratorConfig, time.Duration, error) {
account, err := loadAgentAccount(accountID)
if err != nil {
return GeneratorConfig{}, 0, err
}
provider := strings.ToLower(strings.TrimSpace(account.Provider))
if provider == "" {
return GeneratorConfig{}, 0, fmt.Errorf("agent account provider is required")
}
model, maxTokens, err := resolveAccountModelConfig(account.ID, provider)
if err != nil {
return GeneratorConfig{}, 0, err
}
baseURL := strings.TrimSpace(account.BaseURL)
if baseURL == "" {
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
baseURL = defaultURL
}
}
apiKey := strings.TrimSpace(account.APIKey)
if apiKey == "" {
apiKey = lookupProviderAPIKey(provider)
}
if apiKey == "" && provider != "ollama" {
return GeneratorConfig{}, 0, fmt.Errorf("agent account api key is required")
}
return GeneratorConfig{
Provider: provider,
BaseURL: baseURL,
APIKey: strings.TrimSpace(apiKey),
Model: model,
APIType: strings.TrimSpace(account.APIType),
MaxTokens: maxTokens,
}, 30 * time.Second, nil
}
func lookupProviderAPIKey(provider string) string {
envKey := providercatalog.EnvKey(provider)
if envKey == "" {
return ""
}
return strings.TrimSpace(os.Getenv(envKey))
}
func defaultModelForProvider(provider string) (string, int) {
meta, ok := providercatalog.Get(provider)
if !ok || len(meta.Models) == 0 {
return "", 0
}
return meta.Models[0].ID, meta.Models[0].MaxTokens
}
func resolveAccountModelConfig(accountID uint, provider string) (string, int, error) {
if accountID > 0 {
rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
if err != nil {
return "", 0, err
}
if len(rows) > 0 {
return strings.TrimSpace(rows[0].Model), rows[0].MaxTokens, nil
}
}
model, maxTokens := defaultModelForProvider(provider)
return model, maxTokens, nil
}
func ResolveGeneratorConfigFromAgentSettings() (GeneratorConfig, uint, time.Duration, error) {
status, err := loadAgentSettingValue("AIStatus")
if err != nil && !os.IsNotExist(err) {
return GeneratorConfig{}, 0, 0, err
}
if !strings.EqualFold(strings.TrimSpace(status), "Enable") {
return GeneratorConfig{}, 0, 0, os.ErrNotExist
}
accountValue, err := loadAgentSettingValue("AIAccountID")
if err != nil {
return GeneratorConfig{}, 0, 0, err
}
accountID, err := strconv.ParseUint(strings.TrimSpace(accountValue), 10, 64)
if err != nil || accountID == 0 {
return GeneratorConfig{}, 0, 0, os.ErrNotExist
}
config, timeout, err := ResolveGeneratorConfig(uint(accountID))
return config, uint(accountID), timeout, err
}
func LoadTerminalRuntimeSettings() (TerminalRuntimeSettings, GeneratorConfig, time.Duration, error) {
config, accountID, timeout, err := ResolveGeneratorConfigFromAgentSettings()
if err != nil {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
prefix, err := loadAgentSettingValue("AIPrefix")
if err != nil && !os.IsNotExist(err) {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
riskCommands, err := loadRiskCommands()
if err != nil {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
return TerminalRuntimeSettings{
AccountID: accountID,
Prefix: strings.TrimSpace(prefix),
RiskCommands: riskCommands,
}, config, timeout, nil
}
func loadAgentAccount(accountID uint) (*model.AgentAccount, error) {
if accountID == 0 {
return nil, os.ErrNotExist
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID))
if err != nil {
return nil, err
}
return account, nil
}
func loadAgentSettingValue(key string) (string, error) {
var setting model.Setting
if err := global.DB.Where("key = ?", key).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", os.ErrNotExist
}
return "", err
}
return setting.Value, nil
}
func loadRiskCommands() ([]string, error) {
value, err := loadAgentSettingValue("AIRiskCommands")
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
if strings.TrimSpace(value) == "" {
return []string{}, nil
}
var commands []string
if err := json.Unmarshal([]byte(value), &commands); err != nil {
return nil, err
}
return normalizeRiskCommands(commands), nil
}
func normalizeRiskCommands(commands []string) []string {
seen := make(map[string]struct{}, len(commands))
result := make([]string, 0, len(commands))
for _, command := range commands {
command = strings.TrimSpace(command)
if command == "" {
continue
}
if _, ok := seen[command]; ok {
continue
}
seen[command] = struct{}{}
result = append(result, command)
}
return result
}
+38
View File
@@ -0,0 +1,38 @@
package ai
import (
"context"
"fmt"
"runtime"
"time"
)
const validateTimeout = 15 * time.Second
func ValidateTerminalAccount(accountID uint) error {
cfg, _, err := ResolveGeneratorConfig(accountID)
if err != nil {
return err
}
generator, err := NewCommandGeneratorFromConfig(cfg)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), validateTimeout)
defer cancel()
resp, err := generator.Generate(ctx, CommandGenerateRequest{
Input: "Print current directory.",
Shell: "bash",
OS: runtime.GOOS,
})
if err != nil {
return err
}
if resp == nil || resp.Command == "" {
return fmt.Errorf("terminal ai account returned empty response")
}
return nil
}
+273
View File
@@ -0,0 +1,273 @@
package terminal
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
)
const lineClearControl = 21
type aiInputInterceptor struct {
config terminalai.GeneratorConfig
timeout time.Duration
shell string
lang string
prefix string
version uint64
mu sync.Mutex
currentLine string
recentCommands []string
riskCommands []string
}
func newAIInputInterceptor(shell string, lang string) *aiInputInterceptor {
settings, config, timeout, err := terminalai.LoadTerminalRuntimeSettings()
if err != nil {
if !os.IsNotExist(err) {
global.LOG.Warnf("load terminal ai config failed: %v", err)
}
return nil
}
if strings.TrimSpace(config.APIKey) == "" && !strings.EqualFold(strings.TrimSpace(config.Provider), "ollama") {
return nil
}
return &aiInputInterceptor{
config: config,
timeout: timeout,
shell: strings.TrimSpace(shell),
lang: strings.TrimSpace(lang),
prefix: settings.Prefix,
riskCommands: append([]string(nil), settings.RiskCommands...),
version: terminalai.CurrentTerminalRuntimeVersion(),
}
}
func (i *aiInputInterceptor) refreshSettings() error {
if !i.needsRefresh() {
return nil
}
settings, config, timeout, err := terminalai.LoadTerminalRuntimeSettings()
if err != nil {
return err
}
i.mu.Lock()
defer i.mu.Unlock()
i.config = config
i.timeout = timeout
i.prefix = settings.Prefix
i.riskCommands = append([]string(nil), settings.RiskCommands...)
i.version = terminalai.CurrentTerminalRuntimeVersion()
return nil
}
func (i *aiInputInterceptor) SetCurrentLine(line string) {
if i == nil {
return
}
i.mu.Lock()
defer i.mu.Unlock()
i.currentLine = normalizeCurrentLine(line, i.prefix)
}
func (i *aiInputInterceptor) needsRefresh() bool {
if i == nil {
return false
}
currentVersion := terminalai.CurrentTerminalRuntimeVersion()
i.mu.Lock()
defer i.mu.Unlock()
return i.version != currentVersion
}
func (i *aiInputInterceptor) HandleEnter(onStart func(), onDone func(string), onError func(string)) (string, bool) {
if i == nil {
return "", false
}
if err := i.refreshSettings(); err != nil {
if !os.IsNotExist(err) {
global.LOG.Warnf("refresh terminal ai config failed: %v", err)
}
return "", false
}
i.mu.Lock()
currentLine := i.currentLine
i.currentLine = ""
recentCommands := append([]string(nil), i.recentCommands...)
i.mu.Unlock()
if !matchesAIPrefix(currentLine, i.prefix) {
if currentLine != "" {
i.pushRecentCommand(currentLine)
}
return "", false
}
prompt := strings.TrimSpace(strings.TrimPrefix(currentLine, i.prefix))
if prompt == "" {
return "", true
}
if onStart != nil {
onStart()
}
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), i.timeout)
defer cancel()
generator, err := terminalai.NewCommandGeneratorFromConfig(i.config)
if err != nil {
global.LOG.Errorf("create terminal ai generator failed: %v", err)
if onError != nil {
onError(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIRequestFailed", map[string]interface{}{
"err": err.Error(),
}))
}
return "", true
}
resp, err := generator.Generate(ctx, terminalai.CommandGenerateRequest{
Input: prompt,
Shell: firstNonEmpty(i.shell, filepath.Base(strings.TrimSpace(os.Getenv("SHELL")))),
OS: runtime.GOOS,
RecentCommands: recentCommands,
})
if err != nil {
global.LOG.Errorf("generate terminal ai command failed: %v", err)
if onError != nil {
onError(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIRequestFailed", map[string]interface{}{
"err": err.Error(),
}))
}
return "", true
}
if onDone != nil {
onDone(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIReadyToExecute", map[string]interface{}{
"duration": formatAIDuration(time.Since(start)),
"tokens": resolveTotalTokens(resp.Usage),
}))
}
if i.isRiskCommand(resp.Command) {
return "# " + i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIBlockedRiskyCommand", map[string]interface{}{
"command": resp.Command,
}), true
}
return resp.Command, strings.TrimSpace(resp.Command) != ""
}
func resolveTotalTokens(usage terminalai.ResponseUsage) int {
if usage.TotalTokens > 0 {
return usage.TotalTokens
}
return usage.PromptTokens + usage.CompletionTokens
}
func formatAIDuration(duration time.Duration) string {
if duration < time.Millisecond {
return duration.String()
}
return duration.Round(time.Millisecond).String()
}
func (i *aiInputInterceptor) pushRecentCommand(command string) {
if i == nil {
return
}
command = strings.TrimSpace(command)
if command == "" || strings.HasPrefix(command, i.prefix) {
return
}
i.mu.Lock()
defer i.mu.Unlock()
i.recentCommands = append([]string{command}, i.recentCommands...)
if len(i.recentCommands) > 8 {
i.recentCommands = i.recentCommands[:8]
}
}
func isEnterInput(data []byte) bool {
if len(data) == 1 && (data[0] == '\r' || data[0] == '\n') {
return true
}
return len(data) == 2 && data[0] == '\r' && data[1] == '\n'
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func sanitizeInputLine(raw string) string {
return strings.TrimSpace(raw)
}
func matchesAIPrefix(line, prefix string) bool {
line = strings.TrimSpace(line)
prefix = strings.TrimSpace(prefix)
if line == "" || prefix == "" {
return false
}
return line == prefix || strings.HasPrefix(line, prefix+" ")
}
func normalizeCurrentLine(raw, prefix string) string {
line := sanitizeInputLine(raw)
prefix = strings.TrimSpace(prefix)
if line == "" || prefix == "" {
return line
}
if matchesAIPrefix(line, prefix) {
return line
}
prefixIdx := strings.Index(line, prefix)
if prefixIdx < 0 {
return line
}
for _, marker := range []string{"# ", "$ ", "% ", "> "} {
if idx := strings.LastIndex(line[:prefixIdx], marker); idx >= 0 {
promptPart := strings.TrimSpace(line[:idx+len(marker)-1])
if looksLikePromptPrefix(promptPart) {
return strings.TrimSpace(line[idx+len(marker):])
}
}
}
return line
}
func looksLikePromptPrefix(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return false
}
if strings.ContainsAny(value, "'\"`") {
return false
}
return true
}
func (i *aiInputInterceptor) isRiskCommand(command string) bool {
command = strings.ToLower(strings.TrimSpace(command))
if command == "" {
return false
}
for _, riskCommand := range i.riskCommands {
riskCommand = strings.ToLower(strings.TrimSpace(riskCommand))
if riskCommand == "" {
continue
}
if strings.Contains(command, riskCommand) {
return true
}
}
return false
}
+50 -3
View File
@@ -3,9 +3,11 @@ package terminal
import (
"encoding/base64"
"encoding/json"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
@@ -14,20 +16,25 @@ type LocalWsSession struct {
slave *LocalCommand
wsConn *websocket.Conn
allowCtrlC bool
writeMutex sync.Mutex
allowCtrlC bool
writeMutex sync.Mutex
lang string
aiInterceptor *aiInputInterceptor
}
func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) {
if err := slave.ResizeTerminal(cols, rows); err != nil {
global.LOG.Errorf("ssh pty change windows size failed, err: %v", err)
}
lang := i18n.GetLanguageFromDB()
return &LocalWsSession{
slave: slave,
wsConn: wsConn,
allowCtrlC: allowCtrlC,
allowCtrlC: allowCtrlC,
lang: lang,
aiInterceptor: newAIInputInterceptor("", lang),
}, nil
}
@@ -108,6 +115,19 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
if isEnterInput(decodeBytes) {
if sws.aiInterceptor != nil {
sws.aiInterceptor.SetCurrentLine(msgObj.Line)
}
if generated, handled := sws.aiInterceptor.HandleEnter(sws.notifyAIThinking, sws.notifyAIDone, sws.notifyAIError); handled {
payload := []byte{lineClearControl}
if strings.TrimSpace(generated) != "" {
payload = append(payload, []byte(generated)...)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(payload)
continue
}
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case WsMsgHeartbeat:
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
@@ -119,6 +139,33 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
}
}
func (sws *LocalWsSession) notifyAIThinking() {
if sws == nil {
return
}
if err := sws.masterWrite([]byte("\r\n" + i18n.GetMsgByKeyAndLang(sws.lang, "TerminalAIThinking") + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai thinking message failed, err: %v", err)
}
}
func (sws *LocalWsSession) notifyAIDone(message string) {
if sws == nil || strings.TrimSpace(message) == "" {
return
}
if err := sws.masterWrite([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai done message failed, err: %v", err)
}
}
func (sws *LocalWsSession) notifyAIError(message string) {
if sws == nil || strings.TrimSpace(message) == "" {
return
}
if err := sws.masterWrite([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai error message failed, err: %v", err)
}
}
func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.slave.Write(cmdBytes); err != nil {
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
+62 -17
View File
@@ -5,10 +5,12 @@ import (
"encoding/base64"
"encoding/json"
"io"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
@@ -43,20 +45,22 @@ const (
type WsMsg struct {
Type string `json:"type"`
Data string `json:"data,omitempty"` // WsMsgCmd
Line string `json:"line,omitempty"` // WsMsgCmd
Cols int `json:"cols,omitempty"` // WsMsgResize
Rows int `json:"rows,omitempty"` // WsMsgResize
Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat
}
type LogicSshWsSession struct {
stdinPipe io.WriteCloser
comboOutput *safeBuffer
logBuff *safeBuffer
inputFilterBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
isAdmin bool
IsFlagged bool
stdinPipe io.WriteCloser
comboOutput *safeBuffer
logBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
lang string
isAdmin bool
IsFlagged bool
aiInterceptor *aiInputInterceptor
}
func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websocket.Conn, initCmd string) (*LogicSshWsSession, error) {
@@ -72,7 +76,6 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
comboWriter := new(safeBuffer)
logBuf := new(safeBuffer)
inputBuf := new(safeBuffer)
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
@@ -91,15 +94,17 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
time.Sleep(100 * time.Millisecond)
_, _ = stdinP.Write([]byte(initCmd + "\n"))
}
lang := i18n.GetLanguageFromDB()
return &LogicSshWsSession{
stdinPipe: stdinP,
comboOutput: comboWriter,
logBuff: logBuf,
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
isAdmin: true,
IsFlagged: false,
stdinPipe: stdinP,
comboOutput: comboWriter,
logBuff: logBuf,
session: sshSession,
wsConn: wsConn,
lang: lang,
isAdmin: true,
IsFlagged: false,
aiInterceptor: newAIInputInterceptor("", lang),
}, nil
}
@@ -151,6 +156,19 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
if isEnterInput(decodeBytes) {
if sws.aiInterceptor != nil {
sws.aiInterceptor.SetCurrentLine(msgObj.Line)
}
if generated, handled := sws.aiInterceptor.HandleEnter(sws.notifyAIThinking, sws.notifyAIDone, sws.notifyAIError); handled {
payload := []byte{lineClearControl}
if strings.TrimSpace(generated) != "" {
payload = append(payload, []byte(generated)...)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(payload)
continue
}
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case WsMsgHeartbeat:
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
@@ -162,6 +180,33 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
}
}
func (sws *LogicSshWsSession) notifyAIThinking() {
if sws == nil || sws.comboOutput == nil {
return
}
if _, err := sws.comboOutput.Write([]byte("\r\n" + i18n.GetMsgByKeyAndLang(sws.lang, "TerminalAIThinking") + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai thinking message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) notifyAIDone(message string) {
if sws == nil || sws.comboOutput == nil || strings.TrimSpace(message) == "" {
return
}
if _, err := sws.comboOutput.Write([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai done message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) notifyAIError(message string) {
if sws == nil || sws.comboOutput == nil || strings.TrimSpace(message) == "" {
return
}
if _, err := sws.comboOutput.Write([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai error message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
+1 -1
View File
@@ -144,7 +144,7 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
for _, k := range progress.Keys {
value := global.CACHE.Get(k)
if value == "" {
return nil, fmt.Errorf("get cache error,err value is nil")
continue
}
downloadProcess := &files.Process{}
_ = json.Unmarshal([]byte(value), downloadProcess)
+4
View File
@@ -39,6 +39,10 @@ func IsUseCustomApp() bool {
return false
}
func IsXpack() bool {
return false
}
func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
return nil
}
+1 -1
View File
@@ -94,7 +94,7 @@ func (b *BaseApi) MFALogin(c *gin.Context) {
user, msgKey, err := authService.MFALogin(c, req, string(entrance))
go saveLoginLogs(c, wrapLoginErr(msgKey, err))
if msgKey == "ErrAuth" {
if msgKey == "ErrAuth" || msgKey == "ErrMFA" {
helper.BadAuth(c, msgKey, err)
return
}
-1
View File
@@ -9,7 +9,6 @@ type ApiGroup struct {
var ApiGroupApp = new(ApiGroup)
var (
hostService = service.NewIHostService()
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
-355
View File
@@ -1,355 +0,0 @@
package v2
import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/copier"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/ssh"
"github.com/1Panel-dev/1Panel/core/utils/terminal"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
// @Tags Host
// @Summary Create host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"}
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
host, err := hostService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, host)
}
// @Tags Host
// @Summary Test host conn by info
// @Accept json
// @Param request body dto.HostConnTest true "request"
// @Success 200 {boolean} status
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/test/byinfo [post]
func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
connStatus := hostService.TestByInfo(req)
helper.SuccessWithData(c, connStatus)
}
// @Tags Host
// @Summary Test host conn by host id
// @Accept json
// @Param id path integer true "request"
// @Success 200 {boolean} connStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/test/byid/:id [post]
func (b *BaseApi) TestByID(c *gin.Context) {
idParam, ok := c.Params.Get("id")
if !ok {
helper.BadRequest(c, errors.New("no such params find in request"))
return
}
intNum, err := strconv.Atoi(idParam)
if err != nil {
helper.BadRequest(c, err)
return
}
connStatus := hostService.TestLocalConn(uint(intNum))
helper.SuccessWithData(c, connStatus)
}
// @Tags Host
// @Summary Load host tree
// @Accept json
// @Param request body dto.SearchForTree true "request"
// @Success 200 {array} dto.HostTree
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/tree [post]
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := hostService.SearchForTree(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Host
// @Summary Page host
// @Accept json
// @Param request body dto.SearchPageWithGroup true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/search [post]
func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := hostService.SearchWithPage(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Host
// @Summary Delete host
// @Accept json
// @Param request body dto.OperateByIDs true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"addr","output_value":"addrs"}],"formatZH":"删除主机 [addrs]","formatEN":"delete host [addrs]"}
func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := hostService.Delete(req.IDs); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Host
// @Summary Update host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/update [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机信息 [name][addr]","formatEN":"update host [name][addr]"}
func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var err error
if len(req.Password) != 0 && req.AuthMode == "password" {
req.Password, err = hostService.EncryptHost(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.PrivateKey = ""
req.PassPhrase = ""
}
if len(req.PrivateKey) != 0 && req.AuthMode == "key" {
req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey)
if err != nil {
helper.BadRequest(c, err)
return
}
if len(req.PassPhrase) != 0 {
req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase)
if err != nil {
helper.BadRequest(c, err)
return
}
}
req.Password = ""
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["group_id"] = req.GroupID
upMap["addr"] = req.Addr
upMap["port"] = req.Port
upMap["user"] = req.User
upMap["auth_mode"] = req.AuthMode
upMap["remember_password"] = req.RememberPassword
if req.AuthMode == "password" {
upMap["password"] = req.Password
upMap["private_key"] = ""
upMap["pass_phrase"] = ""
} else {
upMap["password"] = ""
upMap["private_key"] = req.PrivateKey
upMap["pass_phrase"] = req.PassPhrase
}
upMap["description"] = req.Description
hostItem, err := hostService.Update(req.ID, upMap)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, hostItem)
}
// @Tags Host
// @Summary Update host group
// @Accept json
// @Param request body dto.ChangeHostGroup true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/update/group [post]
// @x-panel-log {"bodyKeys":["id","group"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"addr","output_value":"addr"}],"formatZH":"切换主机[addr]分组 => [group]","formatEN":"change host [addr] group => [group]"}
func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeHostGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
upMap := make(map[string]interface{})
upMap["group_id"] = req.GroupID
if _, err := hostService.Update(req.ID, upMap); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Host
// @Summary Get host info
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/info [post]
func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
info, err := hostService.GetHostByID(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, info)
}
func (b *BaseApi) WsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
id, err := strconv.Atoi(c.Query("id"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) {
return
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
host, err := service.GetHostInfo(uint(id))
if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) {
return
}
var connInfo ssh.ConnInfo
_ = copier.Copy(&connInfo, &host)
connInfo.PrivateKey = []byte(host.PrivateKey)
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
client, err := ssh.NewClient(connInfo)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
defer client.Close()
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
if wshandleError(wsConn, err) {
return
}
defer sws.Close()
quitChan := make(chan bool, 3)
sws.Start(quitChan)
go sws.Wait(quitChan)
<-quitChan
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
var upGrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 16384,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wshandleError(ws *websocket.Conn, err error) bool {
if err != nil {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
wsData, err := json.Marshal(terminal.WsMsg{
Type: terminal.WsMsgCmd,
Data: base64.StdEncoding.EncodeToString([]byte(err.Error())),
})
if err != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}"))
} else {
_ = ws.WriteMessage(websocket.TextMessage, wsData)
}
}
return true
}
return false
}
+30
View File
@@ -1,8 +1,10 @@
package v2
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
@@ -228,3 +230,31 @@ func (b *BaseApi) RunScript(c *gin.Context) {
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
var upGrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 16384,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wshandleError(ws *websocket.Conn, err error) bool {
if err != nil {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
wsData, marshalErr := json.Marshal(terminal.WsMsg{
Type: terminal.WsMsgCmd,
Data: base64.StdEncoding.EncodeToString([]byte(err.Error())),
})
if marshalErr != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}"))
} else {
_ = ws.WriteMessage(websocket.TextMessage, wsData)
}
}
return true
}
return false
}
+1
View File
@@ -19,6 +19,7 @@ type SettingInfo struct {
Theme string `json:"theme"`
MenuTabs string `json:"menuTabs"`
Language string `json:"language"`
DocSource string `json:"docSource"`
ServerPort string `json:"serverPort"`
SSL string `json:"ssl"`
-100
View File
@@ -1,100 +0,0 @@
package repo
import (
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/global"
"gorm.io/gorm"
)
type HostRepo struct{}
type IHostRepo interface {
Get(opts ...global.DBOption) (model.Host, error)
GetList(opts ...global.DBOption) ([]model.Host, error)
Page(limit, offset int, opts ...global.DBOption) (int64, []model.Host, error)
Create(host *model.Host) error
Update(id uint, vars map[string]interface{}) error
UpdateGroup(group, newGroup uint) error
Delete(opts ...global.DBOption) error
WithByInfo(info string) global.DBOption
WithByPort(port uint) global.DBOption
WithByUser(user string) global.DBOption
}
func NewIHostRepo() IHostRepo {
return &HostRepo{}
}
func (h *HostRepo) Get(opts ...global.DBOption) (model.Host, error) {
var host model.Host
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&host).Error
return host, err
}
func (h *HostRepo) GetList(opts ...global.DBOption) ([]model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&hosts).Error
return hosts, err
}
func (h *HostRepo) Page(page, size int, opts ...global.DBOption) (int64, []model.Host, error) {
var users []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (h *HostRepo) WithByInfo(info string) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}
}
func (h *HostRepo) WithByPort(port uint) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("port = ?", port)
}
}
func (h *HostRepo) WithByUser(user string) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("user = ?", user)
}
}
func (h *HostRepo) Create(host *model.Host) error {
return global.DB.Create(host).Error
}
func (h *HostRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error
}
func (h *HostRepo) UpdateGroup(group, newGroup uint) error {
return global.DB.Model(&model.Host{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error
}
func (h *HostRepo) Delete(opts ...global.DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Host{}).Error
}

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