mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
114
Commits
v2.1.3
...
release-2.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d78ca5a86 | ||
|
|
a9b65b7ce5 | ||
|
|
291b40a45d | ||
|
|
a959da7d44 | ||
|
|
26c4e8323f | ||
|
|
0534badce7 | ||
|
|
594d818db0 | ||
|
|
04dceb93a7 | ||
|
|
7b554c0d61 | ||
|
|
bb642cd8a8 | ||
|
|
40249b527d | ||
|
|
74ff7ddc19 | ||
|
|
096ec8edc7 | ||
|
|
0d19069059 | ||
|
|
0da833302b | ||
|
|
4a69cf0ff5 | ||
|
|
39704ef4e5 | ||
|
|
e52cfeeb78 | ||
|
|
476429005c | ||
|
|
6fb41dc711 | ||
|
|
2cb2058cfa | ||
|
|
8533dd1d33 | ||
|
|
0cfbcd25fc | ||
|
|
b91ca8bdb3 | ||
|
|
72764863bc | ||
|
|
a7b9fd0181 | ||
|
|
2b3164fe82 | ||
|
|
2c6307049a | ||
|
|
acc1b9270f | ||
|
|
60e4143525 | ||
|
|
99bcd6d059 | ||
|
|
c4259dd004 | ||
|
|
1c5a4bc978 | ||
|
|
42f6e9a9a6 | ||
|
|
569363530a | ||
|
|
ec9eba0b9a | ||
|
|
5376d54089 | ||
|
|
c10e5747b0 | ||
|
|
5a80931a49 | ||
|
|
f541379daa | ||
|
|
0018335d64 | ||
|
|
df90286253 | ||
|
|
8dfa7945d7 | ||
|
|
1f4292d9cf | ||
|
|
e635a7beba | ||
|
|
59c657e403 | ||
|
|
ff8f79ed52 | ||
|
|
57a7edf481 | ||
|
|
bbd33c66f1 | ||
|
|
b05ed1a0ca | ||
|
|
b645c4edb4 | ||
|
|
9f61aac8be | ||
|
|
11d78644ee | ||
|
|
b96428e1d6 | ||
|
|
6432c35b1a | ||
|
|
fa843f0940 | ||
|
|
5c7ffc561b | ||
|
|
c2f0c8c6ac | ||
|
|
5bf6d17451 | ||
|
|
7f799004c3 | ||
|
|
c68251cada | ||
|
|
23e7d03d8d | ||
|
|
5085db7a2f | ||
|
|
a9cf7a7d1d | ||
|
|
cf18995182 | ||
|
|
24d7dc0786 | ||
|
|
142c9b9a71 | ||
|
|
f3c7603f6e | ||
|
|
db26da1700 | ||
|
|
d5ff59f4e7 | ||
|
|
85b88db99e | ||
|
|
f7053934b5 | ||
|
|
bf1339bb40 | ||
|
|
43c5e62c23 | ||
|
|
28634ce631 | ||
|
|
72263dfdda | ||
|
|
479d36e25f | ||
|
|
2f3ad3537e | ||
|
|
2dfe5096f0 | ||
|
|
4375722aef | ||
|
|
bde4d06842 | ||
|
|
17a486b2c4 | ||
|
|
8499c09571 | ||
|
|
7cb70f785a | ||
|
|
f3781320de | ||
|
|
869ee75020 | ||
|
|
1d5a26373d | ||
|
|
176fd0f8ad | ||
|
|
341a55ae63 | ||
|
|
fb87cf545f | ||
|
|
6db2a9352e | ||
|
|
21826bcc1a | ||
|
|
eedbc2e95c | ||
|
|
46763df7bb | ||
|
|
175162753e | ||
|
|
d114933fe9 | ||
|
|
432412e596 | ||
|
|
1b577e3d41 | ||
|
|
eda9641d35 | ||
|
|
6808e4892c | ||
|
|
0d71cd4db2 | ||
|
|
b6b3304a71 | ||
|
|
59a0a573ed | ||
|
|
e82d686940 | ||
|
|
b48bb88aa1 | ||
|
|
5ce9ec5b64 | ||
|
|
740f3549ad | ||
|
|
aa8a9e8b1e | ||
|
|
14218e3495 | ||
|
|
c526eb2962 | ||
|
|
1ff8292be4 | ||
|
|
d4394616af | ||
|
|
462ca6b373 | ||
|
|
657c3ad37a |
+3
-1
@@ -73,4 +73,6 @@ agent/.golangci.yml
|
||||
openspec
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
opencode.json
|
||||
opencode.json
|
||||
superpowers
|
||||
.worktrees/
|
||||
|
||||
+413
-24
@@ -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
|
||||
}
|
||||
@@ -354,19 +456,19 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Browser config
|
||||
// @Summary Get Agent QQ Bot channel 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.AgentWecomConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/browser/get [post]
|
||||
func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
var req dto.AgentBrowserConfigReq
|
||||
// @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.GetBrowserConfig(req)
|
||||
data, err := agentService.GetWecomConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
@@ -375,19 +477,183 @@ func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent Browser config
|
||||
// @Summary Update Agent WeCom channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentBrowserConfigUpdateReq true "request"
|
||||
// @Param request body dto.AgentWecomConfigUpdateReq 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/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.UpdateBrowserConfig(req); err != nil {
|
||||
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.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetQQBotConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentQQBotConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/qqbot/update [post]
|
||||
func (b *BaseApi) UpdateAgentQQBotConfig(c *gin.Context) {
|
||||
var req dto.AgentQQBotConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateQQBotConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Install Agent plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginInstallReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/install [post]
|
||||
func (b *BaseApi) InstallAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginInstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.InstallPlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Check Agent plugin installation status
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginCheckReq true "request"
|
||||
// @Success 200 {object} dto.AgentPluginStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/check [post]
|
||||
func (b *BaseApi) CheckAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginCheckReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.CheckPlugin(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {object} dto.AgentSecurityConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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.GetSecurityConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentSecurityConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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.UpdateSecurityConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -397,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
|
||||
}
|
||||
@@ -436,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,7 @@ var (
|
||||
|
||||
recycleBinService = service.NewIRecycleBinService()
|
||||
favoriteService = service.NewIFavoriteService()
|
||||
hostService = service.NewIHostService()
|
||||
|
||||
websiteCAService = service.NewIWebsiteCAService()
|
||||
taskService = service.NewITaskService()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
+232
-82
@@ -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,80 @@ 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 AgentDiscordConfigReq 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 {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId" validate:"required"`
|
||||
ClientSecret string `json:"clientSecret" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentPluginInstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
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 wecom dingtalk weixin"`
|
||||
}
|
||||
|
||||
type AgentPluginStatus struct {
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentDiscordConfigUpdateReq struct {
|
||||
@@ -219,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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+337
-157
@@ -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,190 +26,295 @@ type Meta struct {
|
||||
Sort uint
|
||||
DefaultBaseURL string
|
||||
EnvKey string
|
||||
Default RuntimeDefault
|
||||
Models []Model
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
var catalog = map[string]Meta{
|
||||
"custom": {
|
||||
Key: "custom",
|
||||
DisplayName: "Custom",
|
||||
Sort: 10,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "CUSTOM_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 128000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{},
|
||||
},
|
||||
"ollama": {
|
||||
Key: "ollama",
|
||||
DisplayName: "Ollama",
|
||||
Sort: 10,
|
||||
Enabled: true,
|
||||
Sort: 15,
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-responses",
|
||||
ContextWindow: 160000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
},
|
||||
"vllm": {
|
||||
Key: "vllm",
|
||||
DisplayName: "vLLM",
|
||||
Sort: 20,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "VLLM_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 128000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{},
|
||||
},
|
||||
"deepseek": {
|
||||
Key: "deepseek",
|
||||
DisplayName: "DeepSeek",
|
||||
Sort: 20,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
"custom": {
|
||||
Key: "custom",
|
||||
DisplayName: "Custom",
|
||||
Sort: 30,
|
||||
DefaultBaseURL: "",
|
||||
EnvKey: "CUSTOM_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{},
|
||||
},
|
||||
"openai": {
|
||||
Key: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
Sort: 40,
|
||||
DefaultBaseURL: "https://api.openai.com/v1",
|
||||
EnvKey: "OPENAI_API_KEY",
|
||||
Enabled: true,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
Key: "anthropic",
|
||||
DisplayName: "Anthropic",
|
||||
Sort: 50,
|
||||
DefaultBaseURL: "https://api.anthropic.com",
|
||||
EnvKey: "ANTHROPIC_API_KEY",
|
||||
Enabled: true,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
"gemini": {
|
||||
Key: "gemini",
|
||||
DisplayName: "Gemini",
|
||||
Sort: 60,
|
||||
DefaultBaseURL: "https://generativelanguage.googleapis.com",
|
||||
EnvKey: "GEMINI_API_KEY",
|
||||
Enabled: true,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
"minimax": {
|
||||
Key: "minimax",
|
||||
DisplayName: "MiniMax (CN)",
|
||||
Sort: 70,
|
||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
||||
EnvKey: "MINIMAX_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "minimax/MiniMax-M2.1", Name: "MiniMax M2.1"},
|
||||
{ID: "minimax/MiniMax-M2.1-lightning", Name: "MiniMax M2.1 Lightning"},
|
||||
},
|
||||
},
|
||||
"moonshot": {
|
||||
Key: "moonshot",
|
||||
DisplayName: "Moonshot (Global)",
|
||||
Sort: 80,
|
||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
||||
EnvKey: "MOONSHOT_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"kimi": {
|
||||
Key: "kimi",
|
||||
DisplayName: "Kimi (CN)",
|
||||
Sort: 90,
|
||||
DefaultBaseURL: "https://api.moonshot.cn/v1",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||
},
|
||||
},
|
||||
"zai": {
|
||||
Key: "zai",
|
||||
DisplayName: "Z.ai",
|
||||
Sort: 95,
|
||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
EnvKey: "ZAI_API_KEY",
|
||||
Enabled: true,
|
||||
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"},
|
||||
},
|
||||
},
|
||||
"kimi-coding": {
|
||||
Key: "kimi-coding",
|
||||
DisplayName: "Kimi Coding",
|
||||
Sort: 100,
|
||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Enabled: true,
|
||||
Models: []Model{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
|
||||
{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": {
|
||||
Key: "bailian-coding-plan",
|
||||
DisplayName: "阿里云百炼 Coding Plan",
|
||||
Sort: 11,
|
||||
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": {
|
||||
Key: "ark-coding-plan",
|
||||
DisplayName: "方舟 Coding Plan",
|
||||
Sort: 12,
|
||||
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},
|
||||
},
|
||||
},
|
||||
"qwen": {
|
||||
Key: "qwen",
|
||||
DisplayName: "Qwen",
|
||||
Sort: 110,
|
||||
DefaultBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
EnvKey: "QWEN_API_KEY",
|
||||
Enabled: false,
|
||||
"zai": {
|
||||
Key: "zai",
|
||||
DisplayName: "Z.ai",
|
||||
Sort: 40,
|
||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
EnvKey: "ZAI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 204800,
|
||||
MaxTokens: 131072,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
|
||||
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
|
||||
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
|
||||
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
|
||||
},
|
||||
},
|
||||
"minimax": {
|
||||
Key: "minimax",
|
||||
DisplayName: "MiniMax (CN)",
|
||||
Sort: 45,
|
||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
||||
EnvKey: "MINIMAX_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "anthropic-messages",
|
||||
ContextWindow: 200000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
|
||||
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
|
||||
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
|
||||
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
|
||||
},
|
||||
},
|
||||
"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": {
|
||||
Key: "kimi",
|
||||
DisplayName: "Kimi (CN)",
|
||||
Sort: 50,
|
||||
DefaultBaseURL: "https://api.moonshot.cn/v1",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
|
||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
|
||||
},
|
||||
},
|
||||
"kimi-coding": {
|
||||
Key: "kimi-coding",
|
||||
DisplayName: "Kimi Coding",
|
||||
Sort: 51,
|
||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 262144,
|
||||
MaxTokens: 32768,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", Reasoning: true},
|
||||
},
|
||||
},
|
||||
"openai": {
|
||||
Key: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
Sort: 55,
|
||||
DefaultBaseURL: "https://api.openai.com/v1",
|
||||
EnvKey: "OPENAI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{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": {
|
||||
Key: "openrouter",
|
||||
DisplayName: "OpenRouter",
|
||||
Sort: 56,
|
||||
DefaultBaseURL: "https://openrouter.ai/api/v1",
|
||||
EnvKey: "OPENROUTER_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 128000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "openrouter/free", Name: "openrouter/free"},
|
||||
{ID: "openrouter/auto", Name: "openrouter/auto"},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
Key: "anthropic",
|
||||
DisplayName: "Anthropic",
|
||||
Sort: 60,
|
||||
DefaultBaseURL: "https://api.anthropic.com",
|
||||
EnvKey: "ANTHROPIC_API_KEY",
|
||||
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", Reasoning: true},
|
||||
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1", Reasoning: true},
|
||||
},
|
||||
},
|
||||
"gemini": {
|
||||
Key: "gemini",
|
||||
DisplayName: "Gemini",
|
||||
Sort: 65,
|
||||
DefaultBaseURL: "https://generativelanguage.googleapis.com",
|
||||
EnvKey: "GEMINI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
|
||||
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
|
||||
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
|
||||
},
|
||||
},
|
||||
"moonshot": {
|
||||
Key: "moonshot",
|
||||
DisplayName: "Moonshot (Global)",
|
||||
Sort: 70,
|
||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
||||
EnvKey: "MOONSHOT_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{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", Reasoning: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -218,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) == "" {
|
||||
@@ -250,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+440
-1832
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
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.]+)\)$`)
|
||||
|
||||
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) {
|
||||
var payload openclawSkillsList
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &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) {
|
||||
var payload openclawSkillInfo
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if payload.SkillKey == "" {
|
||||
return "", fmt.Errorf("skill %s does not have a skillKey", name)
|
||||
}
|
||||
return payload.SkillKey, nil
|
||||
}
|
||||
|
||||
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
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"mime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -483,13 +484,14 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
if username == "" {
|
||||
username = req.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
|
||||
cfg := email.SMTPConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
Username: username,
|
||||
Password: req.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, req.DisplayName, req.Sender),
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package docs
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed x-log.json
|
||||
var XLogJson []byte
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package nginx_conf
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
+43
-1
@@ -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,12 +46,16 @@ 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'
|
||||
ErrAgentIDRequired: 'Agent ID is required'
|
||||
ErrAgentAccountIDRequired: 'Account ID is required'
|
||||
ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Professional Edition for unlimited agents'
|
||||
|
||||
#backup
|
||||
Localhost: 'Local'
|
||||
@@ -231,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 }}'
|
||||
@@ -531,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 }}'
|
||||
|
||||
@@ -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,12 +41,16 @@ 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'
|
||||
ErrAgentIDRequired: 'ID de agente requerido'
|
||||
ErrAgentAccountIDRequired: 'ID de cuenta requerido'
|
||||
ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualice a la edición Professional para eliminar el límite'
|
||||
Localhost: 'Máquina local'
|
||||
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
|
||||
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
|
||||
@@ -208,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.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -2,6 +2,8 @@ ErrInvalidParams: '不正なリクエスト: {{ .detail }}'
|
||||
ErrTokenParse: 'トークンエラー: {{ .detail }}'
|
||||
ErrInitialPassword: '元のパスワードが違います'
|
||||
ErrInternalServer: '内部エラー: {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'リモートのダウンロード URL が無効か、到達できません:{{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'リモート URL がファイルではなくエラーページ等を返しました、{{ .detail }}'
|
||||
ErrRecordExist: 'レコードが既に存在します'
|
||||
ErrRecordNotFound: 'レコードが見つかりません'
|
||||
ErrStructTransform: '型変換エラー: {{ .err }}'
|
||||
@@ -39,12 +41,16 @@ ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
||||
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
||||
ErrAgentAccountNotVerified: 'アカウント未検証です'
|
||||
ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
|
||||
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
|
||||
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
|
||||
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
||||
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'ベースURLが必要です'
|
||||
ErrAgentApiKeyRequired: 'APIキーが必要です'
|
||||
ErrAgentComposeRequired: 'Composeが必要です'
|
||||
ErrAgentIDRequired: 'エージェントIDが必要です'
|
||||
ErrAgentAccountIDRequired: 'アカウントIDが必要です'
|
||||
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Professional Edition にアップグレードすると数の制限はありません'
|
||||
Localhost: 'ローカルマシン'
|
||||
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
|
||||
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
|
||||
@@ -208,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: 'イメージはすでに存在します!イメージ名を変更してください。'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -2,6 +2,8 @@ ErrInvalidParams: '잘못된 요청: {{ .detail }}'
|
||||
ErrTokenParse: '토큰 오류: {{ .detail }}'
|
||||
ErrInitialPassword: '원래 비밀번호가 틀립니다'
|
||||
ErrInternalServer: '서버 오류: {{ .detail }}'
|
||||
ErrWgetRemoteFailed: '원격 다운로드 주소가 잘못되었거나 접근할 수 없습니다: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: '원격 주소가 파일이 아닌 콘텐츠를 반환했습니다 (오류 페이지일 수 있음), {{ .detail }}'
|
||||
ErrRecordExist: '레코드가 이미 있습니다'
|
||||
ErrRecordNotFound: '레코드를 찾을 수 없습니다'
|
||||
ErrStructTransform: '형 변환 실패: {{ .err }}'
|
||||
@@ -39,12 +41,16 @@ ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
||||
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
||||
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
||||
ErrAgentProviderMismatch: '에이전트 공급자 불일치'
|
||||
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
|
||||
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
|
||||
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
||||
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: '기본 URL 필요'
|
||||
ErrAgentApiKeyRequired: 'API 키 필요'
|
||||
ErrAgentComposeRequired: 'Compose 내용 필요'
|
||||
ErrAgentIDRequired: '에이전트 ID 필요'
|
||||
ErrAgentAccountIDRequired: '계정 ID 필요'
|
||||
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 생성할 수 있으며, Professional 에디션으로 업그레이드하면 수량 제한이 없습니다'
|
||||
Localhost: '로컬 머신'
|
||||
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
|
||||
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
|
||||
@@ -208,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: '이미지가 이미 존재합니다 이미지 이름을 수정하세요.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -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,12 +41,16 @@ 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'
|
||||
ErrAgentIDRequired: 'ID ejen diperlukan'
|
||||
ErrAgentAccountIDRequired: 'ID akaun diperlukan'
|
||||
ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke edisi Professional untuk tiada had bilangan'
|
||||
Localhost: 'Mesin Tempatan'
|
||||
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
|
||||
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
|
||||
@@ -208,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.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
@@ -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,12 +41,16 @@ 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'
|
||||
ErrAgentIDRequired: 'ID do agente obrigatório'
|
||||
ErrAgentAccountIDRequired: 'ID da conta obrigatório'
|
||||
ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Atualize para a edição Professional para remover o limite'
|
||||
Localhost: 'Máquina Local'
|
||||
ErrBackupInUsed: 'Conta de backup em uso por tarefa'
|
||||
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
|
||||
@@ -208,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.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Неверный запрос: {{ .detail }}'
|
||||
ErrTokenParse: 'Ошибка токена: {{ .detail }}'
|
||||
ErrInitialPassword: 'Исходный пароль неверен'
|
||||
ErrInternalServer: 'Внутренняя ошибка: {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Неверный или недоступный URL удалённой загрузки: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'Удалённый сервер вернул не файл (возможно, страницу ошибки), {{ .detail }}'
|
||||
ErrRecordExist: 'Запись уже существует'
|
||||
ErrRecordNotFound: 'Запись не найдена'
|
||||
ErrStructTransform: 'Ошибка преобразования: {{ .err }}'
|
||||
@@ -39,12 +41,16 @@ ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступн
|
||||
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
|
||||
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
||||
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
||||
ErrAgentProviderMismatch: 'Провайдер не совпадает'
|
||||
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
|
||||
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
|
||||
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
||||
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Нужен базовый URL'
|
||||
ErrAgentApiKeyRequired: 'Нужен API-ключ'
|
||||
ErrAgentComposeRequired: 'Нужен compose'
|
||||
ErrAgentIDRequired: 'Нужен ID агента'
|
||||
ErrAgentAccountIDRequired: 'Нужен ID аккаунта'
|
||||
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Перейдите на Professional Edition, чтобы снять ограничение'
|
||||
Localhost: 'Локальная машина'
|
||||
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
|
||||
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'
|
||||
@@ -208,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: 'Изображение уже существует Пожалуйста, измените имя изображения.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -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,12 +41,16 @@ 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'
|
||||
ErrAgentIDRequired: 'Ajans ID gerekli'
|
||||
ErrAgentAccountIDRequired: 'Hesap ID gerekli'
|
||||
ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırı kaldırmak için Professional sürüme yükseltin'
|
||||
Localhost: 'Yerel Makine'
|
||||
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
|
||||
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
|
||||
@@ -208,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.'
|
||||
@@ -473,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 }}'
|
||||
|
||||
@@ -2,6 +2,8 @@ ErrInvalidParams: '請求參數錯誤: {{ .detail }}'
|
||||
ErrTokenParse: 'Token 產生錯誤: {{ .detail }}'
|
||||
ErrInitialPassword: '原密碼錯誤'
|
||||
ErrInternalServer: '服務內部錯誤: {{ .detail }}'
|
||||
ErrWgetRemoteFailed: '遠端下載網址無效或無法存取:{{ .detail }}'
|
||||
ErrWgetInvalidContentType: '遠端網址未回傳檔案內容(可能是錯誤頁),{{ .detail }}'
|
||||
ErrRecordExist: '記錄已存在'
|
||||
ErrRecordNotFound: '記錄未能找到'
|
||||
ErrStructTransform: '型別轉換失敗: {{ .err }}'
|
||||
@@ -39,12 +41,16 @@ ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }}
|
||||
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
|
||||
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
|
||||
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
|
||||
ErrAgentProviderMismatch: '帳號提供商不符,請重試'
|
||||
ErrAgentAccountModelsRequired: '請至少配置一個模型'
|
||||
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
|
||||
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
|
||||
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
|
||||
ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
|
||||
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
|
||||
ErrAgentIDRequired: '智能體 ID 不可為空,請重試'
|
||||
ErrAgentAccountIDRequired: '帳號 ID 不可為空,請重試'
|
||||
ErrAgentLimitReached: '社群版最多支援建立 {{ .max }} 個智能體,升級至專業版則無數量限制'
|
||||
Localhost: '本機'
|
||||
ErrBackupInUsed: '此備份帳號已在排程任務中使用,無法刪除'
|
||||
ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
|
||||
@@ -208,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: '映像已存在!請修改映像名稱。'
|
||||
@@ -473,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 }}'
|
||||
|
||||
+43
-1
@@ -2,6 +2,8 @@ ErrInvalidParams: "参数错误: {{ .detail }}"
|
||||
ErrTokenParse: "Token 错误: {{ .detail }}"
|
||||
ErrInitialPassword: "原密码错误"
|
||||
ErrInternalServer: "服务错误: {{ .detail }}"
|
||||
ErrWgetRemoteFailed: "远程下载地址无效或无法访问:{{ .detail }}"
|
||||
ErrWgetInvalidContentType: "远程地址返回的不是文件内容(可能是错误页),{{ .detail }}"
|
||||
ErrRecordExist: "记录已存在"
|
||||
ErrRecordNotFound: "记录不存在"
|
||||
ErrStructTransform: "类型转换失败: {{ .err }}"
|
||||
@@ -44,12 +46,16 @@ ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
|
||||
ErrAgentProviderNotSupported: "不支持该智能体提供商"
|
||||
ErrAgentAccountRequired: "请选择智能体账号后重试"
|
||||
ErrAgentAccountNotVerified: "账号未通过验证"
|
||||
ErrAgentProviderMismatch: "账号提供商不匹配"
|
||||
ErrAgentAccountModelsRequired: "请至少配置一个模型"
|
||||
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
|
||||
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
|
||||
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
|
||||
ErrAgentBaseURLRequired: "Base URL 不能为空"
|
||||
ErrAgentApiKeyRequired: "API Key 不能为空"
|
||||
ErrAgentComposeRequired: "自定义编排内容不能为空"
|
||||
ErrAgentIDRequired: "智能体 ID 不能为空"
|
||||
ErrAgentAccountIDRequired: "账号 ID 不能为空"
|
||||
ErrAgentLimitReached: "社区版最多支持创建 {{ .max }} 个智能体,升级至专业版无数量限制"
|
||||
|
||||
#backup
|
||||
Localhost: '本机'
|
||||
@@ -231,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 }} 文件不存在!请检查源文件完整性!"
|
||||
@@ -531,3 +567,9 @@ PartitionDiskErr: "分区失败,{{ .err }}"
|
||||
FormatDiskErr: "格式化磁盘失败,{{ .err }}"
|
||||
MountDiskErr: "挂载磁盘失败,{{ .err }}"
|
||||
XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs"
|
||||
|
||||
# terminal
|
||||
TerminalAIBlockedRiskyCommand: "已拦截风险命令:{{ .command }}"
|
||||
TerminalAIThinking: "AI 正在思考..."
|
||||
TerminalAIReadyToExecute: "思考完成,请回车执行(耗时 {{ .duration }},token:{{ .tokens }})"
|
||||
TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -21,6 +21,7 @@ func Routers() *gin.Engine {
|
||||
if !global.IsMaster {
|
||||
PrivateGroup.Use(middleware.Certificate())
|
||||
}
|
||||
PrivateGroup.Use(middleware.OperationResolveMeta())
|
||||
for _, router := range rou.RouterGroupApp {
|
||||
router.InitRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/docs"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
headerNeedOperationResolve = "X-Need-Op-Resolve"
|
||||
headerOperationResolved = "X-Op-Resolved"
|
||||
)
|
||||
|
||||
var (
|
||||
logMetaOnce sync.Once
|
||||
logMetaData map[string]operationMeta
|
||||
logMetaLoadErr error
|
||||
)
|
||||
|
||||
type operationMeta struct {
|
||||
BodyKeys []string `json:"bodyKeys"`
|
||||
BeforeFunctions []functionInfo `json:"beforeFunctions"`
|
||||
}
|
||||
|
||||
type functionInfo struct {
|
||||
InputColumn string `json:"input_column"`
|
||||
InputValue string `json:"input_value"`
|
||||
IsList bool `json:"isList"`
|
||||
DB string `json:"db"`
|
||||
OutputColumn string `json:"output_column"`
|
||||
OutputValue string `json:"output_value"`
|
||||
}
|
||||
|
||||
func OperationResolveMeta() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader(headerNeedOperationResolve) != "1" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
metaMap, err := loadOperationMeta()
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
reqPath := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
|
||||
meta, ok := metaMap[reqPath]
|
||||
if !ok || len(meta.BeforeFunctions) == 0 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
values := make(map[string]interface{})
|
||||
if len(meta.BodyKeys) > 0 {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err == nil {
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
bodyMap := make(map[string]interface{})
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
bodyMap, _ = parseMultipart(body, c.Request.Header.Get("Content-Type"))
|
||||
} else {
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.UseNumber()
|
||||
_ = decoder.Decode(&bodyMap)
|
||||
}
|
||||
for _, key := range meta.BodyKeys {
|
||||
if value, ok := bodyMap[key]; ok {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolved, err := resolveOperationValues(reqPath, values, meta.BeforeFunctions)
|
||||
if err == nil && len(resolved) > 0 {
|
||||
if data, err := json.Marshal(resolved); err == nil {
|
||||
c.Header(headerOperationResolved, base64.RawURLEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func loadOperationMeta() (map[string]operationMeta, error) {
|
||||
logMetaOnce.Do(func() {
|
||||
logMetaData = make(map[string]operationMeta)
|
||||
logMetaLoadErr = json.Unmarshal(docs.XLogJson, &logMetaData)
|
||||
})
|
||||
return logMetaData, logMetaLoadErr
|
||||
}
|
||||
|
||||
func parseMultipart(formData []byte, contentType string) (map[string]interface{}, error) {
|
||||
d, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil || d != "multipart/form-data" {
|
||||
return nil, http.ErrNotMultipart
|
||||
}
|
||||
boundary, ok := params["boundary"]
|
||||
if !ok {
|
||||
return nil, http.ErrMissingBoundary
|
||||
}
|
||||
reader := multipart.NewReader(bytes.NewReader(formData), boundary)
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
f, err := reader.ReadForm(32 << 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range f.Value {
|
||||
if len(v) > 0 {
|
||||
ret[k] = v[0]
|
||||
}
|
||||
}
|
||||
for k, v := range f.File {
|
||||
if len(v) > 0 {
|
||||
ret[k] = v[0].Filename
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func resolveOperationValues(pathItem string, values map[string]interface{}, beforeFunctions []functionInfo) (map[string]string, error) {
|
||||
dbItem, err := newResolveDB(pathItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeResolveDB(dbItem)
|
||||
|
||||
resolved := make(map[string]string)
|
||||
for _, funcs := range beforeFunctions {
|
||||
if !isSafeIdentifier(funcs.DB) || !isSafeIdentifier(funcs.InputColumn) || !isSafeIdentifier(funcs.OutputColumn) {
|
||||
continue
|
||||
}
|
||||
for key, value := range values {
|
||||
if funcs.InputValue != key {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
if funcs.IsList {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
} else {
|
||||
sql := fmt.Sprintf("SELECT %s FROM %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
||||
_ = dbItem.Raw(sql, value).Scan(&names)
|
||||
}
|
||||
outputValue := strings.Join(names, ",")
|
||||
resolved[funcs.OutputValue] = outputValue
|
||||
values[funcs.OutputValue] = outputValue
|
||||
break
|
||||
}
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func newResolveDB(pathItem string) (*gorm.DB, error) {
|
||||
dbFile := ""
|
||||
switch {
|
||||
case strings.HasPrefix(pathItem, "/core"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/core.db")
|
||||
case strings.HasPrefix(pathItem, "/xpack"):
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db")
|
||||
default:
|
||||
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/agent.db")
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(4)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
sqlDB.SetConnMaxIdleTime(15 * time.Minute)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func closeResolveDB(db *gorm.DB) {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
|
||||
func isSafeIdentifier(val string) bool {
|
||||
return re.GetRegex(re.SQLIdentifierPattern).MatchString(val)
|
||||
}
|
||||
+22
-3
@@ -45,23 +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/browser/get", baseApi.GetAgentBrowserConfig)
|
||||
aiToolsRouter.POST("/agents/browser/update", baseApi.UpdateAgentBrowserConfig)
|
||||
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/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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/jinzhu/copier"
|
||||
"mime"
|
||||
network "net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -71,13 +72,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
|
||||
if username == "" {
|
||||
username = emailInfo.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailInfo.DisplayName)
|
||||
smtpConfig := email.SMTPConfig{
|
||||
Host: emailInfo.Host,
|
||||
Port: emailInfo.Port,
|
||||
Sender: emailInfo.Sender,
|
||||
Username: username,
|
||||
Password: emailInfo.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, emailInfo.DisplayName, emailInfo.Sender),
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailInfo.Sender),
|
||||
Encryption: emailInfo.Encryption,
|
||||
Recipient: emailInfo.Recipient,
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ type authorizer struct {
|
||||
defAuth Authenticator
|
||||
}
|
||||
|
||||
type preemptiveAuthorizer struct {
|
||||
auth Authenticator
|
||||
}
|
||||
|
||||
type authShim struct {
|
||||
factory AuthFactory
|
||||
body io.Reader
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -120,8 +120,7 @@ func parseRecipients(recipient string) []string {
|
||||
|
||||
func buildMessage(config SMTPConfig, message EmailMessage, toList []string) (string, error) {
|
||||
headers := make(map[string]string)
|
||||
encodedFrom := mime.BEncoding.Encode("UTF-8", config.From)
|
||||
headers["From"] = encodedFrom
|
||||
headers["From"] = config.From
|
||||
encodedSubject := mime.BEncoding.Encode("UTF-8", message.Subject)
|
||||
headers["Subject"] = encodedSubject
|
||||
headers["To"] = strings.Join(toList, ",")
|
||||
|
||||
Vendored
+17
-4
@@ -31,10 +31,10 @@ func Marshal(envMap map[string]string) (string, error) {
|
||||
for k, v := range envMap {
|
||||
if d, err := strconv.Atoi(v); err == nil && !isStartWithZero(v) {
|
||||
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
|
||||
} else if hasEvenDoubleQuotes(v) {
|
||||
} else if shouldUseSingleQuotes(v) {
|
||||
lines = append(lines, fmt.Sprintf(`%s='%s'`, k, v))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, v))
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, escapeDoubleQuotedEnvValue(v)))
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
@@ -88,10 +88,10 @@ func MarshalWithOrder(envMap map[string]string, orders []string) (string, error)
|
||||
func formatEnvLine(k, v string) string {
|
||||
if d, err := strconv.Atoi(v); err == nil && !isStartWithZero(v) {
|
||||
return fmt.Sprintf(`%s=%d`, k, d)
|
||||
} else if hasEvenDoubleQuotes(v) {
|
||||
} else if shouldUseSingleQuotes(v) {
|
||||
return fmt.Sprintf(`%s='%s'`, k, v)
|
||||
} else {
|
||||
return fmt.Sprintf(`%s="%s"`, k, v)
|
||||
return fmt.Sprintf(`%s="%s"`, k, escapeDoubleQuotedEnvValue(v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +123,16 @@ func hasEvenDoubleQuotes(s string) bool {
|
||||
}
|
||||
return count%2 == 0
|
||||
}
|
||||
|
||||
func shouldUseSingleQuotes(s string) bool {
|
||||
return hasEvenDoubleQuotes(s) && !strings.Contains(s, `\`) && !strings.Contains(s, `'`)
|
||||
}
|
||||
|
||||
func escapeDoubleQuotedEnvValue(s string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`"`, `\"`,
|
||||
"\n", `\n`,
|
||||
)
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
+187
-20
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -99,6 +100,13 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
|
||||
|
||||
func (r *Local) Delete(info DeleteInfo) error {
|
||||
if len(info.Name) != 0 {
|
||||
inUse, err := r.isDatabaseInUse(info.Name, info.Timeout)
|
||||
if err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("check database connections failed, err: %v", err)
|
||||
}
|
||||
if inUse && !info.ForceDelete {
|
||||
return buserr.WithDetail("ErrInUsed", info.Name, nil)
|
||||
}
|
||||
dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
|
||||
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("drop database failed, err: %v", err)
|
||||
@@ -203,6 +211,30 @@ func (r *Local) SyncDB() ([]SyncDBInfo, error) {
|
||||
|
||||
func (r *Local) Close() {}
|
||||
|
||||
func (r *Local) isDatabaseInUse(name string, timeout uint) (bool, error) {
|
||||
escapedName := strings.ReplaceAll(name, "'", "''")
|
||||
checkSQL := fmt.Sprintf(
|
||||
"SELECT COUNT(*) FROM pg_stat_activity WHERE datname='%s' AND pid <> pg_backend_pid()",
|
||||
escapedName,
|
||||
)
|
||||
lines, err := r.ExecSQLForRows(checkSQL, timeout)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, line := range lines {
|
||||
countStr := strings.TrimSpace(line)
|
||||
if len(countStr) == 0 {
|
||||
continue
|
||||
}
|
||||
count, parseErr := strconv.Atoi(countStr)
|
||||
if parseErr != nil {
|
||||
return false, parseErr
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *Local) ExecSQL(command string, timeout uint) error {
|
||||
itemCommand := r.PrefixCommand[:]
|
||||
itemCommand = append(itemCommand, command)
|
||||
|
||||
@@ -95,6 +95,13 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
|
||||
|
||||
func (r *Remote) Delete(info DeleteInfo) error {
|
||||
if len(info.Name) != 0 {
|
||||
inUse, err := r.isDatabaseInUse(info.Name, info.Timeout)
|
||||
if err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("check database connections failed, err: %v", err)
|
||||
}
|
||||
if inUse && !info.ForceDelete {
|
||||
return buserr.WithDetail("ErrInUsed", info.Name, nil)
|
||||
}
|
||||
dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
|
||||
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
|
||||
return fmt.Errorf("drop database failed, err: %v", err)
|
||||
@@ -107,6 +114,24 @@ func (r *Remote) Delete(info DeleteInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) isDatabaseInUse(name string, timeout uint) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var count int
|
||||
if err := r.Client.QueryRowContext(
|
||||
ctx,
|
||||
"SELECT COUNT(*) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()",
|
||||
name,
|
||||
).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return false, buserr.New("ErrExecTimeOut")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *Remote) ChangePrivileges(info Privileges) error {
|
||||
super := "SUPERUSER"
|
||||
if !info.SuperUser {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
AnsiEscapePattern = "\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>"
|
||||
RecycleBinFilePattern = `_1p_file_1p_(.+)_p_(\d+)_(\d+)`
|
||||
OrderByValidationPattern = `^[a-zA-Z_][a-zA-Z0-9_]*$`
|
||||
SQLIdentifierPattern = `^[A-Za-z_][A-Za-z0-9_]*$`
|
||||
NginxHostPattern = `^[a-zA-Z0-9.-]+(:[0-9]+)?$`
|
||||
NginxPathPattern = `^/[a-zA-Z0-9._/\-]*$`
|
||||
)
|
||||
@@ -60,6 +61,7 @@ func Init() {
|
||||
AnsiEscapePattern,
|
||||
RecycleBinFilePattern,
|
||||
OrderByValidationPattern,
|
||||
SQLIdentifierPattern,
|
||||
NginxHostPattern,
|
||||
NginxPathPattern,
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user