Compare commits

..
66 Commits
Author SHA1 Message Date
CityFun 2d78ca5a86 fix: Fixed issue with agent acount model name is empty (#12343) 2026-03-26 05:44:27 +00:00
KOMATA a9b65b7ce5 feat: add support for trusted SSL certificates in website proxy configuration (#12342) 2026-03-26 13:42:11 +08:00
wanghe-fit2cloud 291b40a45d chore: fix ru locale formatting 2026-03-26 11:08:36 +08:00
CityFun a959da7d44 feat: Optimize the agent's decision-making logic (#12341) 2026-03-26 10:57:48 +08:00
ssongliu 26c4e8323f fix: update quick command batch separator (#12340) 2026-03-26 02:46:10 +00:00
王贺 0534badce7 fix: unify vLLM copy casing (#12339) 2026-03-26 10:32:36 +08:00
CityFun 594d818db0 style: change openclaw style (#12336) 2026-03-25 10:58:17 +00:00
ssongliu 04dceb93a7 fix: unify terminal scroll sensitivity defaults (#12334)
Refs #11911
2026-03-25 09:34:18 +00:00
CityFun 7b554c0d61 feat: Add skill hub for openclaw (#12333) 2026-03-25 09:32:18 +00:00
ssongliu bb642cd8a8 feat: improve terminal quick command workflow (#12331) 2026-03-25 08:56:18 +00:00
CityFun 40249b527d feat: add version control for openclaw config menu (#12329) 2026-03-25 08:54:18 +00:00
蘭 74ff7ddc19 feat: Upload multiple files and add overall and current file upload progress messages (#12327)
https://github.com/1Panel-dev/1Panel/issues/10593
2026-03-25 07:30:19 +00:00
ssongliu 096ec8edc7 chore: refine terminal ai prefix handling (#12328) 2026-03-25 07:28:22 +00:00
ssongliu 0d19069059 feat: improve terminal ai command interception (#12326) 2026-03-25 06:04:18 +00:00
CityFun 0da833302b feat: change openclaw upgrade logic (#12325)
#### What this PR does / why we need it?

#### Summary of your change

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

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-03-25 06:02:17 +00:00
ssongliu 4a69cf0ff5 fix: migrate ai agents menu title 2026-03-24 19:54:38 +08:00
CityFun 39704ef4e5 feat: Add openclaw overview button (#12319) 2026-03-24 10:42:05 +00:00
王贺 e52cfeeb78 fix: sync fit2cloud table locale on first load (#12316) 2026-03-24 18:37:17 +08:00
蘭 476429005c ref: Change tooltip placement from top to left for better visibility (#12317)
#11922
2026-03-24 10:26:05 +00:00
ssongliu 6fb41dc711 feat: refine terminal ai assistant settings (#12315) 2026-03-24 09:10:07 +00:00
CityFun 2cb2058cfa feat: support edit config file (#12313) 2026-03-24 09:08:07 +00:00
蘭 8533dd1d33 fix: Fix the issue where the gz file cannot be previewed after decompression (#12314) 2026-03-24 09:06:06 +00:00
CityFun 0cfbcd25fc feat: Optimize plugin installation logic (#12312) 2026-03-24 08:04:05 +00:00
CityFun b91ca8bdb3 feat: add openclaw skill management menu (#12311) 2026-03-24 05:48:06 +00:00
ssongliu 72764863bc fix: unify terminal ai validation and loading state (#12310) 2026-03-24 05:46:06 +00:00
蘭 a7b9fd0181 feat: add validation and error messages for wget URL input (#12309) 2026-03-24 05:44:07 +00:00
ssongliu 2b3164fe82 chore: format terminal ai i18n entries (#12303) 2026-03-23 17:26:50 +08:00
ssongliu 2c6307049a fix: handle upgrade clean leaf selection (#12302) 2026-03-23 17:15:50 +08:00
CityFun acc1b9270f feat: add npm registry config for openclaw (#12300) 2026-03-23 09:12:03 +00:00
王贺 60e4143525 Format aiSummary string for better readability (#12301) 2026-03-23 16:48:09 +08:00
KOMATA 99bcd6d059 feat: add SSL verification option to website proxy configuration (#12258)
* feat: add SSL verification option to website proxy configuration

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

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

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

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

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

* feat: change openclaw bind type

* feat: change openclaw bind type

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

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

* fix: change update app_install port logic
2026-03-18 15:37:48 +08:00
CityFun 24d7dc0786 fix: Fixed the issue where the OpenClaw access address could not be set to a domain name. (#12201) 2026-03-17 09:55:21 +00:00
172 changed files with 11192 additions and 4316 deletions
+296 -10
View File
@@ -111,6 +111,27 @@ func (b *BaseApi) UpdateAgentModelConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent overview
// @Accept json
// @Param request body dto.AgentOverviewReq true "request"
// @Success 200 {object} dto.AgentOverview
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/overview [post]
func (b *BaseApi) GetAgentOverview(c *gin.Context) {
var req dto.AgentOverviewReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.GetOverview(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Get Providers
// @Success 200 {array} dto.ProviderInfo
@@ -190,6 +211,87 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
})
}
// @Tags AI
// @Summary List Agent account models
// @Accept json
// @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models [post]
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.GetAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Create Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/create [post]
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.CreateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Update Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/update [post]
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Delete Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/delete [post]
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.DeleteAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Verify Agent account
// @Accept json
@@ -315,13 +417,13 @@ func (b *BaseApi) UpdateAgentTelegramConfig(c *gin.Context) {
// @Tags AI
// @Summary Get Agent Discord channel config
// @Accept json
// @Param request body dto.AgentDiscordConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentDiscordConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/discord/get [post]
func (b *BaseApi) GetAgentDiscordConfig(c *gin.Context) {
var req dto.AgentDiscordConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -356,13 +458,13 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) {
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
// @Param request body dto.AgentWecomConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentWecomConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/wecom/get [post]
func (b *BaseApi) GetAgentWecomConfig(c *gin.Context) {
var req dto.AgentWecomConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -394,16 +496,57 @@ func (b *BaseApi) UpdateAgentWecomConfig(c *gin.Context) {
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.AgentQQBotConfigReq true "request"
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {object} dto.AgentQQBotConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/qqbot/get [post]
func (b *BaseApi) GetAgentQQBotConfig(c *gin.Context) {
var req dto.AgentQQBotConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -479,13 +622,13 @@ func (b *BaseApi) CheckAgentPlugin(c *gin.Context) {
// @Tags AI
// @Summary Get Agent Security config
// @Accept json
// @Param request body dto.AgentSecurityConfigReq true "request"
// @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.AgentSecurityConfigReq
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -520,13 +663,13 @@ func (b *BaseApi) UpdateAgentSecurityConfig(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
}
@@ -558,6 +701,149 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent config file
// @Accept json
// @Param request body dto.AgentConfigFileReq true "request"
// @Success 200 {object} dto.AgentConfigFile
// @Security ApiKeyAuth
// @Security Timestamp
// @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
}
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
}
helper.Success(c)
}
// @Tags AI
// @Summary Approve Agent channel pairing code
// @Accept json
+1
View File
@@ -70,6 +70,7 @@ var (
recycleBinService = service.NewIRecycleBinService()
favoriteService = service.NewIFavoriteService()
hostService = service.NewIHostService()
websiteCAService = service.NewIWebsiteCAService()
taskService = service.NewITaskService()
+23
View File
@@ -515,6 +515,29 @@ func (b *BaseApi) WgetFile(c *gin.Context) {
})
}
// @Tags File
// @Summary Stop wget file download
// @Accept json
// @Param request body request.FileProcessReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/stop [post]
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"停止下载任务 [key]","formatEN":"Stop wget task [key]"}
func (b *BaseApi) StopWget(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if strings.TrimSpace(req.Key) == "" {
helper.BadRequest(c, errors.New("key is required"))
return
}
files.CancelDownload(req.Key)
helper.Success(c)
}
// @Tags File
// @Summary Move file
// @Accept json
+167
View File
@@ -0,0 +1,167 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
host, err := hostService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, host)
}
func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, hostService.TestByInfo(req))
}
func (b *BaseApi) TestByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, hostService.TestLocalConn(req.ID))
}
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := hostService.SearchForTree(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := hostService.SearchWithPage(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{Items: list, Total: total})
}
func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := hostService.Delete(req.IDs); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var err error
if len(req.Password) != 0 && req.AuthMode == "password" {
req.Password, err = hostService.EncryptHost(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.PrivateKey = ""
req.PassPhrase = ""
}
if len(req.PrivateKey) != 0 && req.AuthMode == "key" {
req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey)
if err != nil {
helper.BadRequest(c, err)
return
}
if len(req.PassPhrase) != 0 {
req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase)
if err != nil {
helper.BadRequest(c, err)
return
}
}
req.Password = ""
}
upMap := map[string]interface{}{
"name": req.Name,
"group_id": req.GroupID,
"addr": req.Addr,
"port": req.Port,
"user": req.User,
"auth_mode": req.AuthMode,
"remember_password": req.RememberPassword,
"description": req.Description,
}
if req.AuthMode == "password" {
upMap["password"] = req.Password
upMap["private_key"] = ""
upMap["pass_phrase"] = ""
} else {
upMap["password"] = ""
upMap["private_key"] = req.PrivateKey
upMap["pass_phrase"] = req.PassPhrase
}
hostItem, err := hostService.Update(req.ID, upMap)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, hostItem)
}
func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if _, err := hostService.Update(req.ID, map[string]interface{}{"group_id": req.GroupID}); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
info, err := hostService.GetHostByID(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, info)
}
+22
View File
@@ -27,6 +27,15 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting)
}
func (b *BaseApi) GetTerminalAISettingInfo(c *gin.Context) {
setting, err := settingService.GetTerminalAIInfo()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, setting)
}
// @Tags System Setting
// @Summary Load system available status
// @Success 200
@@ -59,6 +68,19 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
helper.Success(c)
}
func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
var req dto.TerminalAIInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := settingService.UpdateTerminalAI(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags System Setting
// @Summary Load local backup dir
// @Success 200 {string} path
+29 -3
View File
@@ -9,8 +9,10 @@ import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/terminal"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
@@ -40,9 +42,33 @@ func (b *BaseApi) WsSSH(c *gin.Context) {
return
}
client, err := loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
var client *ssh.SSHClient
if hostID > 0 {
host, err := service.GetHostInfo(uint(hostID))
if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) {
return
}
connInfo := ssh.ConnInfo{
Addr: host.Addr,
Port: int(host.Port),
User: host.User,
AuthMode: host.AuthMode,
Password: host.Password,
PrivateKey: []byte(host.PrivateKey),
}
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
client, err = ssh.NewClient(connInfo)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
} else {
client, err = loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
}
defer client.Close()
command := c.DefaultQuery("command", "")
+159 -58
View File
@@ -8,15 +8,9 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType"`
Provider string `json:"provider"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw"`
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"`
@@ -75,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 {
@@ -94,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"`
}
@@ -119,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 {
@@ -195,14 +237,10 @@ type AgentTelegramConfig struct {
type AgentChannelPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom dingtalk-connector"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentWecomConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentWecomConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
@@ -219,8 +257,31 @@ type AgentWecomConfig struct {
Installed bool `json:"installed"`
}
type AgentQQBotConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
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 {
@@ -239,23 +300,19 @@ type AgentQQBotConfig struct {
type AgentPluginInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=qqbot wecom"`
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"`
Type string `json:"type" validate:"required,oneof=qqbot wecom dingtalk weixin"`
}
type AgentPluginStatus struct {
Installed bool `json:"installed"`
}
type AgentDiscordConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentDiscordConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
@@ -273,10 +330,6 @@ type AgentDiscordConfig struct {
Proxy string `json:"proxy"`
}
type AgentSecurityConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentSecurityConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
AllowedOrigins []string `json:"allowedOrigins"`
@@ -286,17 +339,65 @@ type AgentSecurityConfig struct {
AllowedOrigins []string `json:"allowedOrigins"`
}
type AgentOtherConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentOtherConfigUpdateReq struct {
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"`
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"`
}
+6
View File
@@ -11,6 +11,12 @@ type SearchPageWithType struct {
Type string `json:"type"`
}
type SearchPageWithGroup struct {
PageInfo
GroupID uint `json:"groupID"`
Info string `json:"info"`
}
type PageInfo struct {
Page int `json:"page" validate:"required,number"`
PageSize int `json:"pageSize" validate:"required,number"`
+11 -10
View File
@@ -287,16 +287,17 @@ type BatchDelete struct {
}
type ComposeInfo struct {
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerCount int `json:"containerCount"`
RunningCount int `json:"runningCount"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env string `json:"env"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerCount int `json:"containerCount"`
RunningCount int `json:"runningCount"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
ComposeFileExists bool `json:"composeFileExists"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env string `json:"env"`
}
type ComposeContainer struct {
ContainerID string `json:"containerID"`
@@ -1,8 +1,6 @@
package dto
import (
"time"
)
import "time"
type HostOperate struct {
ID uint `json:"id"`
@@ -34,11 +32,6 @@ type SearchForTree struct {
Info string `json:"info"`
}
type ChangeHostGroup struct {
ID uint `json:"id" validate:"required"`
GroupID uint `json:"groupID" validate:"required"`
}
type HostInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
+1
View File
@@ -269,6 +269,7 @@ type WebsiteProxyConfig struct {
Replaces map[string]string `json:"replaces"`
SNI bool `json:"sni"`
ProxySSLName string `json:"proxySSLName"`
SSLVerify bool `json:"sslVerify"`
CorsConfig
}
+8
View File
@@ -87,6 +87,14 @@ type SystemProxy struct {
Password string `json:"password"`
}
type TerminalAIInfo struct {
AIStatus string `json:"aiStatus"`
AIAccountID string `json:"aiAccountId"`
AIPrefix string `json:"aiPrefix" validate:"required,oneof=@ai #ai /ai"`
AIRiskCommands string `json:"aiRiskCommands"`
AIRiskCommandsDefault string `json:"aiRiskCommandsDefault"`
}
type CommonDescription struct {
ID string `json:"id" validate:"required"`
Type string `json:"type" validate:"required"`
-3
View File
@@ -6,10 +6,7 @@ type AgentAccount struct {
Name string `json:"name"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
+17
View File
@@ -0,0 +1,17 @@
package model
type AgentAccountModel struct {
BaseModel
AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
}
func (AgentAccountModel) TableName() string {
return "agent_account_models"
}
+230 -60
View File
@@ -5,8 +5,19 @@ import (
)
type Model struct {
ID string
Name string
ID string
Name string
ContextWindow int
MaxTokens int
Reasoning bool
Input []string
}
type RuntimeDefault struct {
APIType string
ContextWindow int
MaxTokens int
Input []string
}
type Meta struct {
@@ -15,8 +26,8 @@ type Meta struct {
Sort uint
DefaultBaseURL string
EnvKey string
Default RuntimeDefault
Models []Model
Enabled bool
}
var catalog = map[string]Meta{
@@ -26,14 +37,24 @@ var catalog = map[string]Meta{
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Enabled: true,
Models: []Model{},
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"ollama": {
Key: "ollama",
DisplayName: "Ollama",
Sort: 15,
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 160000,
MaxTokens: 8192,
Input: []string{"text"},
},
},
"vllm": {
Key: "vllm",
@@ -41,8 +62,13 @@ var catalog = map[string]Meta{
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Enabled: true,
Models: []Model{},
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"deepseek": {
Key: "deepseek",
@@ -50,11 +76,16 @@ var catalog = map[string]Meta{
Sort: 25,
DefaultBaseURL: "https://api.deepseek.com/v1",
EnvKey: "DEEPSEEK_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"},
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", Reasoning: true},
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B", Reasoning: true},
},
},
"bailian-coding-plan": {
@@ -63,16 +94,21 @@ var catalog = map[string]Meta{
Sort: 30,
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
EnvKey: "QWEN_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max"},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next"},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5"},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5"},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5"},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7"},
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
},
},
"ark-coding-plan": {
@@ -81,14 +117,19 @@ var catalog = map[string]Meta{
Sort: 35,
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "ark-coding-plan/doubao-seed-2.0-code", Name: "Doubao-Seed-2.0-Code"},
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao-Seed-Code"},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5"},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7"},
{ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2"},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking"},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5", Reasoning: true},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2", Reasoning: true},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking", Reasoning: true},
},
},
"zai": {
@@ -97,12 +138,17 @@ var catalog = map[string]Meta{
Sort: 40,
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5"},
{ID: "zai/glm-4.7", Name: "GLM-4.7"},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash"},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX"},
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
},
},
"minimax": {
@@ -111,23 +157,54 @@ var catalog = map[string]Meta{
Sort: 45,
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
EnvKey: "MINIMAX_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5"},
{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",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
"kimi-coding": {
@@ -136,9 +213,14 @@ var catalog = map[string]Meta{
Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", Reasoning: true},
},
},
"openai": {
@@ -147,14 +229,18 @@ var catalog = map[string]Meta{
Sort: 55,
DefaultBaseURL: "https://api.openai.com/v1",
EnvKey: "OPENAI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
{ID: "openai/gpt-4o", Name: "GPT-4o"},
{ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"},
{ID: "openai/gpt-5", Name: "GPT-5"},
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"},
{ID: "openai/codex-mini-latest", Name: "Codex Mini", Reasoning: true},
{ID: "openai/gpt-5", Name: "GPT-5", Reasoning: true},
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini", Reasoning: true},
{ID: "openai/gpt-5.4", Name: "GPT-5.4", Reasoning: true},
{ID: "openai/gpt-5.3-codex", Name: "GPT-5.3-Codex", Reasoning: true},
},
},
"openrouter": {
@@ -163,7 +249,12 @@ var catalog = map[string]Meta{
Sort: 56,
DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
@@ -175,13 +266,18 @@ var catalog = map[string]Meta{
Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"},
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"},
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet", Reasoning: true},
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1", Reasoning: true},
},
},
"gemini": {
@@ -190,11 +286,16 @@ var catalog = map[string]Meta{
Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
},
"moonshot": {
@@ -203,11 +304,16 @@ var catalog = map[string]Meta{
Sort: 70,
DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY",
Enabled: true,
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
}
@@ -228,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) == "" {
@@ -260,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
}
+237 -238
View File
@@ -10,263 +10,322 @@ type OpenClawPatch struct {
Models map[string]interface{}
}
func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
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":
return buildDeepseekPatch(modelName, baseURL, apiKey), nil
spec = buildDeepseekPatchSpec(modelName, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "gemini":
return buildGeminiPatch(modelName), nil
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "moonshot", "kimi":
return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil
spec = buildMoonshotPatchSpec(provider, modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "bailian-coding-plan":
return buildBailianPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
spec = buildBailianPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "ark-coding-plan":
return buildArkPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
spec = buildArkPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "minimax":
return buildMiniMaxPatch(modelID, baseURL, apiKey), nil
spec = buildMiniMaxPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "xiaomi":
spec = buildXiaomiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "custom", "vllm":
return buildCustomPatch(provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
spec = buildCustomPatchSpec(provider, modelName, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "ollama":
return buildOllamaPatch(modelName, modelID, apiType, baseURL), nil
spec = buildOllamaPatchSpec(modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL)
case "kimi-coding":
return buildKimiCodingPatch(modelName, modelID, baseURL, apiKey), nil
spec = buildKimiCodingPatchSpec(modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
case "zai":
return buildZaiPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
spec = buildZaiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
default:
return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
}
return buildOpenClawPatch(spec), nil
}
func buildGeminiPatch(modelName string) *OpenClawPatch {
return &OpenClawPatch{PrimaryModel: modelName}
}
func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch {
func buildOpenClawPatch(spec openClawPatchSpec) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: modelName,
Models: providerModels("deepseek", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.deepseek.com/v1"), "openai-completions", map[string]interface{}{
"id": "deepseek-chat",
"name": "DeepSeek Chat",
"reasoning": false,
"input": []string{"text"},
"contextWindow": 128000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
PrimaryModel: spec.PrimaryModel,
Models: providerModels(
spec.Provider,
spec.APIKey,
spec.BaseURL,
spec.APIType,
spec.AuthHeader,
buildOpenClawModel(spec.Model),
),
}
}
func buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
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 &OpenClawPatch{
return openClawPatchSpec{
PrimaryModel: primaryModel,
Models: providerModels(configProvider, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), "openai-completions", map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": strings.Contains(strings.ToLower(modelID), "thinking"),
"input": []string{"text"},
"contextWindow": 256000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
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 buildBailianPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeBailianCodingPlanModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID),
Models: providerModels("bailian-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("bailian-coding-plan", baseURL), "openai-completions", map[string]interface{}{
"id": normalizedID,
"name": normalizedID,
"reasoning": isReasoningModel(normalizedID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
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 buildArkPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeArkCodingPlanModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "ark-coding-plan/" + normalizedID,
Models: providerModels("ark-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("ark-coding-plan", baseURL), "openai-completions", map[string]interface{}{
"id": normalizedID,
"name": normalizedID,
"reasoning": isReasoningModel(normalizedID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
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 buildMiniMaxPatch(modelID, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeMiniMaxModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "minimax-portal/" + normalizedID,
Models: providerModels("minimax-portal", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.minimaxi.com/anthropic"), "anthropic-messages", map[string]interface{}{
"id": normalizedID,
"name": strings.ReplaceAll(normalizedID, "-", " "),
"reasoning": false,
"input": []string{"text"},
"contextWindow": 200000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
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 buildCustomPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
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 &OpenClawPatch{
return openClawPatchSpec{
PrimaryModel: provider + "/" + customModelID,
Models: providerModels(provider, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), apiType, map[string]interface{}{
"id": customModelID,
"name": customModelID,
"reasoning": isReasoningModel(customModelID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 128000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
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 buildOllamaPatch(modelName, modelID, apiType, baseURL string) *OpenClawPatch {
api := normalizeAPIType(apiType)
if api != "openai-completions" && api != "openai-responses" {
api = "openai-responses"
}
return &OpenClawPatch{
func buildOllamaPatchSpec(modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: modelName,
Models: providerModels("ollama", "ollama", strings.TrimSpace(baseURL), api, map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": api != "openai-completions",
"input": []string{"text"},
"contextWindow": 160000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
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 buildKimiCodingPatch(modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
func buildKimiCodingPatchSpec(modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: modelName,
Models: providerModels("kimi-coding", strings.TrimSpace(apiKey), withCatalogDefault("kimi-coding", baseURL), "anthropic-messages", map[string]interface{}{
"id": modelID,
"name": "Kimi for Coding",
"reasoning": true,
"input": []string{"text", "image"},
"contextWindow": 262144,
"maxTokens": 32768,
"cost": map[string]interface{}{},
}),
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 buildZaiPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
func buildZaiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
return openClawPatchSpec{
PrimaryModel: "zai/" + modelID,
Models: providerModels("zai", strings.TrimSpace(apiKey), withCatalogDefault("zai", baseURL), "openai-completions", map[string]interface{}{
"id": modelID,
"name": zaiModelDisplayName(modelID),
"reasoning": modelID == "glm-5",
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 204800),
"maxTokens": fallbackInt(maxTokens, 131072),
"cost": map[string]interface{}{},
}),
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 buildGenericPatch(provider, modelName, modelID, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
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 &OpenClawPatch{
return openClawPatchSpec{
PrimaryModel: primaryModel,
Models: providerModels(providerName, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), normalizeAPIType(apiType), map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": isReasoningModel(modelID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
}
}
func providerModels(provider, apiKey, baseURL, api string, model map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"mode": "merge",
"providers": map[string]interface{}{
provider: map[string]interface{}{
"apiKey": apiKey,
"baseUrl": baseURL,
"api": api,
"models": []map[string]interface{}{model},
},
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 withCatalogDefault(provider, baseURL string) string {
if strings.TrimSpace(baseURL) != "" {
return strings.TrimSpace(baseURL)
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 defaultURL, ok := DefaultBaseURL(provider); ok {
return defaultURL
if authHeader {
providerConfig["authHeader"] = true
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
return map[string]interface{}{
"mode": "merge",
"providers": map[string]interface{}{
provider: providerConfig,
},
}
return ""
}
func fallbackInt(value, fallback int) int {
if value > 0 {
return value
}
return fallback
}
func normalizeAPIType(apiType string) string {
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "" {
return "openai-completions"
}
return trim
}
func normalizeCustomModel(modelName string) string {
@@ -277,63 +336,3 @@ func normalizeCustomModel(modelName string) string {
}
return trim
}
func normalizeBailianCodingPlanModelID(modelID string) string {
trim := strings.TrimSpace(modelID)
switch strings.ToLower(trim) {
case "minimax-m2.5", "minimax m2.5", "minimax/minimax-m2.5", "minimax/minimax m2.5":
return "MiniMax/MiniMax-M2.5"
default:
return trim
}
}
func normalizeArkCodingPlanModelID(modelID string) string {
return strings.ToLower(strings.TrimSpace(modelID))
}
func normalizeMiniMaxModelID(modelID string) string {
switch strings.ToLower(strings.TrimSpace(modelID)) {
case "minimax-m2.1", "minimax m2.1", "minimax-m2.1-preview", "minimax-m2.1-latest":
return "MiniMax-M2.1"
case "minimax-m2.1-lightning", "minimax m2.1 lightning":
return "MiniMax-M2.1-lightning"
default:
return modelID
}
}
func zaiModelDisplayName(modelID string) string {
switch strings.ToLower(strings.TrimSpace(modelID)) {
case "glm-5":
return "GLM-5"
case "glm-4.7":
return "GLM-4.7"
case "glm-4.7-flash":
return "GLM-4.7-Flash"
case "glm-4.7-flashx":
return "GLM-4.7-FlashX"
default:
return strings.TrimSpace(modelID)
}
}
func bailianPrimaryModelID(modelID string) string {
trim := strings.TrimSpace(modelID)
if trim == "" {
return ""
}
parts := strings.Split(trim, "/")
for i := len(parts) - 1; i >= 0; i-- {
part := strings.TrimSpace(parts[i])
if part != "" {
return part
}
}
return trim
}
func isReasoningModel(modelID string) bool {
trim := strings.ToLower(strings.TrimSpace(modelID))
return strings.Contains(trim, "reason") || strings.Contains(trim, "thinking")
}
+52 -8
View File
@@ -18,6 +18,10 @@ type VerifyRequest struct {
Body []byte
}
const (
defaultVerifyTimeout = 30 * time.Second
)
func SkipVerification(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "custom", "vllm", "ollama", "kimi-coding":
@@ -42,7 +46,7 @@ func VerifyAccount(provider, baseURL, apiKey string) error {
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
@@ -53,6 +57,10 @@ func VerifyAccount(provider, baseURL, apiKey string) error {
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), "/")
@@ -115,17 +123,53 @@ func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
})
case "minimax":
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["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.1",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"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") {
+50
View File
@@ -0,0 +1,50 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
type AgentAccountModelRepo struct{}
type IAgentAccountModelRepo interface {
List(opts ...DBOption) ([]model.AgentAccountModel, error)
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
Create(item *model.AgentAccountModel) error
Save(item *model.AgentAccountModel) error
DeleteByID(id uint) error
Delete(opts ...DBOption) error
}
func NewIAgentAccountModelRepo() IAgentAccountModelRepo {
return &AgentAccountModelRepo{}
}
func (a AgentAccountModelRepo) List(opts ...DBOption) ([]model.AgentAccountModel, error) {
var list []model.AgentAccountModel
if err := getDb(opts...).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (a AgentAccountModelRepo) GetFirst(opts ...DBOption) (*model.AgentAccountModel, error) {
var item model.AgentAccountModel
if err := getDb(opts...).First(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (a AgentAccountModelRepo) Create(item *model.AgentAccountModel) error {
return getDb().Create(item).Error
}
func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) error {
return getDb().Save(item).Error
}
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
return getDb().Delete(&model.AgentAccountModel{}, id).Error
}
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
}
+1 -1
View File
@@ -80,7 +80,7 @@ func (u *LauncherRepo) ListQuickJump(withAll bool) []model.QuickJump {
}
if !withAll && len(quicks) == 0 {
return []model.QuickJump{
{Name: "Agent", Title: "aiTools.agents.agents", Recommend: 1, IsShow: true, Router: "/ai/agents/agent"},
{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: false, Router: "/cronjobs"},
+12
View File
@@ -19,6 +19,12 @@ func WithByID(id uint) DBOption {
}
}
func WithByGroupID(id uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("group_id = ?", id)
}
}
func WithByNOTID(id uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("id != ?", id)
@@ -43,6 +49,12 @@ func WithByName(name string) DBOption {
}
}
func WithByAddr(addr string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("addr = ?", addr)
}
}
func WithByKey(key string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("key = ?", key)
+86
View File
@@ -10,6 +10,18 @@ import (
type HostRepo struct{}
type IHostRepo interface {
Get(opts ...DBOption) (model.Host, error)
GetList(opts ...DBOption) ([]model.Host, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error)
Create(host *model.Host) error
Update(id uint, vars map[string]interface{}) error
UpdateGroup(group, newGroup uint) error
Delete(opts ...DBOption) error
WithByInfo(info string) DBOption
WithByPort(port uint) DBOption
WithByUser(user string) DBOption
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
SaveFirewallRecord(firewall *model.Firewall) error
@@ -30,6 +42,80 @@ func NewIHostRepo() IHostRepo {
return &HostRepo{}
}
func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) {
var host model.Host
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&host).Error
return host, err
}
func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&hosts).Error
return hosts, err
}
func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&hosts).Error
return count, hosts, err
}
func (h *HostRepo) WithByInfo(info string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}
}
func (h *HostRepo) WithByPort(port uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("port = ?", port)
}
}
func (h *HostRepo) WithByUser(user string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("user = ?", user)
}
}
func (h *HostRepo) Create(host *model.Host) error {
return global.DB.Create(host).Error
}
func (h *HostRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error
}
func (h *HostRepo) UpdateGroup(group, newGroup uint) error {
return global.DB.Model(&model.Host{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error
}
func (h *HostRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Host{}).Error
}
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
var firewall model.Firewall
db := global.DB
+349 -1949
View File
File diff suppressed because it is too large Load Diff
+619
View File
@@ -0,0 +1,619 @@
package service
import (
"fmt"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractFeishuConfig(conf)
return &result, nil
}
func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setFeishuConfig(conf, dto.AgentFeishuConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotName: req.BotName,
AppID: req.AppID,
AppSecret: req.AppSecret,
})
setFeishuPluginEnabled(conf, req.Enabled)
return nil
})
}
func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractTelegramConfig(conf)
return &result, nil
}
func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setTelegramConfig(conf, dto.AgentTelegramConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotToken: req.BotToken,
Proxy: req.Proxy,
})
return nil
})
}
func (a AgentService) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) {
_, _, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractDiscordConfig(conf)
return &result, nil
}
func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setDiscordConfig(conf, dto.AgentDiscordConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
GroupPolicy: req.GroupPolicy,
Token: req.Token,
Proxy: req.Proxy,
})
return nil
})
}
func (a AgentService) GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractQQBotConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "qqbot")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setQQBotConfig(conf, dto.AgentQQBotConfig{
Enabled: req.Enabled,
AppID: req.AppID,
ClientSecret: req.ClientSecret,
})
return nil
})
}
func (a AgentService) GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractWecomConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "wecom")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setWecomConfig(conf, dto.AgentWecomConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotID: req.BotID,
Secret: req.Secret,
})
return nil
})
}
func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error) {
_, install, conf, err := a.loadAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
result := extractDingTalkConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "dingtalk")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setDingTalkConfig(conf, dto.AgentDingTalkConfig{
Enabled: req.Enabled,
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
})
return nil
})
}
func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
spec, pluginID, err := resolvePluginMeta(req.Type)
if err != nil {
return err
}
installTask, err := task.NewTaskWithOps(req.Type, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Type == "qqbot" {
legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot")
if err := mgr.RunBashCf("docker exec %s test -d %s", install.ContainerName, legacyPluginPath); err == nil {
if err := mgr.Run("docker", "exec", "-i", install.ContainerName, "sh", "-c", "printf 'yes\\n' | openclaw plugins uninstall qqbot"); err != nil {
return err
}
}
}
if err := mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawPluginInstallScript(spec, pluginID)); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
appendPluginAllow(conf, pluginID)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("install openclaw plugin failed: %v", err)
}
}()
return nil
}
func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
loginTask, err := task.NewTaskWithOps("weixin", task.TaskExec, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
loginTask.AddSubTask("Login OpenClaw Weixin channel", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(30*time.Minute))
return mgr.RunBashCf("docker exec %s openclaw channels login --channel openclaw-weixin", install.ContainerName)
}, nil)
go func() {
if err := loginTask.Execute(); err != nil {
global.LOG.Errorf("login openclaw weixin channel failed: %v", err)
}
}()
return nil
}
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
installed, err := checkPluginInstalled(install.ContainerName, req.Type)
if err != nil {
return nil, err
}
return &dto.AgentPluginStatus{Installed: installed}, nil
}
func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := cmd.RunDefaultBashCf(
"docker exec %s openclaw pairing approve %s %q",
install.ContainerName,
req.Type,
strings.TrimSpace(req.PairingCode),
); err != nil {
return err
}
return nil
}
func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
feishu, ok := channels["feishu"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := feishu["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
accounts, ok := feishu["accounts"].(map[string]interface{})
if !ok {
return result
}
main, ok := accounts["main"].(map[string]interface{})
if !ok {
return result
}
if appID, ok := main["appId"].(string); ok {
result.AppID = appID
}
if appSecret, ok := main["appSecret"].(string); ok {
result.AppSecret = appSecret
}
if botName, ok := main["botName"].(string); ok {
result.BotName = botName
}
return result
}
func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) {
channels := ensureChildMap(conf, "channels")
feishu := ensureChildMap(channels, "feishu")
feishu["enabled"] = config.Enabled
feishu["dmPolicy"] = config.DmPolicy
accounts := ensureChildMap(feishu, "accounts")
main := ensureChildMap(accounts, "main")
main["appId"] = config.AppID
main["appSecret"] = config.AppSecret
main["botName"] = config.BotName
if strings.EqualFold(config.DmPolicy, "open") {
feishu["allowFrom"] = []string{"*"}
}
}
func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
feishu := ensureChildMap(entries, "feishu")
feishu["enabled"] = enabled
}
func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig {
result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
telegram, ok := channels["telegram"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := telegram["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
if botToken, ok := telegram["botToken"].(string); ok {
result.BotToken = botToken
}
if proxy, ok := telegram["proxy"].(string); ok {
result.Proxy = proxy
}
return result
}
func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) {
channels := ensureChildMap(conf, "channels")
telegram := map[string]interface{}{
"enabled": config.Enabled,
"dmPolicy": config.DmPolicy,
"botToken": config.BotToken,
}
if strings.EqualFold(config.DmPolicy, "open") {
telegram["allowFrom"] = []string{"*"}
}
if strings.TrimSpace(config.Proxy) != "" {
telegram["proxy"] = strings.TrimSpace(config.Proxy)
}
channels["telegram"] = telegram
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
discord, ok := channels["discord"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := discord["enabled"].(bool); ok {
result.Enabled = enabled
}
if token, ok := discord["token"].(string); ok {
result.Token = token
}
if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
result.GroupPolicy = groupPolicy
}
if proxy, ok := discord["proxy"].(string); ok {
result.Proxy = proxy
}
if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" {
result.DmPolicy = policy
return result
}
dm, ok := discord["dm"].(map[string]interface{})
if ok {
if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" {
result.DmPolicy = policy
}
}
return result
}
func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) {
channels := ensureChildMap(conf, "channels")
discord := ensureChildMap(channels, "discord")
discord["enabled"] = config.Enabled
discord["token"] = config.Token
discord["dmPolicy"] = config.DmPolicy
discord["groupPolicy"] = config.GroupPolicy
if strings.EqualFold(config.DmPolicy, "open") {
discord["allowFrom"] = []string{"*"}
} else {
delete(discord, "allowFrom")
}
if strings.TrimSpace(config.Proxy) != "" {
discord["proxy"] = strings.TrimSpace(config.Proxy)
} else {
delete(discord, "proxy")
}
delete(discord, "dm")
}
func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
result := dto.AgentQQBotConfig{Enabled: true}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
qqbot, ok := channels["qqbot"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := qqbot["enabled"].(bool); ok {
result.Enabled = enabled
}
if appID, ok := qqbot["appId"].(string); ok {
result.AppID = appID
}
if clientSecret, ok := qqbot["clientSecret"].(string); ok {
result.ClientSecret = clientSecret
}
return result
}
func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
wecom, ok := channels["wecom"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := wecom["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = strings.TrimSpace(dmPolicy)
}
if botID, ok := wecom["botId"].(string); ok {
result.BotID = botID
}
if secret, ok := wecom["secret"].(string); ok {
result.Secret = secret
}
return result
}
func extractDingTalkConfig(conf map[string]interface{}) dto.AgentDingTalkConfig {
result := dto.AgentDingTalkConfig{
Enabled: true,
DmPolicy: "pairing",
GroupPolicy: "disabled",
AllowFrom: []string{},
GroupAllowFrom: []string{},
}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
dingtalk, ok := channels["dingtalk-connector"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := dingtalk["enabled"].(bool); ok {
result.Enabled = enabled
}
if clientID, ok := dingtalk["clientId"].(string); ok {
result.ClientID = clientID
}
if clientSecret, ok := dingtalk["clientSecret"].(string); ok {
result.ClientSecret = clientSecret
}
if dmPolicy, ok := dingtalk["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = dmPolicy
}
if groupPolicy, ok := dingtalk["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
result.GroupPolicy = groupPolicy
}
result.AllowFrom = extractStringList(dingtalk["allowFrom"])
result.GroupAllowFrom = extractStringList(dingtalk["groupAllowFrom"])
return result
}
func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
channels := ensureChildMap(conf, "channels")
wecom := ensureChildMap(channels, "wecom")
wecom["enabled"] = config.Enabled
wecom["botId"] = strings.TrimSpace(config.BotID)
wecom["secret"] = strings.TrimSpace(config.Secret)
wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy)
if strings.EqualFold(config.DmPolicy, "open") {
wecom["allowFrom"] = []string{"*"}
} else {
wecom["allowFrom"] = []string{}
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
wecomEntry["enabled"] = config.Enabled
}
func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConfig) {
channels := ensureChildMap(conf, "channels")
dingtalk := ensureChildMap(channels, "dingtalk-connector")
dingtalk["enabled"] = config.Enabled
dingtalk["clientId"] = strings.TrimSpace(config.ClientID)
dingtalk["clientSecret"] = strings.TrimSpace(config.ClientSecret)
dingtalk["dmPolicy"] = config.DmPolicy
dingtalk["groupPolicy"] = config.GroupPolicy
dingtalk["gatewayToken"] = extractGatewayToken(conf)
switch config.DmPolicy {
case "open":
dingtalk["allowFrom"] = []string{"*"}
case "allowlist":
dingtalk["allowFrom"] = append([]string(nil), config.AllowFrom...)
default:
delete(dingtalk, "allowFrom")
}
switch config.GroupPolicy {
case "open":
dingtalk["groupAllowFrom"] = []string{"*"}
case "allowlist":
dingtalk["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
default:
delete(dingtalk, "groupAllowFrom")
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
dingtalkEntry := ensureChildMap(entries, "dingtalk-connector")
dingtalkEntry["enabled"] = config.Enabled
gateway := ensureChildMap(conf, "gateway")
httpMap := ensureChildMap(gateway, "http")
endpoints := ensureChildMap(httpMap, "endpoints")
chatCompletions := ensureChildMap(endpoints, "chatCompletions")
chatCompletions["enabled"] = true
}
func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
channels := ensureChildMap(conf, "channels")
qqbot := ensureChildMap(channels, "qqbot")
delete(qqbot, "dmPolicy")
qqbot["enabled"] = config.Enabled
qqbot["allowFrom"] = []string{"*"}
qqbot["appId"] = strings.TrimSpace(config.AppID)
qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret)
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
delete(entries, "qqbot")
qqbotEntry := ensureChildMap(entries, "openclaw-qqbot")
qqbotEntry["enabled"] = config.Enabled
}
func appendPluginAllow(conf map[string]interface{}, pluginID string) {
plugins := ensureChildMap(conf, "plugins")
allow := make([]string, 0, 4)
seen := map[string]struct{}{}
switch values := plugins["allow"].(type) {
case []interface{}:
for _, value := range values {
text, ok := value.(string)
if !ok || text == "" {
continue
}
if _, ok := seen[text]; ok {
continue
}
seen[text] = struct{}{}
allow = append(allow, text)
}
}
if _, ok := seen[pluginID]; ok {
plugins["allow"] = allow
return
}
plugins["allow"] = append(allow, pluginID)
}
func buildOpenclawPluginInstallScript(spec, pluginID string) string {
return fmt.Sprintf(
"set -e; workdir=%s/%s; rm -rf \"$workdir\"; mkdir -p \"$workdir\"; cd \"$workdir\"; npm pack --silent %q >/dev/null 2>&1; pkg=$(find \"$workdir\" -maxdepth 1 -type f -name '*.tgz' | head -n 1); printf '%%s\\n' \"$pkg\"; openclaw plugins install \"$pkg\"; rm -rf \"$workdir\"",
openclawPluginPackageTmpDir,
pluginID,
spec,
)
}
func resolvePluginMeta(pluginType string) (string, string, error) {
switch pluginType {
case "qqbot":
return "@tencent-connect/openclaw-qqbot@latest", "openclaw-qqbot", nil
case "wecom":
return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
case "dingtalk":
return "@dingtalk-real-ai/dingtalk-connector", "dingtalk-connector", nil
case "weixin":
return "@tencent-weixin/openclaw-weixin", "openclaw-weixin", nil
default:
return "", "", fmt.Errorf("unsupported plugin type")
}
}
func checkPluginInstalled(containerName, pluginType string) (bool, error) {
_, pluginDir, err := resolvePluginMeta(pluginType)
if err != nil {
return false, err
}
if strings.TrimSpace(containerName) == "" {
return false, buserr.New("ErrRecordNotFound")
}
pluginPath := path.Join(openclawPluginBaseDir, pluginDir)
mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil {
return false, nil
}
return true, nil
}
+244
View File
@@ -0,0 +1,244 @@
package service
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
const (
openclawCronJobsRelativePath = "data/conf/cron/jobs.json"
openclawSessionStoreRelativeGlob = "data/conf/agents/*/sessions/sessions.json"
openclawSessionFilesRelativeGlob = "data/conf/agents/*/sessions/*.jsonl"
)
func (a AgentService) GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error) {
agent, install, conf, err := a.loadOpenclawAgentConfig(req.AgentID)
if err != nil {
return nil, err
}
overview := &dto.AgentOverview{
Snapshot: dto.AgentOverviewSnapshot{
ContainerStatus: install.Status,
AppVersion: install.Version,
DefaultModel: extractOpenclawDefaultModel(conf),
ChannelCount: countOpenclawConfiguredChannels(conf),
},
}
if overview.Snapshot.DefaultModel == "" {
overview.Snapshot.DefaultModel = agent.Model
}
if install.Status != constant.StatusRunning {
return overview, nil
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
skillCount, err := loadOpenclawOverviewSkillStats(install.ContainerName)
if err == nil {
overview.Snapshot.SkillCount = skillCount
}
}()
go func() {
defer wg.Done()
sessionCount, err := loadOpenclawOverviewSessionCount(install.GetPath())
if err == nil {
overview.Snapshot.SessionCount = sessionCount
}
}()
go func() {
defer wg.Done()
jobCount, err := loadOpenclawOverviewJobCount(install.GetPath())
if err == nil {
overview.Snapshot.JobCount = jobCount
}
}()
wg.Wait()
return overview, nil
}
func extractOpenclawDefaultModel(conf map[string]interface{}) string {
agents, ok := conf["agents"].(map[string]interface{})
if !ok {
return ""
}
defaults, ok := agents["defaults"].(map[string]interface{})
if !ok {
return ""
}
model, ok := defaults["model"].(map[string]interface{})
if !ok {
return ""
}
primary, _ := model["primary"].(string)
return strings.TrimSpace(primary)
}
func countOpenclawConfiguredChannels(conf map[string]interface{}) int {
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return 0
}
count := 0
for _, value := range channels {
channel, ok := value.(map[string]interface{})
if !ok || len(channel) == 0 {
continue
}
count++
}
return count
}
func loadOpenclawOverviewSkillStats(containerName string) (int, error) {
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills list --json 2>&1",
30*time.Second,
containerName,
)
if err != nil {
return 0, err
}
if strings.TrimSpace(output) == "" {
return 0, nil
}
skills, err := parseOpenclawSkillsList(output)
if err != nil {
return 0, err
}
return len(skills), nil
}
func loadOpenclawOverviewSessionCount(installPath string) (int, error) {
sessionStorePaths, err := filepath.Glob(filepath.Join(installPath, openclawSessionStoreRelativeGlob))
if err != nil {
return 0, err
}
total := 0
for _, sessionStorePath := range sessionStorePaths {
count, err := loadOpenclawSessionCountFromFile(sessionStorePath)
if err != nil {
continue
}
total += count
}
if total > 0 {
return total, nil
}
sessionFiles, err := filepath.Glob(filepath.Join(installPath, openclawSessionFilesRelativeGlob))
if err != nil {
return 0, err
}
return len(sessionFiles), nil
}
func loadOpenclawSessionCountFromFile(sessionStorePath string) (int, error) {
content, err := os.ReadFile(sessionStorePath)
if err != nil {
return 0, err
}
return parseOpenclawSessionCount(string(content))
}
func loadOpenclawOverviewJobCount(installPath string) (int, error) {
content, err := os.ReadFile(filepath.Join(installPath, openclawCronJobsRelativePath))
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
return parseOpenclawCronCount(string(content))
}
func parseOpenclawSessionCount(output string) (int, error) {
if strings.TrimSpace(output) == "" {
return 0, nil
}
var payload interface{}
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return 0, err
}
return countOpenclawSessions(payload), nil
}
func parseOpenclawCronCount(output string) (int, error) {
if strings.TrimSpace(output) == "" {
return 0, nil
}
var payload interface{}
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return 0, err
}
switch value := payload.(type) {
case []interface{}:
return len(value), nil
case map[string]interface{}:
if total, ok := value["total"].(float64); ok {
return int(total), nil
}
if jobs, ok := value["jobs"].([]interface{}); ok {
return len(jobs), nil
}
return len(value), nil
default:
return 0, nil
}
}
func countOpenclawSessions(payload interface{}) int {
switch value := payload.(type) {
case []interface{}:
return len(value)
case map[string]interface{}:
if count, ok := value["count"].(float64); ok {
return int(count)
}
if sessions, ok := value["sessions"]; ok {
return countOpenclawSessions(sessions)
}
count := 0
for _, item := range value {
switch typed := item.(type) {
case map[string]interface{}:
if looksLikeOpenclawSession(typed) {
count++
}
case string:
if strings.HasSuffix(strings.TrimSpace(typed), ".jsonl") {
count++
}
}
}
return count
default:
return 0
}
}
func looksLikeOpenclawSession(item map[string]interface{}) bool {
if _, ok := item["sessionFile"]; ok {
return true
}
if _, ok := item["agentId"]; ok {
return true
}
if _, ok := item["lastActivityTs"]; ok {
return true
}
if _, ok := item["updatedAt"]; ok {
return true
}
return false
}
+256
View File
@@ -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
+13
View File
@@ -57,6 +57,10 @@ type IAppService interface {
GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
}
type appInstallHooks struct {
AfterCopyData func(appInstall *model.AppInstall) error
}
func NewIAppService() IAppService {
return &AppService{}
}
@@ -340,6 +344,10 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
}
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
return a.installWithHooks(req, executeScript, nil)
}
func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript bool, hooks *appInstallHooks) (appInstall *model.AppInstall, err error) {
if err = docker.CreateDefaultDockerNetwork(); err != nil {
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
return
@@ -537,6 +545,11 @@ func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (a
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
if hooks != nil && hooks.AfterCopyData != nil {
if err = hooks.AfterCopyData(appInstall); err != nil {
return err
}
}
if executeScript {
if err = runScript(t, appInstall, "init"); err != nil {
return err
+15 -6
View File
@@ -337,12 +337,11 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if err != nil {
return err
}
changePort := false
oldInstalled := installed
port, ok := req.Params["PANEL_APP_PORT_HTTP"]
if ok {
portN := int(math.Ceil(port.(float64)))
if portN != installed.HttpPort {
changePort = true
httpPort, err := checkPort("PANEL_APP_PORT_HTTP", req.Params)
if err != nil {
return err
@@ -428,9 +427,17 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
installed.Status = constant.StatusRunning
_ = appInstallRepo.Save(context.Background(), &installed)
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
website, _ := websiteRepo.GetFirst(websiteRepo.WithAppInstallId(installed.ID))
if changePort && website.ID != 0 && website.Status == constant.StatusRunning {
go func() {
if proxyChanged && website.ID != 0 && currentProxyErr == nil {
website.Proxy = currentProxy
if err := websiteRepo.SaveWithoutCtx(&website); err != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
}
}
if proxyChanged && website.ID != 0 && website.Status == constant.StatusRunning && currentProxyErr == nil {
go func(website model.Website, proxy string) {
nginxInstall, err := getNginxFull(&website)
if err != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
@@ -443,7 +450,6 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
return
}
server := servers[0]
proxy := fmt.Sprintf("http://127.0.0.1:%d", installed.HttpPort)
server.UpdateRootProxy([]string{proxy})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
@@ -454,7 +460,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", err).Error())
return
}
}()
}(website, currentProxy)
}
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
}
return nil
}
+1 -1
View File
@@ -802,7 +802,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
var newCompose string
if err = migrateOpenclawHTTPSUpgrade(&install, oldVersion, detail.Version); err != nil {
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
return err
}
if req.DockerCompose == "" {
+16 -5
View File
@@ -1189,7 +1189,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C
ctx := context.Background()
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
isDir := stat.Mode.IsDir()
isLink := stat.Mode&os.ModeSymlink != 0
@@ -1205,7 +1205,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C
output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
lines := strings.Split(strings.TrimSpace(output), "\n")
files := make([]dto.ContainerFileInfo, 0, len(lines))
@@ -1348,7 +1348,7 @@ func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*d
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
if stat.Mode.IsDir() {
return nil, fmt.Errorf("path %s is directory", req.Path)
@@ -1395,7 +1395,7 @@ func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return 0, err
return 0, normalizeContainerFileError(err)
}
if !stat.Mode.IsDir() {
return stat.Size, nil
@@ -1428,7 +1428,7 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
_ = cli.Close()
return nil, "", "", err
return nil, "", "", normalizeContainerFileError(err)
}
fileName := stat.Name
@@ -1474,6 +1474,17 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
}, 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 {
+29 -2
View File
@@ -135,6 +135,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap {
value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
records = append(records, value)
}
if len(req.Info) != 0 {
@@ -164,6 +165,29 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return int64(total), listItem, nil
}
func composeFileExists(workdir, configFile string) bool {
workdir = strings.TrimSpace(workdir)
configFile = strings.TrimSpace(configFile)
if configFile == "" {
return false
}
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) && workdir != "" {
file = filepath.Join(workdir, file)
}
file = filepath.Clean(file)
info, err := os.Stat(file)
if err == nil && !info.IsDir() {
return true
}
}
return false
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if cmd.CheckIllegal(req.Path) {
return false, buserr.New("ErrCmdIllegal")
@@ -383,8 +407,11 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
}
func removeContainerForCompose(composeName, composePath string) error {
if stdout, err := compose.Operate(composePath, "down"); err != nil {
return errors.New(stdout)
if _, err := os.Stat(composePath); err == nil {
if stdout, err := compose.Operate(composePath, "down"); err != nil {
return errors.New(stdout)
}
return nil
}
var options container.ListOptions
options.All = true
+6 -5
View File
@@ -12,11 +12,12 @@ var (
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
@@ -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 {
+62
View File
@@ -3,6 +3,8 @@ package service
import (
"encoding/base64"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -12,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
"github.com/jinzhu/copier"
)
@@ -19,7 +22,9 @@ type SettingService struct{}
type ISettingService interface {
GetSettingInfo() (*dto.SettingInfo, error)
GetTerminalAIInfo() (*dto.TerminalAIInfo, error)
Update(key, value string) error
UpdateTerminalAI(req dto.TerminalAIInfo) error
TestConnByInfo(req dto.SSHConnData) bool
SaveConnInfo(req dto.SSHConnData) error
@@ -57,10 +62,67 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
return &info, err
}
func (u *SettingService) GetTerminalAIInfo() (*dto.TerminalAIInfo, error) {
info := &dto.TerminalAIInfo{
AIStatus: constant.StatusDisable,
AIAccountID: "",
AIPrefix: constant.DefaultTerminalAIPrefix,
AIRiskCommands: "[]",
AIRiskCommandsDefault: constant.DefaultTerminalAIRiskCommands,
}
if value, err := settingRepo.GetValueByKey("AIStatus"); err == nil && value != "" {
info.AIStatus = value
}
if value, err := settingRepo.GetValueByKey("AIAccountID"); err == nil {
info.AIAccountID = value
}
if value, err := settingRepo.GetValueByKey("AIPrefix"); err == nil {
info.AIPrefix = value
}
if value, err := settingRepo.GetValueByKey("AIRiskCommands"); err == nil && value != "" {
info.AIRiskCommands = value
}
return info, nil
}
func (u *SettingService) Update(key, value string) error {
return settingRepo.UpdateOrCreate(key, value)
}
func (u *SettingService) UpdateTerminalAI(req dto.TerminalAIInfo) error {
if strings.EqualFold(strings.TrimSpace(req.AIStatus), constant.StatusEnable) {
accountID, err := strconv.ParseUint(strings.TrimSpace(req.AIAccountID), 10, 64)
if err != nil || accountID == 0 {
return buserr.New("ErrAgentAccountIDRequired")
}
currentStatus, _ := settingRepo.GetValueByKey("AIStatus")
currentAccountID, _ := settingRepo.GetValueByKey("AIAccountID")
needValidate := !strings.EqualFold(strings.TrimSpace(currentStatus), constant.StatusEnable) ||
strings.TrimSpace(currentAccountID) != strings.TrimSpace(req.AIAccountID)
if needValidate {
if err := terminalai.ValidateTerminalAccount(uint(accountID)); err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
}
}
if err := settingRepo.UpdateOrCreate("AIStatus", req.AIStatus); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIAccountID", req.AIAccountID); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIPrefix", req.AIPrefix); err != nil {
return err
}
if err := settingRepo.UpdateOrCreate("AIRiskCommands", req.AIRiskCommands); err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
return nil
}
func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
+15 -20
View File
@@ -389,7 +389,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
appInstall = install
website.AppInstallID = install.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
website.Proxy, err = getAppInstallProxyPass(appInstall)
if err != nil {
return err
}
} else {
var install model.AppInstall
install, err = appInstallRepo.GetFirst(repo.WithByID(create.AppInstallID))
@@ -399,7 +402,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
configApp := func(t *task.Task) error {
appInstall = &install
website.AppInstallID = appInstall.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
website.Proxy, err = getAppInstallProxyPass(appInstall)
if err != nil {
return err
}
return nil
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigApp"), configApp, nil)
@@ -1488,23 +1494,11 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
newBlock.AppendDirectives(ifDir)
}
if website.Type == constant.Deployment {
newBlock.AppendDirectives(
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"Host", "$host"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Real-IP", "$remote_addr"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Forwarded-For", "$proxy_add_x_forwarded_for"},
},
&components.Directive{
Name: "proxy_pass",
Parameters: []string{fmt.Sprintf("http://%s", website.Proxy)},
})
proxyDirectives := getRootProxyDirectives(website.Proxy)
if len(proxyDirectives) == 0 {
return errors.New("failed to build deployment proxy directives")
}
newBlock.AppendDirectives(proxyDirectives...)
}
newDirective.Block = newBlock
index := -1
@@ -1750,7 +1744,7 @@ func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias)
nginxInclude := getWebsiteRedirectInclude(website)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return
}
@@ -2328,6 +2322,7 @@ func (w WebsiteService) ExecComposer(req request.ExecComposerReq) error {
} else {
command = req.ExtCommand
}
command = strings.TrimSpace(command)
resourceName := fmt.Sprintf("composer %s", command)
composerTask, err := task.NewTaskWithOps(resourceName, task.TaskExec, req.Command, req.TaskID, website.ID)
if err != nil {
+10
View File
@@ -115,6 +115,13 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
return
}
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
@@ -329,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
+64 -6
View File
@@ -51,6 +51,58 @@ 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})
@@ -281,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)
@@ -303,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))
@@ -1001,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 {
@@ -1033,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})
@@ -1049,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)
@@ -1069,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:
+5
View File
@@ -0,0 +1,5 @@
package constant
const DefaultTerminalAIPrefix = "@ai"
const DefaultTerminalAIRiskCommands = "[\"rm\",\"mkfs\",\"dd if=\",\"dd of=/dev/\",\"wipefs\",\"fdisk\",\"parted\",\"sfdisk\",\"shred\",\"curl | sh\",\"wget | sh\",\"chmod -R 777 /\",\"shutdown\",\"reboot\",\"poweroff\",\"init 0\",\":(){ :|:& };:\"]"
+46 -38
View File
@@ -1,14 +1,17 @@
module github.com/1Panel-dev/1Panel/agent
go 1.24.9
go 1.25.7
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go v1.55.0
github.com/compose-spec/compose-go/v2 v2.9.0
github.com/compose-spec/compose-go/v2 v2.9.1
github.com/creack/pty v1.1.24
github.com/docker/compose/v2 v2.40.2
github.com/docker/docker v28.5.1+incompatible
github.com/docker/cli v29.2.0+incompatible
github.com/docker/compose/v2 v2.40.3
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.6.0
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-gonic/gin v1.10.0
@@ -18,15 +21,15 @@ require (
github.com/go-playground/validator/v10 v10.26.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-resty/resty/v2 v2.17.0
github.com/go-sql-driver/mysql v1.8.1
github.com/go-sql-driver/mysql v1.9.3
github.com/goh-chunlin/go-onedrive v1.1.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/jackc/pgx/v5 v5.6.0
github.com/jackc/pgx/v5 v5.7.5
github.com/jinzhu/copier v0.4.0
github.com/jinzhu/gorm v1.9.16
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.18.1
github.com/klauspost/compress v1.18.2
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.69
github.com/minio/minio-go/v7 v7.0.74
@@ -36,12 +39,12 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
github.com/qiniu/go-sdk/v7 v7.21.1
github.com/qiniu/go-sdk/v7 v7.26.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.11
github.com/sirupsen/logrus v1.9.3
github.com/sirupsen/logrus v1.9.4
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.19.0
github.com/subosito/gotenv v1.6.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
@@ -102,7 +105,7 @@ require (
github.com/connesc/cipherio v0.2.1 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.10.0 // indirect
github.com/containerd/containerd/v2 v2.2.0 // indirect
github.com/containerd/containerd/v2 v2.2.1 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -112,17 +115,17 @@ require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/buildx v0.29.1 // indirect
github.com/docker/cli v28.5.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/buildx v0.31.1 // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
@@ -145,7 +148,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/h2non/filetype v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -155,8 +158,8 @@ require (
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -167,18 +170,19 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/buildkit v0.25.1 // indirect
github.com/moby/buildkit v0.27.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -188,7 +192,7 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -206,13 +210,15 @@ require (
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/sigstore v1.10.0 // indirect
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
@@ -222,7 +228,7 @@ require (
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
@@ -232,34 +238,36 @@ require (
github.com/volcengine/volc-sdk-golang v1.0.230 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.3.3 // indirect
modernc.org/libc v1.66.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+100 -19
View File
@@ -38,7 +38,6 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -117,6 +116,7 @@ github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7
github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
@@ -215,16 +215,21 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM=
github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs=
github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY=
github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8=
github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE=
github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=
github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -237,13 +242,17 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg=
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM=
github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE=
github.com/containerd/nydus-snapshotter v0.15.10 h1:hphjuKOqSHLGznNJiAvmsOWkdu4qFXjf4DzGrWSuIsM=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4=
github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
@@ -255,7 +264,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -270,16 +278,28 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA=
github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI=
github.com/docker/buildx v0.30.1 h1:3vthfaTQOLt5QfN2nl7rKuPLUvx69nL5ZikFIXp//c8=
github.com/docker/buildx v0.30.1/go.mod h1:8nwT0V6UNYNo9rXq6WO/BQd9KrJ0JYcY/QX6x0y1Oro=
github.com/docker/buildx v0.31.1 h1:zbvbrb9nxBNVV8nnI33f2F+4aAZBA1gY+AmeBFflMqY=
github.com/docker/buildx v0.31.1/go.mod h1:SD+jYLnt3S4SXqohVtV+8z+dihnOgwMJ8t+bLQvsaCk=
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/compose/v2 v2.40.2 h1:h2bDBJkOuqmj93XvT2oI0ArPQonE0lGtWiILXdiXvbA=
github.com/docker/compose/v2 v2.40.2/go.mod h1:CbSJpKGw20LInVsPjglZ8z7Squ3OBQOD7Ux5nkjGfIU=
github.com/docker/compose/v2 v2.40.3 h1:XeYkQu1svDtyfZPv5nTwFryQ25ZJMkIlc4pz9HalMPI=
github.com/docker/compose/v2 v2.40.3/go.mod h1:iNY1tvoHTyN3C3QHCuWAgj3OjR2T6mGkk/qxfbBF/4M=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -316,6 +336,8 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
@@ -333,6 +355,8 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
@@ -379,15 +403,12 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
@@ -397,6 +418,8 @@ github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2m
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
@@ -407,7 +430,6 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ=
github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -489,6 +511,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -515,6 +538,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
@@ -556,7 +581,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 h1:uia+R3K1izQRGpxTV+bS4q3/ueMW7ProAMWqM6OlqOU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
@@ -567,10 +591,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
@@ -615,6 +645,8 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -631,7 +663,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -639,7 +670,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
@@ -648,14 +678,14 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f h1:B0OD7nYl2FPQEVrw8g2uyc1lGEzNbvrKh7fspGZcbvY=
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
@@ -702,12 +732,22 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8=
github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
github.com/moby/buildkit v0.26.3 h1:D+ruZVAk/3ipRq5XRxBH9/DIFpRjSlTtMbghT5gQP9g=
github.com/moby/buildkit v0.26.3/go.mod h1:4T4wJzQS4kYWIfFRjsbJry4QoxDBjK+UGOEOs1izL7w=
github.com/moby/buildkit v0.27.0 h1:1gtNaMcVE0XXCZrybC32L79A7Ga1JeB7V3PfpCt1bDc=
github.com/moby/buildkit v0.27.0/go.mod h1:4STUkNc5t1nf03HS+01UmI2X6FdfOI3XaKt9QNoTsms=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@@ -735,6 +775,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -787,8 +829,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
@@ -808,7 +852,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -853,22 +896,21 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.21.1 h1:D/IjVOlg5pTw0jeDjqTo6H5QM73Obb1AYfPOHmIFN+Q=
github.com/qiniu/go-sdk/v7 v7.21.1/go.mod h1:8EM2awITynlem2VML2dXGHkMYP2UyECsGLOdp6yMpco=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/qiniu/go-sdk/v7 v7.26.4 h1:D/k6cPbhjKvSx9hVBQSh97GuPu1ZtM3OLPCclSpusjo=
github.com/qiniu/go-sdk/v7 v7.26.4/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
@@ -886,18 +928,26 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sigstore/sigstore v1.10.0 h1:lQrmdzqlR8p9SCfWIpFoGUqdXEzJSZT2X+lTXOMPaQI=
github.com/sigstore/sigstore v1.10.0/go.mod h1:Ygq+L/y9Bm3YnjpJTlQrOk/gXyrjkpn3/AEJpmk1n9Y=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 h1:94NLPmq4bxvdmslzcG670IOkrlS98CGpmob8cjpFHuI=
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7/go.mod h1:4r/PNX0G7uzkLpc3PSdYs5E2k4bWEJNXTK6kwAyw9TM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -907,12 +957,15 @@ github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIK
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -964,6 +1017,8 @@ github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK5
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
@@ -983,6 +1038,7 @@ github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNW
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/volcengine/volc-sdk-golang v1.0.230 h1:84/MOF0zUPtAHt3e1+MsFq5qrnQRC+e3XzTUwIOzZxw=
github.com/volcengine/volc-sdk-golang v1.0.230/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -1008,6 +1064,8 @@ go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsX
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1020,20 +1078,33 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
@@ -1045,6 +1116,8 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -1058,6 +1131,7 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
@@ -1082,7 +1156,6 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -1113,6 +1186,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1143,6 +1218,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1477,6 +1554,8 @@ google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOs
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM=
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1514,6 +1593,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+23 -3
View File
@@ -27,13 +27,18 @@ var langFiles = map[string]string{
}
func GetMsgWithMap(key string, maps map[string]interface{}) string {
return GetMsgWithMapAndLang("", key, maps)
}
func GetMsgWithMapAndLang(lang string, key string, maps map[string]interface{}) string {
var content string
localizer := newLocalizer(lang)
if maps == nil {
content, _ = global.I18n.Localize(&i18n.LocalizeConfig{
content, _ = localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
})
} else {
content, _ = global.I18n.Localize(&i18n.LocalizeConfig{
content, _ = localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: maps,
})
@@ -78,7 +83,11 @@ func GetErrMsg(key string, maps map[string]interface{}) string {
}
func GetMsgByKey(key string) string {
content, _ := global.I18n.Localize(&i18n.LocalizeConfig{
return GetMsgByKeyAndLang("", key)
}
func GetMsgByKeyAndLang(lang string, key string) string {
content, _ := newLocalizer(lang).Localize(&i18n.LocalizeConfig{
MessageID: key,
})
return content
@@ -151,6 +160,17 @@ func Init() {
global.I18n = i18n.NewLocalizer(bundle, lang)
}
func newLocalizer(lang string) *i18n.Localizer {
lang = strings.TrimSpace(lang)
if lang == "" {
if global.I18n != nil {
return global.I18n
}
lang = GetLanguageFromDB()
}
return i18n.NewLocalizer(bundle, lang)
}
func GetLanguageFromDB() string {
if global.DB == nil {
return "en"
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Bad request: {{ .detail }}'
ErrTokenParse: 'Token error: {{ .detail }}'
ErrInitialPassword: 'Original password is incorrect'
ErrInternalServer: 'Internal server error: {{ .detail }}'
ErrWgetRemoteFailed: 'Remote download failed or URL is unreachable: {{ .detail }}'
ErrWgetInvalidContentType: 'Remote URL did not return a file (possibly an error page), {{ .detail }}'
ErrRecordExist: 'Record already exists'
ErrRecordNotFound: 'Record not found'
ErrStructTransform: 'Type conversion failed: {{ .err }}'
@@ -44,7 +46,10 @@ ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified'
ErrAgentProviderMismatch: 'Agent provider mismatch'
ErrAgentAccountModelsRequired: 'Please configure at least one model'
ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation'
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
ErrAgentBaseURLRequired: 'Base URL is required'
ErrAgentApiKeyRequired: 'API key is required'
ErrAgentComposeRequired: 'Compose content is required'
@@ -562,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Solicitud inválida: {{ .detail }}'
ErrTokenParse: 'Token inválido: {{ .detail }}'
ErrInitialPassword: 'Contraseña original incorrecta'
ErrInternalServer: 'Error interno: {{ .detail }}'
ErrWgetRemoteFailed: 'La descarga remota falló o la URL no es accesible: {{ .detail }}'
ErrWgetInvalidContentType: 'La URL remota no devolvió un archivo (posiblemente una página de error), {{ .detail }}'
ErrRecordExist: 'Registro ya existe'
ErrRecordNotFound: 'Registro no encontrado'
ErrStructTransform: 'Error de conversión: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
ErrAgentProviderMismatch: 'Proveedor de agente no coincide'
ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta'
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base requerida'
ErrAgentApiKeyRequired: 'Clave API requerida'
ErrAgentComposeRequired: 'Contenido compose requerido'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '不正なリクエスト: {{ .detail }}'
ErrTokenParse: 'トークンエラー: {{ .detail }}'
ErrInitialPassword: '元のパスワードが違います'
ErrInternalServer: '内部エラー: {{ .detail }}'
ErrWgetRemoteFailed: 'リモートのダウンロード URL が無効か、到達できません:{{ .detail }}'
ErrWgetInvalidContentType: 'リモート URL がファイルではなくエラーページ等を返しました、{{ .detail }}'
ErrRecordExist: 'レコードが既に存在します'
ErrRecordNotFound: 'レコードが見つかりません'
ErrStructTransform: '型変換エラー: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です'
ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
ErrAgentBaseURLRequired: 'ベースURLが必要です'
ErrAgentApiKeyRequired: 'APIキーが必要です'
ErrAgentComposeRequired: 'Composeが必要です'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '잘못된 요청: {{ .detail }}'
ErrTokenParse: '토큰 오류: {{ .detail }}'
ErrInitialPassword: '원래 비밀번호가 틀립니다'
ErrInternalServer: '서버 오류: {{ .detail }}'
ErrWgetRemoteFailed: '원격 다운로드 주소가 잘못되었거나 접근할 수 없습니다: {{ .detail }}'
ErrWgetInvalidContentType: '원격 주소가 파일이 아닌 콘텐츠를 반환했습니다 (오류 페이지일 수 있음), {{ .detail }}'
ErrRecordExist: '레코드가 이미 있습니다'
ErrRecordNotFound: '레코드를 찾을 수 없습니다'
ErrStructTransform: '형 변환 실패: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
ErrAgentProviderMismatch: '에이전트 공급자 불일치'
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
ErrAgentBaseURLRequired: '기본 URL 필요'
ErrAgentApiKeyRequired: 'API 키 필요'
ErrAgentComposeRequired: 'Compose 내용 필요'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Permintaan tidak sah: {{ .detail }}'
ErrTokenParse: 'Token tidak sah: {{ .detail }}'
ErrInitialPassword: 'Kata laluan asal salah'
ErrInternalServer: 'Ralat pelayan: {{ .detail }}'
ErrWgetRemoteFailed: 'Muat turun jauh gagal atau URL tidak boleh diakses: {{ .detail }}'
ErrWgetInvalidContentType: 'URL jauh tidak mengembalikan fail (mungkin halaman ralat), {{ .detail }}'
ErrRecordExist: 'Rekod sudah wujud'
ErrRecordNotFound: 'Rekod tidak ditemui'
ErrStructTransform: 'Ralat penukaran: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
ErrAgentProviderMismatch: 'Penyedia ejen tidak sepadan'
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun'
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
ErrAgentBaseURLRequired: 'URL asas diperlukan'
ErrAgentApiKeyRequired: 'Kunci API diperlukan'
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Requisição inválida: {{ .detail }}'
ErrTokenParse: 'Erro de token: {{ .detail }}'
ErrInitialPassword: 'Senha original incorreta'
ErrInternalServer: 'Erro interno: {{ .detail }}'
ErrWgetRemoteFailed: 'Falha no download remoto ou URL inacessível: {{ .detail }}'
ErrWgetInvalidContentType: 'A URL remota não retornou um arquivo (possivelmente uma página de erro), {{ .detail }}'
ErrRecordExist: 'Registro já existe'
ErrRecordNotFound: 'Registro não encontrado'
ErrStructTransform: 'Erro de conversão: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
ErrAgentProviderMismatch: 'Provedor de agente diferente'
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta'
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base obrigatória'
ErrAgentApiKeyRequired: 'Chave API obrigatória'
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Неверный запрос: {{ .detail }}'
ErrTokenParse: 'Ошибка токена: {{ .detail }}'
ErrInitialPassword: 'Исходный пароль неверен'
ErrInternalServer: 'Внутренняя ошибка: {{ .detail }}'
ErrWgetRemoteFailed: 'Неверный или недоступный URL удалённой загрузки: {{ .detail }}'
ErrWgetInvalidContentType: 'Удалённый сервер вернул не файл (возможно, страницу ошибки), {{ .detail }}'
ErrRecordExist: 'Запись уже существует'
ErrRecordNotFound: 'Запись не найдена'
ErrStructTransform: 'Ошибка преобразования: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступн
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
ErrAgentProviderMismatch: 'Провайдер не совпадает'
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
ErrAgentBaseURLRequired: 'Нужен базовый URL'
ErrAgentApiKeyRequired: 'Нужен API-ключ'
ErrAgentComposeRequired: 'Нужен compose'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: 'İstek geçersiz: {{ .detail }}'
ErrTokenParse: 'Token hatası: {{ .detail }}'
ErrInitialPassword: 'Orijinal şifre yanlış'
ErrInternalServer: 'Sunucu hatası: {{ .detail }}'
ErrWgetRemoteFailed: 'Uzak indirme başarısız veya URL erişilemiyor: {{ .detail }}'
ErrWgetInvalidContentType: 'Uzak URL bir dosya değil, muhtemelen bir hata sayfası döndürdü, {{ .detail }}'
ErrRecordExist: 'Kayıt zaten var'
ErrRecordNotFound: 'Kayıt bulunamadı'
ErrStructTransform: 'Dönüşüm hatası: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
ErrAgentProviderMismatch: 'Ajans sağlayıcısı eşleşmiyor'
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir'
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}'
ErrAgentBaseURLRequired: 'Temel URL gerekli'
ErrAgentApiKeyRequired: 'API anahtarı gerekli'
ErrAgentComposeRequired: 'Compose içeriği gerekli'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: '請求參數錯誤: {{ .detail }}'
ErrTokenParse: 'Token 產生錯誤: {{ .detail }}'
ErrInitialPassword: '原密碼錯誤'
ErrInternalServer: '服務內部錯誤: {{ .detail }}'
ErrWgetRemoteFailed: '遠端下載網址無效或無法存取:{{ .detail }}'
ErrWgetInvalidContentType: '遠端網址未回傳檔案內容(可能是錯誤頁),{{ .detail }}'
ErrRecordExist: '記錄已存在'
ErrRecordNotFound: '記錄未能找到'
ErrStructTransform: '型別轉換失敗: {{ .err }}'
@@ -39,7 +41,10 @@ ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }}
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
ErrAgentProviderMismatch: '帳號提供商不符,請重試'
ErrAgentAccountModelsRequired: '請至少配置一個模型'
ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
@@ -504,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 }}'
+12 -1
View File
@@ -2,6 +2,8 @@ ErrInvalidParams: "参数错误: {{ .detail }}"
ErrTokenParse: "Token 错误: {{ .detail }}"
ErrInitialPassword: "原密码错误"
ErrInternalServer: "服务错误: {{ .detail }}"
ErrWgetRemoteFailed: "远程下载地址无效或无法访问:{{ .detail }}"
ErrWgetInvalidContentType: "远程地址返回的不是文件内容(可能是错误页),{{ .detail }}"
ErrRecordExist: "记录已存在"
ErrRecordNotFound: "记录不存在"
ErrStructTransform: "类型转换失败: {{ .err }}"
@@ -44,7 +46,10 @@ ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证"
ErrAgentProviderMismatch: "账号提供商不匹配"
ErrAgentAccountModelsRequired: "请至少配置一个模型"
ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
ErrAgentBaseURLRequired: "Base URL 不能为空"
ErrAgentApiKeyRequired: "API Key 不能为空"
ErrAgentComposeRequired: "自定义编排内容不能为空"
@@ -562,3 +567,9 @@ PartitionDiskErr: "分区失败,{{ .err }}"
FormatDiskErr: "格式化磁盘失败,{{ .err }}"
MountDiskErr: "挂载磁盘失败,{{ .err }}"
XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs"
# terminal
TerminalAIBlockedRiskyCommand: "已拦截风险命令:{{ .command }}"
TerminalAIThinking: "AI 正在思考..."
TerminalAIReadyToExecute: "思考完成,请回车执行(耗时 {{ .duration }},token:{{ .tokens }})"
TerminalAIRequestFailed: "AI 请求失败:{{ .err }}"
+5 -1
View File
@@ -65,7 +65,6 @@ func InitAgentDB() {
migrations.AddCronjobArgs,
migrations.AddWebsiteAcmeAccountColumn,
migrations.AddAgentTables,
migrations.MigrateOpenclawAgents,
migrations.AddAgentCustomModelFields,
migrations.AddAppInstallSortOrder,
migrations.AddAgentAccountRememberAPIKey,
@@ -73,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)
+165 -109
View File
@@ -52,6 +52,7 @@ var AddTable = &gormigrate.Migration{
&model.DatabasePostgresql{},
&model.Favorite{},
&model.Firewall{},
&model.Host{},
&model.Ftp{},
&model.ImageRepo{},
&model.ScriptLibrary{},
@@ -642,7 +643,7 @@ var AddAgentQuickJump = &gormigrate.Migration{
return tx.Create(&model.QuickJump{
Name: "Agent",
Title: "aiTools.agents.agents",
Title: "aiTools.agents.agent",
Recommend: 1,
IsShow: true,
Router: "/ai/agents/agent",
@@ -896,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 {
@@ -1095,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")
}
+14
View File
@@ -45,10 +45,15 @@ 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)
@@ -59,6 +64,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/channel/discord/update", baseApi.UpdateAgentDiscordConfig)
aiToolsRouter.POST("/agents/channel/wecom/get", baseApi.GetAgentWecomConfig)
aiToolsRouter.POST("/agents/channel/wecom/update", baseApi.UpdateAgentWecomConfig)
aiToolsRouter.POST("/agents/channel/dingtalk/get", baseApi.GetAgentDingTalkConfig)
aiToolsRouter.POST("/agents/channel/dingtalk/update", baseApi.UpdateAgentDingTalkConfig)
aiToolsRouter.POST("/agents/channel/weixin/login", baseApi.LoginAgentWeixinChannel)
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
@@ -67,6 +75,12 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
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)
}
}
+1
View File
@@ -33,6 +33,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles)
fileRouter.POST("/rename", baseApi.ChangeFileName)
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/wget/stop", baseApi.StopWget)
fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
+10
View File
@@ -11,6 +11,16 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi
{
hostRouter.POST("", baseApi.CreateHost)
hostRouter.POST("/info", baseApi.GetHostByID)
hostRouter.POST("/del", baseApi.DeleteHost)
hostRouter.POST("/update", baseApi.UpdateHost)
hostRouter.POST("/update/group", baseApi.UpdateHostGroup)
hostRouter.POST("/search", baseApi.SearchHost)
hostRouter.POST("/tree", baseApi.HostTree)
hostRouter.POST("/test/byinfo", baseApi.TestByInfo)
hostRouter.POST("/test/byid", baseApi.TestByID)
hostRouter.POST("/firewall/base", baseApi.LoadFirewallBaseInfo)
hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
+2
View File
@@ -12,8 +12,10 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
settingRouter.POST("/search", baseApi.GetSettingInfo)
settingRouter.POST("/terminal/ai/search", baseApi.GetTerminalAISettingInfo)
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.POST("/terminal/ai/update", baseApi.UpdateTerminalAISetting)
settingRouter.GET("/get/:key", baseApi.GetSettingByKey)
settingRouter.POST("/description/save", baseApi.SaveDescription)
-1
View File
@@ -374,7 +374,6 @@ func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) {
Username: auth.Username,
Password: auth.Password,
Auth: auth.Auth,
Email: auth.Email,
ServerAddress: auth.ServerAddress,
IdentityToken: auth.IdentityToken,
RegistryToken: auth.RegistryToken,
+188 -21
View File
@@ -17,6 +17,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -333,6 +334,17 @@ func (f FileOp) Rename(oldName string, newName string) error {
return f.Fs.Rename(oldName, newName)
}
type downloadTask struct {
resp *http.Response
file *os.File
dst string
}
var (
downloadMu sync.Mutex
downloadTasks = make(map[string]*downloadTask)
)
type WriteCounter struct {
Total uint64
Written uint64
@@ -382,37 +394,83 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
}
}
defer client.CloseIdleConnections()
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil
return buserr.WithDetail("ErrWgetRemoteFailed", err.Error(), err)
}
request.Header.Set("Accept-Encoding", "identity")
resp, err := client.Do(request)
if err != nil {
global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error())
return err
return buserr.WithDetail("ErrWgetRemoteFailed", err.Error(), err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64*1024))
_ = resp.Body.Close()
global.LOG.Errorf("wget remote returned non-success status %s for url %s", resp.Status, url)
return buserr.WithDetail("ErrWgetRemoteFailed", resp.StatusCode, nil)
}
ct := strings.ToLower(resp.Header.Get("Content-Type"))
dstExt := strings.ToLower(filepath.Ext(dst))
if (strings.Contains(ct, "text/html") || strings.Contains(ct, "text/xml")) &&
dstExt != ".html" && dstExt != ".htm" && dstExt != ".xml" && dstExt != ".svg" {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64*1024))
_ = resp.Body.Close()
detail := fmt.Sprintf("Content-Type: %s", ct)
global.LOG.Errorf("wget got html/xml response for non-html file %s, url %s, %s", dst, url, detail)
return buserr.WithDetail("ErrWgetInvalidContentType", detail, nil)
}
out, err := os.Create(dst)
if err != nil {
global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error())
resp.Body.Close()
return err
}
downloadMu.Lock()
downloadTasks[key] = &downloadTask{
resp: resp,
file: out,
dst: dst,
}
downloadMu.Unlock()
go func() {
defer func() {
out.Close()
resp.Body.Close()
downloadMu.Lock()
delete(downloadTasks, key)
downloadMu.Unlock()
}()
counter := &WriteCounter{}
counter.Key = key
if resp.ContentLength > 0 {
counter.Total = uint64(resp.ContentLength)
}
counter.Name = filepath.Base(dst)
if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
if _, err := io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error())
global.CACHE.Del(counter.Key)
return
}
out.Close()
resp.Body.Close()
value := global.CACHE.Get(counter.Key)
if value == "" {
return
}
process := &Process{}
_ = json.Unmarshal([]byte(value), process)
if err := json.Unmarshal([]byte(value), process); err != nil {
return
}
process.Percent = 100
process.Name = counter.Name
process.Total = process.Written
@@ -422,6 +480,25 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
return nil
}
func CancelDownload(key string) {
downloadMu.Lock()
task, ok := downloadTasks[key]
if !ok {
downloadMu.Unlock()
return
}
dst := task.dst
downloadMu.Unlock()
_ = task.file.Close()
_ = task.resp.Body.Close()
if dst != "" {
_ = os.Remove(dst)
}
global.CACHE.Del(key)
}
func (f FileOp) DownloadFile(url, dst string) error {
resp, err := req_helper.HandleGet(url)
if err != nil {
@@ -464,7 +541,7 @@ func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
}
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
if err := cmd.RunDefaultBashC(mvCommand); err != nil {
if err := cmd.RunDefaultBashCf(mvCommand); err != nil {
return err
}
return nil
@@ -527,22 +604,22 @@ func (f FileOp) CopyAndReName(src, dst, name string, cover bool) error {
if name != "" && !cover {
dstPath = filepath.Join(dst, name)
}
return cmd.RunDefaultBashCf(`cp -rf '%s' '%s'`, src, dstPath)
return cmd.RunDefaultBashCf(`cp -rfp '%s' '%s'`, src, dstPath)
} else {
dstPath := filepath.Join(dst, name)
if cover {
dstPath = dst
}
return cmd.RunDefaultBashCf(`cp -f '%s' '%s'`, src, dstPath)
return cmd.RunDefaultBashCf(`cp -fp '%s' '%s'`, src, dstPath)
}
}
func (f FileOp) CopyDirWithNewName(src, dst, newName string) error {
if newName == "." || newName == "" {
return cmd.RunDefaultBashCf(`cp -rf '%s'/. '%s'`, src, dst)
return cmd.RunDefaultBashCf(`cp -rfp '%s'/. '%s'`, src, dst)
}
dstDir := filepath.Join(dst, newName)
return cmd.RunDefaultBashCf(`cp -rf '%s' '%s'`, src, dstDir)
return cmd.RunDefaultBashCf(`cp -rfp '%s' '%s'`, src, dstDir)
}
func (f FileOp) CopyDir(src, dst string) error {
@@ -554,7 +631,7 @@ func (f FileOp) CopyDir(src, dst string) error {
if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rf '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rfp '%s' '%s'`, src, dst+"/")
}
func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error {
@@ -567,7 +644,7 @@ func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error
return err
}
if len(excludeNames) == 0 {
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rf '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -rfp '%s' '%s'`, src, dst+"/")
}
tmpFiles, err := os.ReadDir(src)
if err != nil {
@@ -600,7 +677,7 @@ func (f FileOp) CopyDirWithExclude(src, dst string, excludeNames []string) error
func (f FileOp) CopyFile(src, dst string) error {
dst = filepath.Clean(dst) + string(filepath.Separator)
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -f '%s' '%s'`, src, dst+"/")
return cmd.NewCommandMgr(cmd.WithIgnoreExist1()).RunBashCf(`cp -fp '%s' '%s'`, src, dst+"/")
}
func (f FileOp) GetDirSize(path string) (int64, error) {
@@ -797,13 +874,18 @@ func decodeGBK(input string) (string, error) {
func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType) error {
format := getFormat(cType)
if cType == Gz {
err := f.DecompressGzFile(srcFile, dst)
if err != nil {
return err
if err := f.tryDecompressTarGz(srcFile, dst, format); err == nil {
return nil
}
return nil
return f.DecompressGzFile(srcFile, dst)
}
type dirEntry struct {
path string
modTime time.Time
}
var dirs []dirEntry
handler := func(ctx context.Context, archFile archiver.File) error {
info := archFile.FileInfo
if isIgnoreFile(archFile.Name()) {
@@ -824,6 +906,7 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil {
return err
}
dirs = append(dirs, dirEntry{path: filePath, modTime: info.ModTime()})
return nil
} else {
parentDir := path.Dir(filePath)
@@ -846,14 +929,20 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType
if _, err := io.Copy(fw, fr); err != nil {
return err
}
_ = os.Chtimes(filePath, info.ModTime(), info.ModTime())
return nil
}
input, err := f.Fs.Open(srcFile)
if err != nil {
return err
}
return format.Extract(context.Background(), input, nil, handler)
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
return err
}
for i := len(dirs) - 1; i >= 0; i-- {
_ = os.Chtimes(dirs[i].path, dirs[i].modTime, dirs[i].modTime)
}
return nil
}
func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error {
@@ -926,7 +1015,75 @@ func ZipFile(files []archiver.File, dst afero.File) error {
return nil
}
func (f FileOp) tryDecompressTarGz(srcFile string, dst string, format archiver.CompressedArchive) error {
input, err := f.Fs.Open(srcFile)
if err != nil {
return err
}
defer input.Close()
type dirEntry struct {
path string
modTime time.Time
}
var dirs []dirEntry
extracted := false
handler := func(ctx context.Context, archFile archiver.File) error {
info := archFile.FileInfo
if isIgnoreFile(archFile.Name()) {
return nil
}
filePath := filepath.Join(dst, archFile.NameInArchive)
if info.IsDir() {
if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil {
return err
}
dirs = append(dirs, dirEntry{path: filePath, modTime: info.ModTime()})
} else {
parentDir := filepath.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
return err
}
}
fr, err := archFile.Open()
if err != nil {
return err
}
defer fr.Close()
fw, err := f.Fs.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer fw.Close()
if _, err := io.Copy(fw, fr); err != nil {
return err
}
_ = os.Chtimes(filePath, info.ModTime(), info.ModTime())
}
extracted = true
return nil
}
if err := format.Extract(context.Background(), input, nil, handler); err != nil {
return err
}
if !extracted {
return fmt.Errorf("no files extracted as tar.gz")
}
for i := len(dirs) - 1; i >= 0; i-- {
_ = os.Chtimes(dirs[i].path, dirs[i].modTime, dirs[i].modTime)
}
return nil
}
func (f FileOp) DecompressGzFile(srcFile, dst string) error {
var archiveModTime time.Time
if st, err := f.Fs.Stat(srcFile); err == nil {
archiveModTime = st.ModTime()
}
in, err := f.Fs.Open(srcFile)
if err != nil {
return fmt.Errorf("open source file failed: %w", err)
@@ -939,7 +1096,13 @@ func (f FileOp) DecompressGzFile(srcFile, dst string) error {
}
defer gr.Close()
outName := strings.TrimSuffix(filepath.Base(srcFile), ".gz")
outName := ""
if gr.Name != "" {
outName = filepath.Base(gr.Name)
}
if outName == "" || outName == "." {
outName = strings.TrimSuffix(filepath.Base(srcFile), ".gz")
}
outPath := filepath.Join(dst, outName)
parentDir := filepath.Dir(outPath)
if !f.Stat(parentDir) {
@@ -958,6 +1121,10 @@ func (f FileOp) DecompressGzFile(srcFile, dst string) error {
return fmt.Errorf("copy content failed: %w", err)
}
if !archiveModTime.IsZero() {
_ = os.Chtimes(outPath, archiveModTime, archiveModTime)
}
return nil
}
+10
View File
@@ -3,6 +3,7 @@ package components
import (
"errors"
"fmt"
"strings"
)
type Server struct {
@@ -352,6 +353,7 @@ func (s *Server) UpdateRootLocation() {
}
func (s *Server) UpdateRootProxy(proxy []string) {
httpsProxy := len(proxy) > 0 && strings.HasPrefix(strings.ToLower(strings.TrimSpace(proxy[0])), "https://")
newDir := Directive{
Name: "location",
Parameters: []string{"/"},
@@ -392,6 +394,14 @@ func (s *Server) UpdateRootProxy(proxy []string) {
Name: "proxy_http_version",
Parameters: []string{"1.1"},
},
&Directive{
Name: "proxy_ssl_server_name",
Parameters: []string{map[bool]string{true: "on", false: "off"}[httpsProxy]},
},
&Directive{
Name: "proxy_ssl_name",
Parameters: []string{"$proxy_host"},
},
&Directive{
Name: "proxy_pass",
Parameters: proxy,
+823
View File
@@ -0,0 +1,823 @@
package ai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/global"
)
const (
defaultTimeout = 30 * time.Second
defaultUserAgent = "1panel-terminal-ai/1.0"
defaultAnthropicToken = 1024
)
type Client interface {
ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error)
}
type ClientConfig struct {
Provider string
BaseURL string
APIKey string
Model string
APIType string
MaxTokens int
Timeout time.Duration
HTTPClient *http.Client
}
type GeneratorConfig struct {
Provider string
BaseURL string
APIKey string
Model string
APIType string
MaxTokens int
}
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatCompletionRequest struct {
Messages []ChatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type ThinkingMode struct {
Type string `json:"type"`
}
type ChatCompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content string `json:"content"`
RawText string `json:"rawText"`
Usage ResponseUsage `json:"usage"`
}
type ResponseUsage struct {
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
TotalTokens int `json:"totalTokens"`
}
type terminalAIClient struct {
config ClientConfig
httpClient *http.Client
}
func NewClient(cfg ClientConfig) (Client, error) {
providerKey := strings.ToLower(strings.TrimSpace(cfg.Provider))
if providerKey == "" {
providerKey = "custom"
}
if strings.TrimSpace(cfg.Model) == "" {
return nil, fmt.Errorf("model is required")
}
if strings.TrimSpace(cfg.APIKey) == "" && providerKey != "ollama" {
return nil, fmt.Errorf("api key is required")
}
baseURL := normalizeBaseURL(providerKey, cfg.BaseURL)
if baseURL == "" {
return nil, fmt.Errorf("base url is required")
}
cfg.Provider = providerKey
cfg.BaseURL = baseURL
cfg.APIType = normalizeClientAPIType(providerKey, cfg.APIType)
if cfg.Timeout <= 0 {
cfg.Timeout = defaultTimeout
}
client := cfg.HTTPClient
if client == nil {
client = &http.Client{Timeout: cfg.Timeout}
}
return &terminalAIClient{
config: cfg,
httpClient: client,
}, nil
}
type ClientOption func(*ClientConfig)
func WithBaseURL(baseURL string) ClientOption {
return func(cfg *ClientConfig) {
cfg.BaseURL = baseURL
}
}
func WithTimeout(timeout time.Duration) ClientOption {
return func(cfg *ClientConfig) {
cfg.Timeout = timeout
}
}
func WithHTTPClient(client *http.Client) ClientOption {
return func(cfg *ClientConfig) {
cfg.HTTPClient = client
}
}
func (c *terminalAIClient) ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
if len(req.Messages) == 0 {
return nil, fmt.Errorf("messages are required")
}
switch c.config.APIType {
case "anthropic-messages":
return c.chatCompletionAnthropic(ctx, req)
case "gemini-generate-content":
return c.chatCompletionGemini(ctx, req)
case "openai-responses":
return c.chatCompletionOpenAIResponses(ctx, req)
default:
return c.chatCompletionOpenAI(ctx, req)
}
}
func (c *terminalAIClient) chatCompletionOpenAI(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := openAIChatCompletionRequest{
Model: normalizeModelID(c.config.Model),
Messages: req.Messages,
MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
Thinking: thinkingModeForModel(c.config.Model),
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildChatCompletionsURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp openAIChatCompletionResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractAssistantContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.PromptTokens,
CompletionTokens: completionResp.Usage.CompletionTokens,
TotalTokens: completionResp.Usage.TotalTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionOpenAIResponses(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := openAIResponsesRequest{
Model: normalizeModelID(c.config.Model),
Input: toResponsesInput(req.Messages),
MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildResponsesURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
if strings.TrimSpace(c.config.APIKey) != "" {
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp openAIResponsesResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractResponsesContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.InputTokens,
CompletionTokens: completionResp.Usage.OutputTokens,
TotalTokens: completionResp.Usage.TotalTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionAnthropic(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
system, messages := toAnthropicMessages(req.Messages)
payload := anthropicMessagesRequest{
Model: normalizeModelID(c.config.Model),
System: system,
Messages: messages,
MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens, defaultAnthropicToken),
Temperature: req.Temperature,
TopP: req.TopP,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildAnthropicMessagesURL(c.config.BaseURL), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("x-api-key", strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("anthropic-version", "2023-06-01")
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp anthropicMessagesResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractAnthropicContent(completionResp)
return &ChatCompletionResponse{
ID: completionResp.ID,
Model: firstNonEmptyString(c.config.Model, completionResp.Model),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.InputTokens,
CompletionTokens: completionResp.Usage.OutputTokens,
TotalTokens: completionResp.Usage.InputTokens + completionResp.Usage.OutputTokens,
},
}, nil
}
func (c *terminalAIClient) chatCompletionGemini(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
payload := geminiGenerateContentRequest{
Contents: toGeminiContents(req.Messages),
GenerationConfig: &geminiGenerationConfig{
MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
Temperature: req.Temperature,
TopP: req.TopP,
},
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildGeminiGenerateContentURL(c.config.BaseURL, normalizeModelID(c.config.Model)), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("x-goog-api-key", strings.TrimSpace(c.config.APIKey))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", defaultUserAgent)
respBody, err := c.do(httpReq)
if err != nil {
return nil, err
}
var completionResp geminiGenerateContentResponse
if err := json.Unmarshal(respBody, &completionResp); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
content := extractGeminiContent(completionResp)
return &ChatCompletionResponse{
Model: firstNonEmptyString(c.config.Model, completionResp.ModelVersion),
Content: content,
RawText: strings.TrimSpace(string(respBody)),
Usage: ResponseUsage{
PromptTokens: completionResp.Usage.PromptTokenCount,
CompletionTokens: completionResp.Usage.CandidatesTokenCount,
TotalTokens: completionResp.Usage.TotalTokenCount,
},
}, nil
}
func (c *terminalAIClient) do(httpReq *http.Request) ([]byte, error) {
start := time.Now()
resp, err := c.httpClient.Do(httpReq)
if err != nil {
c.logRequestCost(httpReq, 0, time.Since(start), err)
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), err)
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= http.StatusBadRequest {
providerErr := parseProviderError(resp.StatusCode, respBody)
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), providerErr)
return nil, providerErr
}
c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), nil)
return respBody, nil
}
func (c *terminalAIClient) logRequestCost(httpReq *http.Request, statusCode int, duration time.Duration, err error) {
if global.LOG == nil || httpReq == nil {
return
}
if err != nil {
global.LOG.Warnf("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s err=%v",
c.config.Provider,
c.config.Model,
c.config.APIType,
httpReq.Method,
httpReq.URL.String(),
statusCode,
duration,
err,
)
return
}
global.LOG.Infof("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s",
c.config.Provider,
c.config.Model,
c.config.APIType,
httpReq.Method,
httpReq.URL.String(),
statusCode,
duration,
)
}
type openAIChatCompletionRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Thinking *ThinkingMode `json:"thinking,omitempty"`
}
type openAIChatCompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []struct {
Message ChatMessage `json:"message"`
Text string `json:"text"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
type openAIResponsesRequest struct {
Model string `json:"model"`
Input []openAIResponsesInput `json:"input"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type openAIResponsesInput struct {
Role string `json:"role"`
Content []openAIResponsesInputPart `json:"content"`
}
type openAIResponsesInputPart struct {
Type string `json:"type"`
Text string `json:"text"`
}
type openAIResponsesResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Output []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
OutputText string `json:"output_text"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
type anthropicMessagesRequest struct {
Model string `json:"model"`
System string `json:"system,omitempty"`
Messages []anthropicMessage `json:"messages"`
MaxTokens int `json:"max_tokens"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
type anthropicMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type anthropicMessagesResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
type geminiGenerateContentRequest struct {
Contents []geminiContent `json:"contents"`
GenerationConfig *geminiGenerationConfig `json:"generationConfig,omitempty"`
}
type geminiContent struct {
Role string `json:"role,omitempty"`
Parts []geminiContentPart `json:"parts"`
}
type geminiContentPart struct {
Text string `json:"text"`
}
type geminiGenerationConfig struct {
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"topP,omitempty"`
}
type geminiGenerateContentResponse struct {
Candidates []struct {
Content geminiContent `json:"content"`
} `json:"candidates"`
ModelVersion string `json:"modelVersion"`
Usage struct {
PromptTokenCount int `json:"promptTokenCount"`
CandidatesTokenCount int `json:"candidatesTokenCount"`
TotalTokenCount int `json:"totalTokenCount"`
} `json:"usageMetadata"`
}
type openAIErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
func normalizeBaseURL(provider, rawBaseURL string) string {
baseURL := strings.TrimSpace(rawBaseURL)
if baseURL == "" {
defaultBaseURL, ok := providercatalog.DefaultBaseURL(provider)
if ok {
baseURL = defaultBaseURL
}
}
return strings.TrimRight(baseURL, "/")
}
func buildChatCompletionsURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/chat/completions") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/chat/completions"
}
switch {
case strings.HasSuffix(parsed.Path, "/v1"):
parsed.Path += "/chat/completions"
case strings.HasSuffix(parsed.Path, "/v1beta"):
parsed.Path += "/chat/completions"
default:
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/chat/completions"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildResponsesURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/responses") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/responses"
}
if strings.HasSuffix(parsed.Path, "/v1") {
parsed.Path += "/responses"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/responses"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildAnthropicMessagesURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if strings.HasSuffix(baseURL, "/messages") {
return baseURL
}
parsed, err := url.Parse(baseURL)
if err != nil {
return baseURL + "/messages"
}
if strings.HasSuffix(parsed.Path, "/v1") {
parsed.Path += "/messages"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/messages"
}
return strings.TrimRight(parsed.String(), "/")
}
func buildGeminiGenerateContentURL(baseURL, model string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
model = strings.TrimSpace(model)
if model == "" {
model = "gemini-3-flash-preview"
}
parsed, err := url.Parse(baseURL)
if err != nil {
return strings.TrimRight(baseURL, "/") + "/v1beta/models/" + model + ":generateContent"
}
if strings.Contains(parsed.Path, "/models/") && strings.HasSuffix(parsed.Path, ":generateContent") {
return parsed.String()
}
if strings.Contains(parsed.Path, "/v1beta") {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/models/" + model + ":generateContent"
} else {
parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1beta/models/" + model + ":generateContent"
}
return parsed.String()
}
func normalizeModelID(model string) string {
model = strings.TrimSpace(model)
if parts := strings.SplitN(model, "/", 2); len(parts) == 2 {
return parts[1]
}
return model
}
func thinkingModeForModel(model string) *ThinkingMode {
if strings.EqualFold(normalizeModelID(model), "kimi-k2.5") {
return &ThinkingMode{Type: "disabled"}
}
return nil
}
func normalizeClientAPIType(provider, apiType string) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "anthropic", "kimi-coding", "minimax":
return "anthropic-messages"
case "gemini":
return "gemini-generate-content"
case "ollama":
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "openai-completions" {
return trim
}
return "openai-responses"
default:
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "" {
return "openai-completions"
}
return trim
}
}
func extractAssistantContent(resp openAIChatCompletionResponse) string {
if len(resp.Choices) == 0 {
return ""
}
if content := strings.TrimSpace(resp.Choices[0].Message.Content); content != "" {
return content
}
return strings.TrimSpace(resp.Choices[0].Text)
}
func extractResponsesContent(resp openAIResponsesResponse) string {
if text := strings.TrimSpace(resp.OutputText); text != "" {
return text
}
for _, item := range resp.Output {
for _, content := range item.Content {
if strings.TrimSpace(content.Text) != "" {
return strings.TrimSpace(content.Text)
}
}
}
return ""
}
func extractAnthropicContent(resp anthropicMessagesResponse) string {
var parts []string
for _, item := range resp.Content {
if strings.TrimSpace(item.Text) == "" {
continue
}
parts = append(parts, strings.TrimSpace(item.Text))
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
func extractGeminiContent(resp geminiGenerateContentResponse) string {
if len(resp.Candidates) == 0 {
return ""
}
var parts []string
for _, part := range resp.Candidates[0].Content.Parts {
if strings.TrimSpace(part.Text) == "" {
continue
}
parts = append(parts, strings.TrimSpace(part.Text))
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
func toResponsesInput(messages []ChatMessage) []openAIResponsesInput {
result := make([]openAIResponsesInput, 0, len(messages))
for _, message := range messages {
if strings.TrimSpace(message.Content) == "" {
continue
}
result = append(result, openAIResponsesInput{
Role: normalizeRole(message.Role),
Content: []openAIResponsesInputPart{{
Type: "input_text",
Text: message.Content,
}},
})
}
return result
}
func toAnthropicMessages(messages []ChatMessage) (string, []anthropicMessage) {
var systemParts []string
result := make([]anthropicMessage, 0, len(messages))
for _, message := range messages {
content := strings.TrimSpace(message.Content)
if content == "" {
continue
}
role := normalizeRole(message.Role)
if role == "system" {
systemParts = append(systemParts, content)
continue
}
if role != "assistant" {
role = "user"
}
result = append(result, anthropicMessage{
Role: role,
Content: content,
})
}
if len(result) == 0 && len(systemParts) > 0 {
result = append(result, anthropicMessage{
Role: "user",
Content: "Generate one shell command only.",
})
}
return strings.Join(systemParts, "\n\n"), result
}
func toGeminiContents(messages []ChatMessage) []geminiContent {
result := make([]geminiContent, 0, len(messages))
for _, message := range messages {
content := strings.TrimSpace(message.Content)
if content == "" {
continue
}
role := normalizeRole(message.Role)
if role == "assistant" {
role = "model"
} else {
role = "user"
}
result = append(result, geminiContent{
Role: role,
Parts: []geminiContentPart{{
Text: content,
}},
})
}
return result
}
func normalizeRole(role string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "assistant", "model":
return "assistant"
case "system":
return "system"
default:
return "user"
}
}
func parseProviderError(statusCode int, body []byte) error {
var errResp openAIErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil && strings.TrimSpace(errResp.Error.Message) != "" {
if strings.TrimSpace(errResp.Error.Code) != "" {
return fmt.Errorf("provider returned %d: %s (%s)", statusCode, errResp.Error.Message, errResp.Error.Code)
}
return fmt.Errorf("provider returned %d: %s", statusCode, errResp.Error.Message)
}
var generic map[string]interface{}
if err := json.Unmarshal(body, &generic); err == nil {
if message := extractErrorMessage(generic); message != "" {
return fmt.Errorf("provider returned %d: %s", statusCode, message)
}
}
message := strings.TrimSpace(string(body))
if message == "" {
message = http.StatusText(statusCode)
}
if !utf8.ValidString(message) {
message = http.StatusText(statusCode)
}
return fmt.Errorf("provider returned %d: %s", statusCode, message)
}
func extractErrorMessage(value map[string]interface{}) string {
if errorValue, ok := value["error"]; ok {
switch typed := errorValue.(type) {
case string:
return strings.TrimSpace(typed)
case map[string]interface{}:
if msg, ok := typed["message"].(string); ok {
return strings.TrimSpace(msg)
}
if msg, ok := typed["status"].(string); ok {
return strings.TrimSpace(msg)
}
}
}
if msg, ok := value["message"].(string); ok {
return strings.TrimSpace(msg)
}
return ""
}
func firstPositive(values ...int) int {
for _, value := range values {
if value > 0 {
return value
}
}
return 0
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
@@ -0,0 +1,172 @@
package ai
import (
"context"
"fmt"
"strings"
)
type CommandGenerator struct {
client Client
}
type CommandGenerateRequest struct {
Input string
Shell string
WorkingDir string
OS string
RecentCommands []string
DirectoryHints []string
}
type CommandGenerateResponse struct {
Command string
Model string
Provider string
RawText string
Usage ResponseUsage
}
func NewCommandGeneratorFromConfig(cfg GeneratorConfig) (*CommandGenerator, error) {
client, err := NewClient(ClientConfig{
Provider: cfg.Provider,
BaseURL: cfg.BaseURL,
APIKey: cfg.APIKey,
Model: cfg.Model,
APIType: cfg.APIType,
MaxTokens: cfg.MaxTokens,
})
if err != nil {
return nil, err
}
return NewCommandGenerator(client)
}
func NewCommandGenerator(client Client) (*CommandGenerator, error) {
if client == nil {
return nil, fmt.Errorf("client is required")
}
return &CommandGenerator{client: client}, nil
}
func (g *CommandGenerator) Generate(ctx context.Context, req CommandGenerateRequest) (*CommandGenerateResponse, error) {
if strings.TrimSpace(req.Input) == "" {
return nil, fmt.Errorf("input is required")
}
resp, err := g.client.ChatCompletion(ctx, ChatCompletionRequest{
Messages: []ChatMessage{
{Role: "system", Content: buildCommandSystemPrompt()},
{Role: "user", Content: buildCommandUserPrompt(req)},
},
})
if err != nil {
return nil, err
}
command := sanitizeCommand(resp.Content)
if command == "" {
return nil, fmt.Errorf("model returned empty command")
}
return &CommandGenerateResponse{
Command: command,
Model: resp.Model,
Provider: providerNameFromModel(resp.Model),
RawText: resp.RawText,
Usage: resp.Usage,
}, nil
}
func buildCommandSystemPrompt() string {
return strings.Join([]string{
"You are a shell command generator.",
"Return exactly one command suitable for direct execution in the user's shell.",
"Do not include markdown, code fences, explanations, numbering, comments, or backticks.",
"If multiple commands are required, join them with shell operators in a single line.",
"Prefer safe, non-destructive commands unless the user explicitly asks for destructive behavior.",
"Preserve the user's language when filenames or arguments are ambiguous, but output only the command.",
}, "\n")
}
func buildCommandUserPrompt(req CommandGenerateRequest) string {
var sections []string
sections = append(sections, "Task:\n"+strings.TrimSpace(req.Input))
var env []string
if shell := strings.TrimSpace(req.Shell); shell != "" {
env = append(env, "Shell: "+shell)
}
if wd := strings.TrimSpace(req.WorkingDir); wd != "" {
env = append(env, "Working directory: "+wd)
}
if osName := strings.TrimSpace(req.OS); osName != "" {
env = append(env, "Operating system: "+osName)
}
if len(env) > 0 {
sections = append(sections, "Environment:\n"+strings.Join(env, "\n"))
}
if block := formatBulletBlock(req.DirectoryHints); block != "" {
sections = append(sections, "Directory hints:\n"+block)
}
if block := formatBulletBlock(req.RecentCommands); block != "" {
sections = append(sections, "Recent commands:\n"+block)
}
sections = append(sections, "Output requirement:\nReturn one shell command only.")
return strings.Join(sections, "\n\n")
}
func formatBulletBlock(values []string) string {
var lines []string
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
lines = append(lines, "- "+value)
}
return strings.Join(lines, "\n")
}
func sanitizeCommand(raw string) string {
command := strings.TrimSpace(raw)
if command == "" {
return ""
}
command = strings.TrimPrefix(command, "```sh")
command = strings.TrimPrefix(command, "```bash")
command = strings.TrimPrefix(command, "```zsh")
command = strings.TrimPrefix(command, "```shell")
command = strings.TrimPrefix(command, "```")
command = strings.TrimSuffix(command, "```")
command = strings.TrimSpace(command)
lines := strings.Split(command, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(strings.ToLower(line), "command:") {
line = strings.TrimSpace(line[len("command:"):])
}
return strings.Trim(line, "` ")
}
return ""
}
func providerNameFromModel(model string) string {
model = strings.TrimSpace(model)
if model == "" {
return ""
}
if parts := strings.SplitN(model, "/", 2); len(parts) == 2 {
return parts[0]
}
return ""
}
+200
View File
@@ -0,0 +1,200 @@
package ai
import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
var agentAccountRepo = repo.NewIAgentAccountRepo()
var agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
var terminalRuntimeVersion atomic.Uint64
type TerminalRuntimeSettings struct {
AccountID uint
Prefix string
RiskCommands []string
}
func CurrentTerminalRuntimeVersion() uint64 {
return terminalRuntimeVersion.Load()
}
func InvalidateTerminalRuntimeCache() {
terminalRuntimeVersion.Add(1)
}
func ResolveGeneratorConfig(accountID uint) (GeneratorConfig, time.Duration, error) {
account, err := loadAgentAccount(accountID)
if err != nil {
return GeneratorConfig{}, 0, err
}
provider := strings.ToLower(strings.TrimSpace(account.Provider))
if provider == "" {
return GeneratorConfig{}, 0, fmt.Errorf("agent account provider is required")
}
model, maxTokens, err := resolveAccountModelConfig(account.ID, provider)
if err != nil {
return GeneratorConfig{}, 0, err
}
baseURL := strings.TrimSpace(account.BaseURL)
if baseURL == "" {
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
baseURL = defaultURL
}
}
apiKey := strings.TrimSpace(account.APIKey)
if apiKey == "" {
apiKey = lookupProviderAPIKey(provider)
}
if apiKey == "" && provider != "ollama" {
return GeneratorConfig{}, 0, fmt.Errorf("agent account api key is required")
}
return GeneratorConfig{
Provider: provider,
BaseURL: baseURL,
APIKey: strings.TrimSpace(apiKey),
Model: model,
APIType: strings.TrimSpace(account.APIType),
MaxTokens: maxTokens,
}, 30 * time.Second, nil
}
func lookupProviderAPIKey(provider string) string {
envKey := providercatalog.EnvKey(provider)
if envKey == "" {
return ""
}
return strings.TrimSpace(os.Getenv(envKey))
}
func defaultModelForProvider(provider string) (string, int) {
meta, ok := providercatalog.Get(provider)
if !ok || len(meta.Models) == 0 {
return "", 0
}
return meta.Models[0].ID, meta.Models[0].MaxTokens
}
func resolveAccountModelConfig(accountID uint, provider string) (string, int, error) {
if accountID > 0 {
rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
if err != nil {
return "", 0, err
}
if len(rows) > 0 {
return strings.TrimSpace(rows[0].Model), rows[0].MaxTokens, nil
}
}
model, maxTokens := defaultModelForProvider(provider)
return model, maxTokens, nil
}
func ResolveGeneratorConfigFromAgentSettings() (GeneratorConfig, uint, time.Duration, error) {
status, err := loadAgentSettingValue("AIStatus")
if err != nil && !os.IsNotExist(err) {
return GeneratorConfig{}, 0, 0, err
}
if !strings.EqualFold(strings.TrimSpace(status), "Enable") {
return GeneratorConfig{}, 0, 0, os.ErrNotExist
}
accountValue, err := loadAgentSettingValue("AIAccountID")
if err != nil {
return GeneratorConfig{}, 0, 0, err
}
accountID, err := strconv.ParseUint(strings.TrimSpace(accountValue), 10, 64)
if err != nil || accountID == 0 {
return GeneratorConfig{}, 0, 0, os.ErrNotExist
}
config, timeout, err := ResolveGeneratorConfig(uint(accountID))
return config, uint(accountID), timeout, err
}
func LoadTerminalRuntimeSettings() (TerminalRuntimeSettings, GeneratorConfig, time.Duration, error) {
config, accountID, timeout, err := ResolveGeneratorConfigFromAgentSettings()
if err != nil {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
prefix, err := loadAgentSettingValue("AIPrefix")
if err != nil && !os.IsNotExist(err) {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
riskCommands, err := loadRiskCommands()
if err != nil {
return TerminalRuntimeSettings{}, GeneratorConfig{}, 0, err
}
return TerminalRuntimeSettings{
AccountID: accountID,
Prefix: strings.TrimSpace(prefix),
RiskCommands: riskCommands,
}, config, timeout, nil
}
func loadAgentAccount(accountID uint) (*model.AgentAccount, error) {
if accountID == 0 {
return nil, os.ErrNotExist
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID))
if err != nil {
return nil, err
}
return account, nil
}
func loadAgentSettingValue(key string) (string, error) {
var setting model.Setting
if err := global.DB.Where("key = ?", key).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", os.ErrNotExist
}
return "", err
}
return setting.Value, nil
}
func loadRiskCommands() ([]string, error) {
value, err := loadAgentSettingValue("AIRiskCommands")
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
if strings.TrimSpace(value) == "" {
return []string{}, nil
}
var commands []string
if err := json.Unmarshal([]byte(value), &commands); err != nil {
return nil, err
}
return normalizeRiskCommands(commands), nil
}
func normalizeRiskCommands(commands []string) []string {
seen := make(map[string]struct{}, len(commands))
result := make([]string, 0, len(commands))
for _, command := range commands {
command = strings.TrimSpace(command)
if command == "" {
continue
}
if _, ok := seen[command]; ok {
continue
}
seen[command] = struct{}{}
result = append(result, command)
}
return result
}
+38
View File
@@ -0,0 +1,38 @@
package ai
import (
"context"
"fmt"
"runtime"
"time"
)
const validateTimeout = 15 * time.Second
func ValidateTerminalAccount(accountID uint) error {
cfg, _, err := ResolveGeneratorConfig(accountID)
if err != nil {
return err
}
generator, err := NewCommandGeneratorFromConfig(cfg)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), validateTimeout)
defer cancel()
resp, err := generator.Generate(ctx, CommandGenerateRequest{
Input: "Print current directory.",
Shell: "bash",
OS: runtime.GOOS,
})
if err != nil {
return err
}
if resp == nil || resp.Command == "" {
return fmt.Errorf("terminal ai account returned empty response")
}
return nil
}
+273
View File
@@ -0,0 +1,273 @@
package terminal
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
)
const lineClearControl = 21
type aiInputInterceptor struct {
config terminalai.GeneratorConfig
timeout time.Duration
shell string
lang string
prefix string
version uint64
mu sync.Mutex
currentLine string
recentCommands []string
riskCommands []string
}
func newAIInputInterceptor(shell string, lang string) *aiInputInterceptor {
settings, config, timeout, err := terminalai.LoadTerminalRuntimeSettings()
if err != nil {
if !os.IsNotExist(err) {
global.LOG.Warnf("load terminal ai config failed: %v", err)
}
return nil
}
if strings.TrimSpace(config.APIKey) == "" && !strings.EqualFold(strings.TrimSpace(config.Provider), "ollama") {
return nil
}
return &aiInputInterceptor{
config: config,
timeout: timeout,
shell: strings.TrimSpace(shell),
lang: strings.TrimSpace(lang),
prefix: settings.Prefix,
riskCommands: append([]string(nil), settings.RiskCommands...),
version: terminalai.CurrentTerminalRuntimeVersion(),
}
}
func (i *aiInputInterceptor) refreshSettings() error {
if !i.needsRefresh() {
return nil
}
settings, config, timeout, err := terminalai.LoadTerminalRuntimeSettings()
if err != nil {
return err
}
i.mu.Lock()
defer i.mu.Unlock()
i.config = config
i.timeout = timeout
i.prefix = settings.Prefix
i.riskCommands = append([]string(nil), settings.RiskCommands...)
i.version = terminalai.CurrentTerminalRuntimeVersion()
return nil
}
func (i *aiInputInterceptor) SetCurrentLine(line string) {
if i == nil {
return
}
i.mu.Lock()
defer i.mu.Unlock()
i.currentLine = normalizeCurrentLine(line, i.prefix)
}
func (i *aiInputInterceptor) needsRefresh() bool {
if i == nil {
return false
}
currentVersion := terminalai.CurrentTerminalRuntimeVersion()
i.mu.Lock()
defer i.mu.Unlock()
return i.version != currentVersion
}
func (i *aiInputInterceptor) HandleEnter(onStart func(), onDone func(string), onError func(string)) (string, bool) {
if i == nil {
return "", false
}
if err := i.refreshSettings(); err != nil {
if !os.IsNotExist(err) {
global.LOG.Warnf("refresh terminal ai config failed: %v", err)
}
return "", false
}
i.mu.Lock()
currentLine := i.currentLine
i.currentLine = ""
recentCommands := append([]string(nil), i.recentCommands...)
i.mu.Unlock()
if !matchesAIPrefix(currentLine, i.prefix) {
if currentLine != "" {
i.pushRecentCommand(currentLine)
}
return "", false
}
prompt := strings.TrimSpace(strings.TrimPrefix(currentLine, i.prefix))
if prompt == "" {
return "", true
}
if onStart != nil {
onStart()
}
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), i.timeout)
defer cancel()
generator, err := terminalai.NewCommandGeneratorFromConfig(i.config)
if err != nil {
global.LOG.Errorf("create terminal ai generator failed: %v", err)
if onError != nil {
onError(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIRequestFailed", map[string]interface{}{
"err": err.Error(),
}))
}
return "", true
}
resp, err := generator.Generate(ctx, terminalai.CommandGenerateRequest{
Input: prompt,
Shell: firstNonEmpty(i.shell, filepath.Base(strings.TrimSpace(os.Getenv("SHELL")))),
OS: runtime.GOOS,
RecentCommands: recentCommands,
})
if err != nil {
global.LOG.Errorf("generate terminal ai command failed: %v", err)
if onError != nil {
onError(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIRequestFailed", map[string]interface{}{
"err": err.Error(),
}))
}
return "", true
}
if onDone != nil {
onDone(i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIReadyToExecute", map[string]interface{}{
"duration": formatAIDuration(time.Since(start)),
"tokens": resolveTotalTokens(resp.Usage),
}))
}
if i.isRiskCommand(resp.Command) {
return "# " + i18n.GetMsgWithMapAndLang(i.lang, "TerminalAIBlockedRiskyCommand", map[string]interface{}{
"command": resp.Command,
}), true
}
return resp.Command, strings.TrimSpace(resp.Command) != ""
}
func resolveTotalTokens(usage terminalai.ResponseUsage) int {
if usage.TotalTokens > 0 {
return usage.TotalTokens
}
return usage.PromptTokens + usage.CompletionTokens
}
func formatAIDuration(duration time.Duration) string {
if duration < time.Millisecond {
return duration.String()
}
return duration.Round(time.Millisecond).String()
}
func (i *aiInputInterceptor) pushRecentCommand(command string) {
if i == nil {
return
}
command = strings.TrimSpace(command)
if command == "" || strings.HasPrefix(command, i.prefix) {
return
}
i.mu.Lock()
defer i.mu.Unlock()
i.recentCommands = append([]string{command}, i.recentCommands...)
if len(i.recentCommands) > 8 {
i.recentCommands = i.recentCommands[:8]
}
}
func isEnterInput(data []byte) bool {
if len(data) == 1 && (data[0] == '\r' || data[0] == '\n') {
return true
}
return len(data) == 2 && data[0] == '\r' && data[1] == '\n'
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func sanitizeInputLine(raw string) string {
return strings.TrimSpace(raw)
}
func matchesAIPrefix(line, prefix string) bool {
line = strings.TrimSpace(line)
prefix = strings.TrimSpace(prefix)
if line == "" || prefix == "" {
return false
}
return line == prefix || strings.HasPrefix(line, prefix+" ")
}
func normalizeCurrentLine(raw, prefix string) string {
line := sanitizeInputLine(raw)
prefix = strings.TrimSpace(prefix)
if line == "" || prefix == "" {
return line
}
if matchesAIPrefix(line, prefix) {
return line
}
prefixIdx := strings.Index(line, prefix)
if prefixIdx < 0 {
return line
}
for _, marker := range []string{"# ", "$ ", "% ", "> "} {
if idx := strings.LastIndex(line[:prefixIdx], marker); idx >= 0 {
promptPart := strings.TrimSpace(line[:idx+len(marker)-1])
if looksLikePromptPrefix(promptPart) {
return strings.TrimSpace(line[idx+len(marker):])
}
}
}
return line
}
func looksLikePromptPrefix(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return false
}
if strings.ContainsAny(value, "'\"`") {
return false
}
return true
}
func (i *aiInputInterceptor) isRiskCommand(command string) bool {
command = strings.ToLower(strings.TrimSpace(command))
if command == "" {
return false
}
for _, riskCommand := range i.riskCommands {
riskCommand = strings.ToLower(strings.TrimSpace(riskCommand))
if riskCommand == "" {
continue
}
if strings.Contains(command, riskCommand) {
return true
}
}
return false
}
+50 -3
View File
@@ -3,9 +3,11 @@ package terminal
import (
"encoding/base64"
"encoding/json"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
@@ -14,20 +16,25 @@ type LocalWsSession struct {
slave *LocalCommand
wsConn *websocket.Conn
allowCtrlC bool
writeMutex sync.Mutex
allowCtrlC bool
writeMutex sync.Mutex
lang string
aiInterceptor *aiInputInterceptor
}
func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) {
if err := slave.ResizeTerminal(cols, rows); err != nil {
global.LOG.Errorf("ssh pty change windows size failed, err: %v", err)
}
lang := i18n.GetLanguageFromDB()
return &LocalWsSession{
slave: slave,
wsConn: wsConn,
allowCtrlC: allowCtrlC,
allowCtrlC: allowCtrlC,
lang: lang,
aiInterceptor: newAIInputInterceptor("", lang),
}, nil
}
@@ -108,6 +115,19 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
if isEnterInput(decodeBytes) {
if sws.aiInterceptor != nil {
sws.aiInterceptor.SetCurrentLine(msgObj.Line)
}
if generated, handled := sws.aiInterceptor.HandleEnter(sws.notifyAIThinking, sws.notifyAIDone, sws.notifyAIError); handled {
payload := []byte{lineClearControl}
if strings.TrimSpace(generated) != "" {
payload = append(payload, []byte(generated)...)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(payload)
continue
}
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case WsMsgHeartbeat:
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
@@ -119,6 +139,33 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
}
}
func (sws *LocalWsSession) notifyAIThinking() {
if sws == nil {
return
}
if err := sws.masterWrite([]byte("\r\n" + i18n.GetMsgByKeyAndLang(sws.lang, "TerminalAIThinking") + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai thinking message failed, err: %v", err)
}
}
func (sws *LocalWsSession) notifyAIDone(message string) {
if sws == nil || strings.TrimSpace(message) == "" {
return
}
if err := sws.masterWrite([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai done message failed, err: %v", err)
}
}
func (sws *LocalWsSession) notifyAIError(message string) {
if sws == nil || strings.TrimSpace(message) == "" {
return
}
if err := sws.masterWrite([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai error message failed, err: %v", err)
}
}
func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.slave.Write(cmdBytes); err != nil {
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
+62 -17
View File
@@ -5,10 +5,12 @@ import (
"encoding/base64"
"encoding/json"
"io"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
@@ -43,20 +45,22 @@ const (
type WsMsg struct {
Type string `json:"type"`
Data string `json:"data,omitempty"` // WsMsgCmd
Line string `json:"line,omitempty"` // WsMsgCmd
Cols int `json:"cols,omitempty"` // WsMsgResize
Rows int `json:"rows,omitempty"` // WsMsgResize
Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat
}
type LogicSshWsSession struct {
stdinPipe io.WriteCloser
comboOutput *safeBuffer
logBuff *safeBuffer
inputFilterBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
isAdmin bool
IsFlagged bool
stdinPipe io.WriteCloser
comboOutput *safeBuffer
logBuff *safeBuffer
session *ssh.Session
wsConn *websocket.Conn
lang string
isAdmin bool
IsFlagged bool
aiInterceptor *aiInputInterceptor
}
func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websocket.Conn, initCmd string) (*LogicSshWsSession, error) {
@@ -72,7 +76,6 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
comboWriter := new(safeBuffer)
logBuf := new(safeBuffer)
inputBuf := new(safeBuffer)
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
@@ -91,15 +94,17 @@ func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websock
time.Sleep(100 * time.Millisecond)
_, _ = stdinP.Write([]byte(initCmd + "\n"))
}
lang := i18n.GetLanguageFromDB()
return &LogicSshWsSession{
stdinPipe: stdinP,
comboOutput: comboWriter,
logBuff: logBuf,
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
isAdmin: true,
IsFlagged: false,
stdinPipe: stdinP,
comboOutput: comboWriter,
logBuff: logBuf,
session: sshSession,
wsConn: wsConn,
lang: lang,
isAdmin: true,
IsFlagged: false,
aiInterceptor: newAIInputInterceptor("", lang),
}, nil
}
@@ -151,6 +156,19 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
if isEnterInput(decodeBytes) {
if sws.aiInterceptor != nil {
sws.aiInterceptor.SetCurrentLine(msgObj.Line)
}
if generated, handled := sws.aiInterceptor.HandleEnter(sws.notifyAIThinking, sws.notifyAIDone, sws.notifyAIError); handled {
payload := []byte{lineClearControl}
if strings.TrimSpace(generated) != "" {
payload = append(payload, []byte(generated)...)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(payload)
continue
}
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case WsMsgHeartbeat:
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
@@ -162,6 +180,33 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
}
}
func (sws *LogicSshWsSession) notifyAIThinking() {
if sws == nil || sws.comboOutput == nil {
return
}
if _, err := sws.comboOutput.Write([]byte("\r\n" + i18n.GetMsgByKeyAndLang(sws.lang, "TerminalAIThinking") + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai thinking message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) notifyAIDone(message string) {
if sws == nil || sws.comboOutput == nil || strings.TrimSpace(message) == "" {
return
}
if _, err := sws.comboOutput.Write([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai done message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) notifyAIError(message string) {
if sws == nil || sws.comboOutput == nil || strings.TrimSpace(message) == "" {
return
}
if _, err := sws.comboOutput.Write([]byte(message + "\r\n")); err != nil {
global.LOG.Errorf("write terminal ai error message failed, err: %v", err)
}
}
func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
+1 -1
View File
@@ -144,7 +144,7 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
for _, k := range progress.Keys {
value := global.CACHE.Get(k)
if value == "" {
return nil, fmt.Errorf("get cache error,err value is nil")
continue
}
downloadProcess := &files.Process{}
_ = json.Unmarshal([]byte(value), downloadProcess)
+1 -1
View File
@@ -94,7 +94,7 @@ func (b *BaseApi) MFALogin(c *gin.Context) {
user, msgKey, err := authService.MFALogin(c, req, string(entrance))
go saveLoginLogs(c, wrapLoginErr(msgKey, err))
if msgKey == "ErrAuth" {
if msgKey == "ErrAuth" || msgKey == "ErrMFA" {
helper.BadAuth(c, msgKey, err)
return
}
-1
View File
@@ -9,7 +9,6 @@ type ApiGroup struct {
var ApiGroupApp = new(ApiGroup)
var (
hostService = service.NewIHostService()
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
-355
View File
@@ -1,355 +0,0 @@
package v2
import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/copier"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/ssh"
"github.com/1Panel-dev/1Panel/core/utils/terminal"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
// @Tags Host
// @Summary Create host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"}
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
host, err := hostService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, host)
}
// @Tags Host
// @Summary Test host conn by info
// @Accept json
// @Param request body dto.HostConnTest true "request"
// @Success 200 {boolean} status
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/test/byinfo [post]
func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
connStatus := hostService.TestByInfo(req)
helper.SuccessWithData(c, connStatus)
}
// @Tags Host
// @Summary Test host conn by host id
// @Accept json
// @Param id path integer true "request"
// @Success 200 {boolean} connStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/test/byid/:id [post]
func (b *BaseApi) TestByID(c *gin.Context) {
idParam, ok := c.Params.Get("id")
if !ok {
helper.BadRequest(c, errors.New("no such params find in request"))
return
}
intNum, err := strconv.Atoi(idParam)
if err != nil {
helper.BadRequest(c, err)
return
}
connStatus := hostService.TestLocalConn(uint(intNum))
helper.SuccessWithData(c, connStatus)
}
// @Tags Host
// @Summary Load host tree
// @Accept json
// @Param request body dto.SearchForTree true "request"
// @Success 200 {array} dto.HostTree
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/tree [post]
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := hostService.SearchForTree(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Host
// @Summary Page host
// @Accept json
// @Param request body dto.SearchPageWithGroup true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/search [post]
func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := hostService.SearchWithPage(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Host
// @Summary Delete host
// @Accept json
// @Param request body dto.OperateByIDs true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"addr","output_value":"addrs"}],"formatZH":"删除主机 [addrs]","formatEN":"delete host [addrs]"}
func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := hostService.Delete(req.IDs); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Host
// @Summary Update host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/update [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机信息 [name][addr]","formatEN":"update host [name][addr]"}
func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var err error
if len(req.Password) != 0 && req.AuthMode == "password" {
req.Password, err = hostService.EncryptHost(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.PrivateKey = ""
req.PassPhrase = ""
}
if len(req.PrivateKey) != 0 && req.AuthMode == "key" {
req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey)
if err != nil {
helper.BadRequest(c, err)
return
}
if len(req.PassPhrase) != 0 {
req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase)
if err != nil {
helper.BadRequest(c, err)
return
}
}
req.Password = ""
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["group_id"] = req.GroupID
upMap["addr"] = req.Addr
upMap["port"] = req.Port
upMap["user"] = req.User
upMap["auth_mode"] = req.AuthMode
upMap["remember_password"] = req.RememberPassword
if req.AuthMode == "password" {
upMap["password"] = req.Password
upMap["private_key"] = ""
upMap["pass_phrase"] = ""
} else {
upMap["password"] = ""
upMap["private_key"] = req.PrivateKey
upMap["pass_phrase"] = req.PassPhrase
}
upMap["description"] = req.Description
hostItem, err := hostService.Update(req.ID, upMap)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, hostItem)
}
// @Tags Host
// @Summary Update host group
// @Accept json
// @Param request body dto.ChangeHostGroup true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/update/group [post]
// @x-panel-log {"bodyKeys":["id","group"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"addr","output_value":"addr"}],"formatZH":"切换主机[addr]分组 => [group]","formatEN":"change host [addr] group => [group]"}
func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeHostGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
upMap := make(map[string]interface{})
upMap["group_id"] = req.GroupID
if _, err := hostService.Update(req.ID, upMap); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Host
// @Summary Get host info
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} dto.HostInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/info [post]
func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
info, err := hostService.GetHostByID(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, info)
}
func (b *BaseApi) WsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
id, err := strconv.Atoi(c.Query("id"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) {
return
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
host, err := service.GetHostInfo(uint(id))
if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) {
return
}
var connInfo ssh.ConnInfo
_ = copier.Copy(&connInfo, &host)
connInfo.PrivateKey = []byte(host.PrivateKey)
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
client, err := ssh.NewClient(connInfo)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
defer client.Close()
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
if wshandleError(wsConn, err) {
return
}
defer sws.Close()
quitChan := make(chan bool, 3)
sws.Start(quitChan)
go sws.Wait(quitChan)
<-quitChan
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
var upGrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 16384,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wshandleError(ws *websocket.Conn, err error) bool {
if err != nil {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
wsData, err := json.Marshal(terminal.WsMsg{
Type: terminal.WsMsgCmd,
Data: base64.StdEncoding.EncodeToString([]byte(err.Error())),
})
if err != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}"))
} else {
_ = ws.WriteMessage(websocket.TextMessage, wsData)
}
}
return true
}
return false
}
+30
View File
@@ -1,8 +1,10 @@
package v2
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
@@ -228,3 +230,31 @@ func (b *BaseApi) RunScript(c *gin.Context) {
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
var upGrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 16384,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wshandleError(ws *websocket.Conn, err error) bool {
if err != nil {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
wsData, marshalErr := json.Marshal(terminal.WsMsg{
Type: terminal.WsMsgCmd,
Data: base64.StdEncoding.EncodeToString([]byte(err.Error())),
})
if marshalErr != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}"))
} else {
_ = ws.WriteMessage(websocket.TextMessage, wsData)
}
}
return true
}
return false
}
-100
View File
@@ -1,100 +0,0 @@
package repo
import (
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/global"
"gorm.io/gorm"
)
type HostRepo struct{}
type IHostRepo interface {
Get(opts ...global.DBOption) (model.Host, error)
GetList(opts ...global.DBOption) ([]model.Host, error)
Page(limit, offset int, opts ...global.DBOption) (int64, []model.Host, error)
Create(host *model.Host) error
Update(id uint, vars map[string]interface{}) error
UpdateGroup(group, newGroup uint) error
Delete(opts ...global.DBOption) error
WithByInfo(info string) global.DBOption
WithByPort(port uint) global.DBOption
WithByUser(user string) global.DBOption
}
func NewIHostRepo() IHostRepo {
return &HostRepo{}
}
func (h *HostRepo) Get(opts ...global.DBOption) (model.Host, error) {
var host model.Host
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&host).Error
return host, err
}
func (h *HostRepo) GetList(opts ...global.DBOption) ([]model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&hosts).Error
return hosts, err
}
func (h *HostRepo) Page(page, size int, opts ...global.DBOption) (int64, []model.Host, error) {
var users []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (h *HostRepo) WithByInfo(info string) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}
}
func (h *HostRepo) WithByPort(port uint) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("port = ?", port)
}
}
func (h *HostRepo) WithByUser(user string) global.DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("user = ?", user)
}
}
func (h *HostRepo) Create(host *model.Host) error {
return global.DB.Create(host).Error
}
func (h *HostRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error
}
func (h *HostRepo) UpdateGroup(group, newGroup uint) error {
return global.DB.Model(&model.Host{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error
}
func (h *HostRepo) Delete(opts ...global.DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Host{}).Error
}
+4
View File
@@ -11,6 +11,7 @@ type IScriptRepo interface {
GetList(opts ...global.DBOption) ([]model.ScriptLibrary, error)
Create(script *model.ScriptLibrary) error
Update(id uint, vars map[string]interface{}) error
UpdateGroup(group, newGroup uint) error
Page(limit, offset int, opts ...global.DBOption) (int64, []model.ScriptLibrary, error)
Delete(opts ...global.DBOption) error
SyncAll(scripts []model.ScriptLibrary) error
@@ -63,6 +64,9 @@ func (u *ScriptRepo) Create(ScriptLibrary *model.ScriptLibrary) error {
func (u *ScriptRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.ScriptLibrary{}).Where("id = ?", id).Updates(vars).Error
}
func (u *ScriptRepo) UpdateGroup(group, newGroup uint) error {
return global.DB.Model(&model.ScriptLibrary{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error
}
func (u *ScriptRepo) Delete(opts ...global.DBOption) error {
db := global.DB
-1
View File
@@ -3,7 +3,6 @@ package service
import "github.com/1Panel-dev/1Panel/core/app/repo"
var (
hostRepo = repo.NewIHostRepo()
commandRepo = repo.NewICommandRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()
+1 -3
View File
@@ -100,10 +100,8 @@ func (u *GroupService) Delete(id uint) error {
return err
}
switch group.Type {
case "host":
err = hostRepo.UpdateGroup(id, defaultGroup.ID)
case "script":
err = hostRepo.UpdateGroup(id, defaultGroup.ID)
err = scriptRepo.UpdateGroup(id, defaultGroup.ID)
case "command":
err = commandRepo.UpdateGroup(id, defaultGroup.ID)
case "node":
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/1Panel-dev/1Panel/core
go 1.24.9
go 1.25.7
require (
github.com/1panel-dev/base64Captcha v1.3.8
+1 -1
View File
@@ -16,7 +16,7 @@ func LoadMenus() string {
{ID: "2", Disabled: true, Title: "menu.apps", IsShow: true, Label: "App-Menu", Path: "/apps/all", Sort: 200},
{ID: "3", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model", Sort: 300,
Children: []dto.ShowMenu{
{ID: "44", Disabled: false, Title: "aiTools.agents.agents", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50},
{ID: "44", Disabled: false, Title: "aiTools.agents.agent", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50},
{ID: "41", Disabled: false, Title: "aiTools.model.localModel", IsShow: true, Label: "OllamaModel", Path: "/ai/model", Sort: 100},
{ID: "42", Disabled: false, Title: "menu.mcp", IsShow: true, Label: "MCPServer", Path: "/ai/mcp", Sort: 200},
{ID: "43", Disabled: false, Title: "aiTools.gpu.gpu", IsShow: true, Label: "GPU", Path: "/ai/gpu", Sort: 300},
+1
View File
@@ -34,6 +34,7 @@ func Init() {
migrations.AddDashboardCarouselSetting,
migrations.AddEditionSetting,
migrations.UpdateAiLocalModelMenuTitle,
migrations.UpdateAiAgentsHideMenuTitle,
migrations.AddDocSourceSetting,
})
if err := m.Migrate(); err != nil {
+14 -3
View File
@@ -30,7 +30,6 @@ var AddTable = &gormigrate.Migration{
&model.Setting{},
&model.BackupAccount{},
&model.Group{},
&model.Host{},
&model.Command{},
&model.UpgradeLog{},
&model.ScriptLibrary{},
@@ -597,7 +596,7 @@ var AddAgentsMenu = &gormigrate.Migration{
return helper.AddMenu(dto.ShowMenu{
ID: "44",
Disabled: false,
Title: "aiTools.agents.agents",
Title: "aiTools.agents.agent",
IsShow: true,
Label: "Agents",
Path: "/ai/agents/agent",
@@ -768,7 +767,7 @@ var UpdateAiAgentsMenu = &gormigrate.Migration{
newItem := dto.ShowMenu{
ID: "44",
Disabled: false,
Title: "aiTools.agents.agents",
Title: "aiTools.agents.agent",
IsShow: true,
Label: "Agents",
Path: "/ai/agents/agent",
@@ -866,3 +865,15 @@ var UpdateAiLocalModelMenuTitle = &gormigrate.Migration{
).Error
},
}
var UpdateAiAgentsHideMenuTitle = &gormigrate.Migration{
ID: "20260324-update-ai-agents-hide-menu-title",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(
`UPDATE settings
SET value = REPLACE(value, 'aiTools.agents.agents', 'aiTools.agents.agent')
WHERE key = 'HideMenu'
AND value LIKE '%aiTools.agents.agents%'`,
).Error
},
}
-1
View File
@@ -7,7 +7,6 @@ func commonGroups() []CommonRouter {
&LogRouter{},
&SettingRouter{},
&CommandRouter{},
&HostRouter{},
&GroupRouter{},
&ScriptRouter{},
}
-29
View File
@@ -1,29 +0,0 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/core/app/api/v2"
"github.com/1Panel-dev/1Panel/core/middleware"
"github.com/gin-gonic/gin"
)
type HostRouter struct{}
func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts").
Use(middleware.SessionAuth()).
Use(middleware.PasswordExpired())
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/:id", baseApi.TestByID)
hostRouter.GET("/terminal", baseApi.WsSsh)
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ const i18nLocale = computed(() => {
if (globalStore.language === 'ja') return ja;
if (globalStore.language === 'ms') return ms;
if (globalStore.language === 'ru') return ru;
if (globalStore.language === 'pt-br') return ptBR;
if (globalStore.language === 'pt-BR') return ptBR;
if (globalStore.language === 'ko') return ko;
if (globalStore.language === 'tr') return tr;
if (globalStore.language === 'es-ES') return esES;
+143 -18
View File
@@ -243,14 +243,8 @@ export namespace AI {
bridgePort?: number;
allowedOrigins?: string[];
agentType: 'openclaw' | 'copaw';
provider?: string;
model?: string;
apiType?: string;
maxTokens?: number;
contextWindow?: number;
accountId?: number;
apiKey?: string;
baseURL?: string;
token?: string;
taskID: string;
advanced: boolean;
@@ -309,9 +303,60 @@ export namespace AI {
model: string;
}
export interface AgentOverviewReq {
agentId: number;
}
export interface AgentOverviewSnapshot {
containerStatus: string;
appVersion: string;
defaultModel: string;
channelCount: number;
skillCount: number;
jobCount: number;
sessionCount: number;
}
export interface AgentOverview {
snapshot: AgentOverviewSnapshot;
}
export interface AgentAccountModel {
recordId: number;
id: string;
name: string;
contextWindow: number;
maxTokens: number;
reasoning: boolean;
input: string[];
}
export interface AgentAccountModelReq {
accountId: number;
}
export interface AgentAccountModelCreateReq {
accountId: number;
model: AgentAccountModel;
}
export interface AgentAccountModelUpdateReq {
accountId: number;
model: AgentAccountModel;
}
export interface AgentAccountModelDeleteReq {
accountId: number;
recordId: number;
}
export interface ProviderModelInfo {
id: string;
name: string;
contextWindow: number;
maxTokens: number;
reasoning: boolean;
input: string[];
}
export interface ProviderInfo {
@@ -327,10 +372,8 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
models?: AgentAccountModel[];
remark: string;
}
@@ -340,10 +383,7 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
remark: string;
syncAgents: boolean;
}
@@ -363,10 +403,8 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseUrl: string;
model: string;
models: AgentAccountModel[];
apiType: string;
maxTokens: number;
contextWindow: number;
verified: boolean;
remark: string;
createdAt: string;
@@ -429,7 +467,7 @@ export namespace AI {
export interface AgentChannelPairingApproveReq {
agentId: number;
type: 'feishu' | 'telegram' | 'discord' | 'wecom';
type: 'feishu' | 'telegram' | 'discord' | 'wecom' | 'dingtalk-connector';
pairingCode: string;
}
@@ -453,6 +491,37 @@ export namespace AI {
secret: string;
}
export interface AgentDingTalkConfigReq {
agentId: number;
}
export interface AgentDingTalkConfig {
enabled: boolean;
clientId: string;
clientSecret: string;
dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
allowFrom: string[];
groupPolicy: 'open' | 'allowlist' | 'disabled';
groupAllowFrom: string[];
installed: boolean;
}
export interface AgentDingTalkConfigUpdateReq {
agentId: number;
enabled: boolean;
clientId: string;
clientSecret: string;
dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
allowFrom: string[];
groupPolicy: 'open' | 'allowlist' | 'disabled';
groupAllowFrom: string[];
}
export interface AgentWeixinLoginReq {
agentId: number;
taskID: string;
}
export interface AgentQQBotConfigReq {
agentId: number;
}
@@ -473,13 +542,13 @@ export namespace AI {
export interface AgentPluginInstallReq {
agentId: number;
type: 'qqbot' | 'wecom';
type: 'qqbot' | 'wecom' | 'dingtalk' | 'weixin';
taskID: string;
}
export interface AgentPluginCheckReq {
agentId: number;
type: 'qqbot' | 'wecom';
type: 'qqbot' | 'wecom' | 'dingtalk' | 'weixin';
}
export interface AgentPluginStatus {
@@ -527,11 +596,67 @@ export namespace AI {
export interface AgentOtherConfig {
userTimezone: string;
browserEnabled: boolean;
npmRegistry: string;
}
export interface AgentOtherConfigUpdateReq {
agentId: number;
userTimezone: string;
browserEnabled: boolean;
npmRegistry: string;
}
export interface AgentConfigFileReq {
agentId: number;
}
export interface AgentConfigFile {
content: string;
}
export interface AgentConfigFileUpdateReq {
agentId: number;
content: string;
}
export interface AgentSkillsReq {
agentId: number;
}
export interface AgentSkillSearchReq {
agentId: number;
source: 'clawhub' | 'skillhub';
keyword: string;
}
export interface AgentSkillItem {
name: string;
description: string;
source: string;
bundled: boolean;
disabled: boolean;
}
export interface AgentSkillSearchItem {
slug: string;
name: string;
description: string;
summary: string;
version: string;
source: string;
score: string;
}
export interface AgentSkillUpdateReq {
agentId: number;
name: string;
enabled: boolean;
}
export interface AgentSkillInstallReq {
agentId: number;
source: 'clawhub' | 'skillhub';
slug: string;
taskID: string;
}
}
+1
View File
@@ -335,6 +335,7 @@ export namespace Container {
runningCount: number;
configFile: string;
workdir: string;
composeFileExists: boolean;
path: string;
containers: Array<ComposeContainer>;
expand: boolean;
+7
View File
@@ -82,6 +82,13 @@ export namespace Setting {
scrollback: string;
scrollSensitivity: string;
}
export interface TerminalAIInfo {
aiStatus: string;
aiAccountId: string;
aiPrefix: string;
aiRiskCommands: string;
aiRiskCommandsDefault?: string;
}
export interface SettingUpdate {
key: string;
value: string;
+1
View File
@@ -439,6 +439,7 @@ export namespace Website {
proxyProtocol?: string;
sni?: boolean;
proxySSLName: string;
sslVerify?: boolean;
cors: boolean;
allowOrigins: string;
allowMethods: string;
+56
View File
@@ -113,6 +113,10 @@ export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => {
return http.post(`/ai/agents/model/update`, req);
};
export const getAgentOverview = (req: AI.AgentOverviewReq) => {
return http.post<AI.AgentOverview>(`/ai/agents/overview`, req);
};
export const getAgentProviders = () => {
return http.get<AI.ProviderInfo[]>(`/ai/agents/providers`);
};
@@ -129,6 +133,22 @@ export const pageAgentAccounts = (req: AI.AgentAccountSearch) => {
return http.post<ResPage<AI.AgentAccountItem>>(`/ai/agents/accounts/search`, req);
};
export const getAgentAccountModels = (req: AI.AgentAccountModelReq) => {
return http.post<AI.AgentAccountModel[]>(`/ai/agents/accounts/models`, req);
};
export const createAgentAccountModel = (req: AI.AgentAccountModelCreateReq) => {
return http.post(`/ai/agents/accounts/models/create`, req);
};
export const updateAgentAccountModel = (req: AI.AgentAccountModelUpdateReq) => {
return http.post(`/ai/agents/accounts/models/update`, req);
};
export const deleteAgentAccountModel = (req: AI.AgentAccountModelDeleteReq) => {
return http.post(`/ai/agents/accounts/models/delete`, req);
};
export const verifyAgentAccount = (req: AI.AgentAccountVerifyReq) => {
return http.post(`/ai/agents/accounts/verify`, req);
};
@@ -169,6 +189,18 @@ export const updateAgentWecomConfig = (req: AI.AgentWecomConfigUpdateReq) => {
return http.post(`/ai/agents/channel/wecom/update`, req);
};
export const getAgentDingTalkConfig = (req: AI.AgentDingTalkConfigReq) => {
return http.post<AI.AgentDingTalkConfig>(`/ai/agents/channel/dingtalk/get`, req);
};
export const updateAgentDingTalkConfig = (req: AI.AgentDingTalkConfigUpdateReq) => {
return http.post(`/ai/agents/channel/dingtalk/update`, req);
};
export const loginAgentWeixinChannel = (req: AI.AgentWeixinLoginReq) => {
return http.post(`/ai/agents/channel/weixin/login`, req);
};
export const getAgentQQBotConfig = (req: AI.AgentQQBotConfigReq) => {
return http.post<AI.AgentQQBotConfig>(`/ai/agents/channel/qqbot/get`, req);
};
@@ -201,6 +233,30 @@ export const updateAgentOtherConfig = (req: AI.AgentOtherConfigUpdateReq) => {
return http.post(`/ai/agents/other/update`, req);
};
export const getAgentConfigFile = (req: AI.AgentConfigFileReq) => {
return http.post<AI.AgentConfigFile>(`/ai/agents/config-file/get`, req);
};
export const updateAgentConfigFile = (req: AI.AgentConfigFileUpdateReq) => {
return http.post(`/ai/agents/config-file/update`, req);
};
export const listAgentSkills = (req: AI.AgentSkillsReq) => {
return http.post<AI.AgentSkillItem[]>(`/ai/agents/skills/list`, req);
};
export const searchAgentSkills = (req: AI.AgentSkillSearchReq) => {
return http.post<AI.AgentSkillSearchItem[]>(`/ai/agents/skills/search`, req);
};
export const updateAgentSkill = (req: AI.AgentSkillUpdateReq) => {
return http.post(`/ai/agents/skills/update`, req);
};
export const installAgentSkill = (req: AI.AgentSkillInstallReq) => {
return http.post(`/ai/agents/skills/install`, req);
};
export const approveAgentChannelPairing = (req: AI.AgentChannelPairingApproveReq) => {
return http.post(`/ai/agents/channel/pairing/approve`, req);
};
+4
View File
@@ -98,6 +98,10 @@ export const wgetFile = (params: File.FileWget) => {
return http.post<File.FileWgetRes>('files/wget', params);
};
export const stopWgetFile = (key: string) => {
return http.post('files/wget/stop', { key });
};
export const moveFile = (params: File.FileMove) => {
return http.post<File.File>('files/move', params, TimeoutEnum.T_5M);
};
+6
View File
@@ -73,6 +73,12 @@ export const updateAgentSetting = (param: Setting.SettingUpdate) => {
export const getAgentSettingInfo = () => {
return http.post<Setting.SettingInfo>(`/settings/search`);
};
export const getAgentTerminalAIInfo = () => {
return http.post<Setting.TerminalAIInfo>(`/settings/terminal/ai/search`);
};
export const updateAgentTerminalAIInfo = (param: Setting.TerminalAIInfo) => {
return http.post(`/settings/terminal/ai/update`, param);
};
export const getAgentSettingByKey = (key: string) => {
return http.get<string>(`/settings/get/${key}`);
};
+9 -9
View File
@@ -5,13 +5,13 @@ import { Base64 } from 'js-base64';
import { deepCopy } from '@/utils/util';
export const searchHosts = (params: Host.SearchWithPage) => {
return http.post<ResPage<Host.Host>>(`/core/hosts/search`, params);
return http.postLocalNode<ResPage<Host.Host>>(`/hosts/search`, params);
};
export const getHostByID = (id: number) => {
return http.post<Host.Host>(`/core/hosts/info`, { id: id });
return http.postLocalNode<Host.Host>(`/hosts/info`, { id: id });
};
export const getHostTree = (params: Host.ReqSearch) => {
return http.post<Array<Host.HostTree>>(`/core/hosts/tree`, params);
return http.postLocalNode<Array<Host.HostTree>>(`/hosts/tree`, params);
};
export const updateLocalConn = (param: { withReset: boolean; defaultConn: string }) => {
return http.post(`/settings/ssh/default`, param);
@@ -27,7 +27,7 @@ export const addHost = (params: Host.HostOperate) => {
if (params.isLocal) {
return http.post(`/settings/ssh`, request);
}
return http.post<Host.HostOperate>(`/core/hosts`, request);
return http.postLocalNode<Host.HostOperate>(`/hosts`, request);
};
export const testByInfo = (params: Host.HostConnTest) => {
let request = deepCopy(params) as Host.HostOperate;
@@ -40,10 +40,10 @@ export const testByInfo = (params: Host.HostConnTest) => {
if (params.isLocal) {
return http.post<boolean>(`/settings/ssh/check/info`, request);
}
return http.post<boolean>(`/core/hosts/test/byinfo`, request);
return http.postLocalNode<boolean>(`/hosts/test/byinfo`, request);
};
export const testByID = (id: number) => {
return http.post<boolean>(`/core/hosts/test/byid/${id}`);
return http.postLocalNode<boolean>(`/hosts/test/byid`, { id: id });
};
export const editHost = (params: Host.HostOperate) => {
let request = deepCopy(params) as Host.HostOperate;
@@ -53,13 +53,13 @@ export const editHost = (params: Host.HostOperate) => {
if (request.privateKey) {
request.privateKey = Base64.encode(request.privateKey);
}
return http.post(`/core/hosts/update`, request);
return http.postLocalNode(`/hosts/update`, request);
};
export const editHostGroup = (params: Host.GroupChange) => {
return http.post(`/core/hosts/update/group`, params);
return http.postLocalNode(`/hosts/update/group`, params);
};
export const deleteHost = (params: { ids: number[] }) => {
return http.post(`/core/hosts/del`, params);
return http.postLocalNode(`/hosts/del`, params);
};
// agent
+24 -4
View File
@@ -1,9 +1,9 @@
@font-face {
font-family: "iconfont"; /* Project id 4776196 */
src: url('iconfont.woff2?t=1760516880031') format('woff2'),
url('iconfont.woff?t=1760516880031') format('woff'),
url('iconfont.ttf?t=1760516880031') format('truetype'),
url('iconfont.svg?t=1760516880031#iconfont') format('svg');
src: url('iconfont.woff2?t=1773998579784') format('woff2'),
url('iconfont.woff?t=1773998579784') format('woff'),
url('iconfont.ttf?t=1773998579784') format('truetype'),
url('iconfont.svg?t=1773998579784#iconfont') format('svg');
}
.iconfont {
@@ -14,6 +14,26 @@
-moz-osx-font-smoothing: grayscale;
}
.p-gailan1:before {
content: "\e62e";
}
.p-tuichudenglu3:before {
content: "\e62d";
}
.p-renwuzhongxin1:before {
content: "\e807";
}
.p-zhuji:before {
content: "\e629";
}
.p-pcm:before {
content: "\e7b2";
}
.p-run-menu:before {
content: "\e636";
}
File diff suppressed because one or more lines are too long
@@ -5,6 +5,41 @@
"css_prefix_text": "p-",
"description": "",
"glyphs": [
{
"icon_id": "9783460",
"name": "概览",
"font_class": "gailan1",
"unicode": "e62e",
"unicode_decimal": 58926
},
{
"icon_id": "26213819",
"name": "退出登陆",
"font_class": "tuichudenglu3",
"unicode": "e62d",
"unicode_decimal": 58925
},
{
"icon_id": "32303204",
"name": "任务中心",
"font_class": "renwuzhongxin1",
"unicode": "e807",
"unicode_decimal": 59399
},
{
"icon_id": "3453887",
"name": "主机",
"font_class": "zhuji",
"unicode": "e629",
"unicode_decimal": 58921
},
{
"icon_id": "39180612",
"name": "pcm",
"font_class": "pcm",
"unicode": "e7b2",
"unicode_decimal": 59314
},
{
"icon_id": "5041257",
"name": "运行环境",
+10
View File
@@ -14,6 +14,16 @@
/>
<missing-glyph />
<glyph glyph-name="gailan1" unicode="&#58926;" d="M485.9 408.4H315.6c-93.9 0-170.3 76.4-170.3 170.3S221.7 749 315.6 749s170.3-76.4 170.3-170.3v-170.3zM315.6 705c-69.7 0-126.3-56.7-126.3-126.3S246 452.4 315.6 452.4h126.3V578.7c0 69.7-56.6 126.3-126.3 126.3z m390.8-296.6H536.1V578.7c0 93.9 76.4 170.3 170.3 170.3s170.3-76.4 170.3-170.3-76.4-170.3-170.3-170.3z m-126.3 44h126.3c69.7 0 126.3 56.7 126.3 126.3S776 705 706.4 705s-126.3-56.7-126.3-126.3v-126.3zM706.4 19c-93.9 0-170.3 76.4-170.3 170.3V359.6h170.3c93.9 0 170.3-76.4 170.3-170.3S800.3 19 706.4 19zM580.1 315.6v-126.3c0-69.7 56.7-126.3 126.3-126.3s126.3 56.7 126.3 126.3c0 69.7-56.7 126.3-126.3 126.3H580.1zM315.6 19c-93.9 0-170.3 76.4-170.3 170.3s76.4 170.3 170.3 170.3h170.3v-170.3c0-93.9-76.4-170.3-170.3-170.3z m0 296.6c-69.7 0-126.3-56.7-126.3-126.3 0-69.7 56.7-126.3 126.3-126.3s126.3 56.7 126.3 126.3V315.6H315.6z" horiz-adv-x="1024" />
<glyph glyph-name="tuichudenglu3" unicode="&#58925;" d="M0 896h1024v-1024H0zM603.204267 265.284267h-241.834667a19.012267 19.012267 0 0 0-21.026133 14.779733 31.095467 31.095467 0 0 0-0.375467 5.870933v208.213334a18.261333 18.261333 0 0 0 20.48 20.206933h242.653867V705.979733a18.056533 18.056533 0 0 0 10.922666 17.92 17.7152 17.7152 0 0 0 20.855467-4.676266c3.413333-3.413333 7.168-6.826667 10.6496-10.478934l132.676267-132.4032q85.333333-84.957867 170.3936-170.052266a18.056533 18.056533 0 0 0 0.170666-28.637867q-121.890133-121.7536-243.848533-243.4048-34.7136-34.577067-69.495467-69.051733a18.2272 18.2272 0 0 0-20.650666-5.495467 17.954133 17.954133 0 0 0-11.5712 19.012267zM457.216 721.783467v-70.144H272.384A63.658667 63.658667 0 0 1 206.916267 586.069333c0-130.6624 0.273067-261.290667-0.170667-391.953066a65.365333 65.365333 0 0 1 50.4832-64 89.736533 89.736533 0 0 1 16.418133-1.467734h183.466667v-69.768533a13.346133 13.346133 0 0 0-2.7648-0.546133c-62.600533 0-125.3376-0.648533-187.938133 0.273066a134.2464 134.2464 0 0 0-127.146667 107.52 135.953067 135.953067 0 0 0-2.491733 25.9072c-0.170667 132.027733-0.443733 264.157867 0 396.1856a130.8672 130.8672 0 0 0 75.4688 120.456534 114.449067 114.449067 0 0 0 54.613333 13.2096c62.1568 0.170667 124.2112 0 186.368 0 1.2288-0.034133 2.321067-0.1024 3.9936-0.1024z m0 0" horiz-adv-x="1024" />
<glyph glyph-name="renwuzhongxin1" unicode="&#59399;" d="M293.63 292.47l368.709 0.553 0.072-48-368.71-0.553zM293.903 479.47l246.768 0.37 0.072-48-246.768-0.37zM833.477 530.304l-0.006 0.007-222.66 222.66-7.03 7.029H271.5c-48.523 0-88-39.477-88-88v-576c0-48.523 39.477-88 88-88h481c48.523 0 88 39.477 88 88V523.281l-7.023 7.023z m-74.918 7.036H617.84V678.059l70.359-70.359 70.36-70.36zM752.5 56h-481c-22.056 0-40 17.944-40 40V672c0 22.056 17.944 40 40 40h298.34v-198.66c0-13.255 10.745-24 24-24H792.5V96c0-22.056-17.944-40-40-40z" horiz-adv-x="1024" />
<glyph glyph-name="zhuji" unicode="&#58921;" d="M747.52 844.8H327.68c-40.96 0-71.68-30.72-71.68-71.68v-783.36c0-35.84 30.72-66.56 71.68-66.56h424.96c40.96 0 71.68 30.72 71.68 71.68V773.12c-5.12 40.96-35.84 71.68-76.8 71.68zM768-5.12c0-10.24-10.24-20.48-20.48-20.48H327.68c-10.24 0-20.48 10.24-20.48 20.48V773.12c0 10.24 10.24 20.48 20.48 20.48h424.96c5.12 0 15.36-10.24 15.36-20.48v-778.24zM691.2 691.2H384c-15.36 0-25.6-10.24-25.6-25.6S368.64 640 384 640h307.2c15.36 0 25.6 10.24 25.6 25.6s-10.24 25.6-25.6 25.6z m0-102.4H384c-15.36 0-25.6-10.24-25.6-25.6s10.24-25.6 25.6-25.6h307.2c15.36 0 25.6 10.24 25.6 25.6s-10.24 25.6-25.6 25.6zM537.6 230.4c-40.96 0-76.8-35.84-76.8-76.8s35.84-76.8 76.8-76.8 76.8 35.84 76.8 76.8-35.84 76.8-76.8 76.8z m0-102.4c-15.36 0-25.6 10.24-25.6 25.6s10.24 25.6 25.6 25.6 25.6-10.24 25.6-25.6-10.24-25.6-25.6-25.6z" horiz-adv-x="1024" />
<glyph glyph-name="pcm" unicode="&#59314;" d="M165.327238 229.327238V666.672762H773.36381V554.666667h58.660571V693.345524c0 17.65181-14.336 31.98781-32.012191 31.987809H138.703238c-17.67619 0-32.01219-14.336-32.01219-32.01219v-490.642286c0-17.67619 14.336-32.01219 32.01219-32.01219h394.654476v58.660571H165.327238zM298.666667 490.666667h234.666666v-58.660572H298.666667v58.660572zM533.333333 74.654476v37.351619H298.666667v-58.684952h242.834285a31.890286 31.890286 0 0 0-8.167619 21.357714zM576 479.98781c0 17.67619 14.336 32.01219 32.01219 32.01219h277.333334c17.65181 0 31.98781-14.336 31.987809-32.01219v-405.333334c0-17.65181-14.336-31.98781-32.01219-31.987809H608.01219a31.98781 31.98781 0 0 0-32.01219 32.01219V479.963429z m58.660571-378.636191h224.012191V453.315048H634.63619v-352.012191z" horiz-adv-x="1024" />
<glyph glyph-name="run-menu" unicode="&#58934;" d="M888.875 740.25h-750c-20.625 0-37.5-16.875-37.5-37.5v-521.25c0-20.625 16.875-37.5 37.5-37.5h750c20.625 0 37.5 16.875 37.5 37.5V702.75c0 20.625-16.875 37.5-37.5 37.5z m-37.5-521.25h-675V665.25h675v-446.25zM740.75 65.25h-450c-20.625 0-37.5-16.875-37.5-37.5s16.875-37.5 37.5-37.5h450c20.625 0 37.5 16.875 37.5 37.5s-16.875 37.5-37.5 37.5zM384.5 273.375c7.5-3.75 15-5.625 22.5-5.625 7.5 0 16.875 1.875 24.375 5.625l238.125 138.75c15 9.375 22.5 24.375 22.5 41.25 0 16.875-9.375 31.875-22.5 41.25l-238.125 138.75c-15 9.375-31.875 9.375-46.875 0S362 609 362 592.125v-275.625c-1.875-18.75 7.5-33.75 22.5-43.125z m41.25 76.875v206.25l178.125-103.125-178.125-103.125z" horiz-adv-x="1024" />
<glyph glyph-name="ssh-menu" unicode="&#58984;" d="M830.068053 740.898133c-172.305067 0-315.665067 117.80096-317.412693 118.94784L494.086827 875.52l-18.568534-15.674027s-144.452267-118.94784-316.757333-118.94784H129.706667v-489.71776c1.14688-7.591253 24.958293-182.790827 355.095893-300.591786l9.885013-3.495254 9.8304 3.495254c329.64608 117.200213 354.003627 293.000533 355.150507 303.486293V740.898133h-29.600427zM494.086827 8.587947c-280.221013 103.273813-305.780053 246.033067-306.3808 246.033066V684.045653c144.50688 9.284267 262.30784 84.70528 306.3808 117.200214 44.127573-32.494933 162.52928-107.915947 306.981546-117.200214v-425.929386s-23.7568-145.65376-306.926933-249.52832zM624.667307 533.74976a130.74432 130.74432 0 0 1-130.525867 130.525867 130.74432 130.74432 0 0 1-130.58048-130.525867c0-62.09536 43.526827-113.759573 101.5808-127.085227v-244.886186h57.99936V270.9504h91.641173V328.840533h-91.641173V406.664533a130.635093 130.635093 0 0 1 101.5808 127.085227z m-203.106987 0a72.58112 72.58112 0 1 0 145.16224-0.054613 72.58112 72.58112 0 0 0-145.16224 0z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.
Binary file not shown.

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