mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: Add allowedOrigins config for openclaw (#12162)
This commit is contained in:
+13
-13
@@ -477,19 +477,19 @@ func (b *BaseApi) CheckAgentPlugin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Browser config
|
||||
// @Summary Get Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentBrowserConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentBrowserConfig
|
||||
// @Param request body dto.AgentSecurityConfigReq true "request"
|
||||
// @Success 200 {object} dto.AgentSecurityConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/browser/get [post]
|
||||
func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
var req dto.AgentBrowserConfigReq
|
||||
// @Router /ai/agents/security/get [post]
|
||||
func (b *BaseApi) GetAgentSecurityConfig(c *gin.Context) {
|
||||
var req dto.AgentSecurityConfigReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetBrowserConfig(req)
|
||||
data, err := agentService.GetSecurityConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
@@ -498,19 +498,19 @@ func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent Browser config
|
||||
// @Summary Update Agent Security config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentBrowserConfigUpdateReq true "request"
|
||||
// @Param request body dto.AgentSecurityConfigUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/browser/update [post]
|
||||
func (b *BaseApi) UpdateAgentBrowserConfig(c *gin.Context) {
|
||||
var req dto.AgentBrowserConfigUpdateReq
|
||||
// @Router /ai/agents/security/update [post]
|
||||
func (b *BaseApi) UpdateAgentSecurityConfig(c *gin.Context) {
|
||||
var req dto.AgentSecurityConfigUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateBrowserConfig(req); err != nil {
|
||||
if err := agentService.UpdateSecurityConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
+38
-42
@@ -3,32 +3,33 @@ package dto
|
||||
import "time"
|
||||
|
||||
type AgentCreateReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
AccountID uint `json:"accountId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Token string `json:"token"`
|
||||
TaskID string `json:"taskID"`
|
||||
Advanced bool `json:"advanced"`
|
||||
ContainerName string `json:"containerName"`
|
||||
AllowPort bool `json:"allowPort"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
CpuQuota float64 `json:"cpuQuota"`
|
||||
MemoryLimit float64 `json:"memoryLimit"`
|
||||
MemoryUnit string `json:"memoryUnit"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
EditCompose bool `json:"editCompose"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
AccountID uint `json:"accountId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Token string `json:"token"`
|
||||
TaskID string `json:"taskID"`
|
||||
Advanced bool `json:"advanced"`
|
||||
ContainerName string `json:"containerName"`
|
||||
AllowPort bool `json:"allowPort"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
CpuQuota float64 `json:"cpuQuota"`
|
||||
MemoryLimit float64 `json:"memoryLimit"`
|
||||
MemoryUnit string `json:"memoryUnit"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
EditCompose bool `json:"editCompose"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
@@ -272,24 +273,17 @@ type AgentDiscordConfig struct {
|
||||
Proxy string `json:"proxy"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfigReq struct {
|
||||
type AgentSecurityConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Headless bool `json:"headless"`
|
||||
NoSandbox bool `json:"noSandbox"`
|
||||
DefaultProfile string `json:"defaultProfile" validate:"required"`
|
||||
type AgentSecurityConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
}
|
||||
|
||||
type AgentBrowserConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ExecutablePath string `json:"executablePath"`
|
||||
Headless bool `json:"headless"`
|
||||
NoSandbox bool `json:"noSandbox"`
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
type AgentSecurityConfig struct {
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
}
|
||||
|
||||
type AgentOtherConfigReq struct {
|
||||
@@ -297,10 +291,12 @@ type AgentOtherConfigReq struct {
|
||||
}
|
||||
|
||||
type AgentOtherConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
}
|
||||
|
||||
type AgentOtherConfig struct {
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
}
|
||||
|
||||
+168
-27
@@ -57,8 +57,8 @@ type IAgentService interface {
|
||||
UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
|
||||
InstallPlugin(req dto.AgentPluginInstallReq) error
|
||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||
GetBrowserConfig(req dto.AgentBrowserConfigReq) (*dto.AgentBrowserConfig, error)
|
||||
UpdateBrowserConfig(req dto.AgentBrowserConfigUpdateReq) error
|
||||
GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.AgentSecurityConfig, error)
|
||||
UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error
|
||||
GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error)
|
||||
UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||
@@ -133,8 +133,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
token := ""
|
||||
configPath := ""
|
||||
storedModel := ""
|
||||
var allowedOrigins []string
|
||||
|
||||
if agentType == constant.AppOpenclaw {
|
||||
var err error
|
||||
allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return nil, fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
provider = strings.ToLower(strings.TrimSpace(req.Provider))
|
||||
if !isSupportedAgentProvider(provider) {
|
||||
return nil, buserr.New("ErrAgentProviderNotSupported")
|
||||
@@ -267,7 +276,19 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return nil, err
|
||||
}
|
||||
if agentType == constant.AppOpenclaw {
|
||||
go a.writeConfigWithRetry(appInstall, provider, req.Model, apiType, maxTokens, contextWindow, baseURL, apiKey, token, agent.ID)
|
||||
go a.writeConfigWithRetry(
|
||||
appInstall,
|
||||
provider,
|
||||
req.Model,
|
||||
apiType,
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
baseURL,
|
||||
apiKey,
|
||||
token,
|
||||
agent.ID,
|
||||
allowedOrigins,
|
||||
)
|
||||
}
|
||||
|
||||
item := buildAgentItem(agent, appInstall, nil)
|
||||
@@ -412,7 +433,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if confDir == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.Provider = provider
|
||||
@@ -900,35 +921,42 @@ func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPlugin
|
||||
return &dto.AgentPluginStatus{Installed: installed}, nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetBrowserConfig(req dto.AgentBrowserConfigReq) (*dto.AgentBrowserConfig, error) {
|
||||
func (a AgentService) GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.AgentSecurityConfig, error) {
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
|
||||
return nil, fmt.Errorf("copaw does not support security config")
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := extractBrowserConfig(conf)
|
||||
result := extractSecurityConfig(conf)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateBrowserConfig(req dto.AgentBrowserConfigUpdateReq) error {
|
||||
func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error {
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
|
||||
return fmt.Errorf("copaw does not support security config")
|
||||
}
|
||||
allowedOrigins, err := normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setBrowserConfig(conf, dto.AgentBrowserConfig{
|
||||
Enabled: req.Enabled,
|
||||
ExecutablePath: defaultBrowserExecutablePath,
|
||||
Headless: req.Headless,
|
||||
NoSandbox: req.NoSandbox,
|
||||
DefaultProfile: strings.TrimSpace(req.DefaultProfile),
|
||||
})
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -957,7 +985,10 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setOtherConfig(conf, dto.AgentOtherConfig{UserTimezone: strings.TrimSpace(req.UserTimezone)})
|
||||
setOtherConfig(conf, dto.AgentOtherConfig{
|
||||
UserTimezone: strings.TrimSpace(req.UserTimezone),
|
||||
BrowserEnabled: req.BrowserEnabled,
|
||||
})
|
||||
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1027,6 +1058,100 @@ func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) erro
|
||||
return fileOp.SaveFile(configPath, string(payload), 0600)
|
||||
}
|
||||
|
||||
func normalizeAllowedOrigins(origins []string) ([]string, error) {
|
||||
if len(origins) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]string, 0, len(origins))
|
||||
seen := make(map[string]struct{}, len(origins))
|
||||
for _, origin := range origins {
|
||||
origin = strings.TrimSpace(origin)
|
||||
if origin == "" {
|
||||
continue
|
||||
}
|
||||
normalized, err := normalizeAllowedOrigin(origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
result = append(result, normalized)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeAllowedOrigin(origin string) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(origin))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid allowed origin: %s", origin)
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", fmt.Errorf("invalid allowed origin: %s", origin)
|
||||
}
|
||||
if parsed.User != nil || parsed.Host == "" || parsed.Hostname() == "" {
|
||||
return "", fmt.Errorf("invalid allowed origin: %s", origin)
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return "", fmt.Errorf("invalid allowed origin: %s", origin)
|
||||
}
|
||||
if pathValue := strings.TrimSpace(parsed.EscapedPath()); pathValue != "" && pathValue != "/" {
|
||||
return "", fmt.Errorf("invalid allowed origin: %s", origin)
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
if strings.Contains(host, ":") {
|
||||
host = "[" + host + "]"
|
||||
}
|
||||
normalized := parsed.Scheme + "://" + host
|
||||
if port := parsed.Port(); port != "" {
|
||||
normalized += ":" + port
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func extractSecurityConfig(conf map[string]interface{}) dto.AgentSecurityConfig {
|
||||
result := dto.AgentSecurityConfig{AllowedOrigins: []string{}}
|
||||
gateway, ok := conf["gateway"].(map[string]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
controlUi, ok := gateway["controlUi"].(map[string]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
switch values := controlUi["allowedOrigins"].(type) {
|
||||
case []interface{}:
|
||||
for _, value := range values {
|
||||
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
||||
result.AllowedOrigins = append(result.AllowedOrigins, strings.TrimSpace(text))
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
result.AllowedOrigins = append(result.AllowedOrigins, strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func setSecurityConfig(conf map[string]interface{}, config dto.AgentSecurityConfig) {
|
||||
gateway := ensureChildMap(conf, "gateway")
|
||||
controlUi := ensureChildMap(gateway, "controlUi")
|
||||
if _, ok := controlUi["dangerouslyDisableDeviceAuth"]; !ok {
|
||||
controlUi["dangerouslyDisableDeviceAuth"] = true
|
||||
}
|
||||
allowedOrigins := append([]string(nil), config.AllowedOrigins...)
|
||||
if len(allowedOrigins) > 0 {
|
||||
controlUi["allowedOrigins"] = allowedOrigins
|
||||
} else {
|
||||
delete(controlUi, "allowedOrigins")
|
||||
}
|
||||
delete(controlUi, "dangerouslyAllowHostHeaderOriginFallback")
|
||||
}
|
||||
|
||||
func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||
result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"}
|
||||
channels, ok := conf["channels"].(map[string]interface{})
|
||||
@@ -1184,8 +1309,8 @@ func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig
|
||||
delete(discord, "dm")
|
||||
}
|
||||
|
||||
func extractBrowserConfig(conf map[string]interface{}) dto.AgentBrowserConfig {
|
||||
result := dto.AgentBrowserConfig{
|
||||
func extractBrowserConfig(conf map[string]interface{}) browserConfig {
|
||||
result := browserConfig{
|
||||
Enabled: true,
|
||||
ExecutablePath: defaultBrowserExecutablePath,
|
||||
Headless: true,
|
||||
@@ -1214,7 +1339,7 @@ func extractBrowserConfig(conf map[string]interface{}) dto.AgentBrowserConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
func setBrowserConfig(conf map[string]interface{}, config dto.AgentBrowserConfig) {
|
||||
func setBrowserConfig(conf map[string]interface{}, config browserConfig) {
|
||||
browser := ensureChildMap(conf, "browser")
|
||||
browser["enabled"] = config.Enabled
|
||||
browser["executablePath"] = defaultBrowserExecutablePath
|
||||
@@ -1335,18 +1460,24 @@ func checkPluginInstalled(containerName, pluginType string) (bool, error) {
|
||||
}
|
||||
|
||||
func extractOtherConfig(conf map[string]interface{}) dto.AgentOtherConfig {
|
||||
result := dto.AgentOtherConfig{UserTimezone: resolveServerTimezone()}
|
||||
result := dto.AgentOtherConfig{UserTimezone: resolveServerTimezone(), BrowserEnabled: true}
|
||||
agents, ok := conf["agents"].(map[string]interface{})
|
||||
if !ok {
|
||||
browser := extractBrowserConfig(conf)
|
||||
result.BrowserEnabled = browser.Enabled
|
||||
return result
|
||||
}
|
||||
defaults, ok := agents["defaults"].(map[string]interface{})
|
||||
if !ok {
|
||||
browser := extractBrowserConfig(conf)
|
||||
result.BrowserEnabled = browser.Enabled
|
||||
return result
|
||||
}
|
||||
if timezone, ok := defaults["userTimezone"].(string); ok && strings.TrimSpace(timezone) != "" {
|
||||
result.UserTimezone = strings.TrimSpace(timezone)
|
||||
}
|
||||
browser := extractBrowserConfig(conf)
|
||||
result.BrowserEnabled = browser.Enabled
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1358,6 +1489,13 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
|
||||
timezone = resolveServerTimezone()
|
||||
}
|
||||
defaults["userTimezone"] = timezone
|
||||
setBrowserConfig(conf, browserConfig{
|
||||
Enabled: config.BrowserEnabled,
|
||||
ExecutablePath: defaultBrowserExecutablePath,
|
||||
Headless: true,
|
||||
NoSandbox: true,
|
||||
DefaultProfile: defaultBrowserProfile,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
@@ -1389,7 +1527,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
if strings.EqualFold(account.Provider, "vllm") && strings.TrimSpace(account.Model) != "" {
|
||||
modelName = account.Model
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account.Provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account.Provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.BaseURL = baseURL
|
||||
@@ -1493,7 +1631,7 @@ func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, agentID uint) {
|
||||
func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, agentID uint, allowedOrigins []string) {
|
||||
if appInstall == nil {
|
||||
return
|
||||
}
|
||||
@@ -1506,7 +1644,7 @@ func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provide
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey, token); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey, token, allowedOrigins); err != nil {
|
||||
global.LOG.Errorf("write openclaw config failed: %v", err)
|
||||
agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID))
|
||||
if errGet == nil && agent != nil {
|
||||
@@ -1560,8 +1698,8 @@ type gatewayConfig struct {
|
||||
}
|
||||
|
||||
type gatewayControlUi struct {
|
||||
DangerouslyDisableDeviceAuth bool `json:"dangerouslyDisableDeviceAuth"`
|
||||
DangerouslyAllowHostHeaderOriginFallback bool `json:"dangerouslyAllowHostHeaderOriginFallback"`
|
||||
DangerouslyDisableDeviceAuth bool `json:"dangerouslyDisableDeviceAuth"`
|
||||
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
|
||||
}
|
||||
|
||||
type gatewayAuth struct {
|
||||
@@ -1619,7 +1757,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string) error {
|
||||
func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, allowedOrigins []string) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -1646,8 +1784,8 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens
|
||||
Token: token,
|
||||
},
|
||||
ControlUi: gatewayControlUi{
|
||||
DangerouslyDisableDeviceAuth: true,
|
||||
DangerouslyAllowHostHeaderOriginFallback: true,
|
||||
DangerouslyDisableDeviceAuth: true,
|
||||
AllowedOrigins: append([]string(nil), allowedOrigins...),
|
||||
},
|
||||
},
|
||||
Agents: agentsConfig{
|
||||
@@ -1738,6 +1876,9 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens
|
||||
}
|
||||
authMap["token"] = token
|
||||
}
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
|
||||
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/plugin/check", baseApi.CheckAgentPlugin)
|
||||
aiToolsRouter.POST("/agents/browser/get", baseApi.GetAgentBrowserConfig)
|
||||
aiToolsRouter.POST("/agents/browser/update", baseApi.UpdateAgentBrowserConfig)
|
||||
aiToolsRouter.POST("/agents/security/get", baseApi.GetAgentSecurityConfig)
|
||||
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
|
||||
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
|
||||
aiToolsRouter.POST("/agents/other/update", baseApi.UpdateAgentOtherConfig)
|
||||
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
|
||||
|
||||
@@ -241,6 +241,7 @@ export namespace AI {
|
||||
appVersion: string;
|
||||
webUIPort: number;
|
||||
bridgePort?: number;
|
||||
allowedOrigins?: string[];
|
||||
agentType: 'openclaw' | 'copaw';
|
||||
provider?: string;
|
||||
model?: string;
|
||||
@@ -506,24 +507,17 @@ export namespace AI {
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfigReq {
|
||||
export interface AgentSecurityConfigReq {
|
||||
agentId: number;
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfig {
|
||||
enabled: boolean;
|
||||
executablePath: string;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
defaultProfile: string;
|
||||
export interface AgentSecurityConfig {
|
||||
allowedOrigins: string[];
|
||||
}
|
||||
|
||||
export interface AgentBrowserConfigUpdateReq {
|
||||
export interface AgentSecurityConfigUpdateReq {
|
||||
agentId: number;
|
||||
enabled: boolean;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
defaultProfile: string;
|
||||
allowedOrigins: string[];
|
||||
}
|
||||
|
||||
export interface AgentOtherConfigReq {
|
||||
@@ -532,10 +526,12 @@ export namespace AI {
|
||||
|
||||
export interface AgentOtherConfig {
|
||||
userTimezone: string;
|
||||
browserEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface AgentOtherConfigUpdateReq {
|
||||
agentId: number;
|
||||
userTimezone: string;
|
||||
browserEnabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,12 +185,12 @@ export const checkAgentPlugin = (req: AI.AgentPluginCheckReq) => {
|
||||
return http.post<AI.AgentPluginStatus>(`/ai/agents/plugin/check`, req);
|
||||
};
|
||||
|
||||
export const getAgentBrowserConfig = (req: AI.AgentBrowserConfigReq) => {
|
||||
return http.post<AI.AgentBrowserConfig>(`/ai/agents/browser/get`, req);
|
||||
export const getAgentSecurityConfig = (req: AI.AgentSecurityConfigReq) => {
|
||||
return http.post<AI.AgentSecurityConfig>(`/ai/agents/security/get`, req);
|
||||
};
|
||||
|
||||
export const updateAgentBrowserConfig = (req: AI.AgentBrowserConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/browser/update`, req);
|
||||
export const updateAgentSecurityConfig = (req: AI.AgentSecurityConfigUpdateReq) => {
|
||||
return http.post(`/ai/agents/security/update`, req);
|
||||
};
|
||||
|
||||
export const getAgentOtherConfig = (req: AI.AgentOtherConfigReq) => {
|
||||
|
||||
@@ -680,6 +680,12 @@ const message = {
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
allowedOrigins: 'Access Addresses',
|
||||
allowedOriginsHelper:
|
||||
'Enter one full access address per line, for example http://192.168.1.2:18789. Fill it manually if the default access address is not configured.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Enter at least one access address',
|
||||
allowedOriginsInvalid: 'Use the format http(s)://host-or-ip:port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
@@ -689,14 +695,10 @@ const message = {
|
||||
verifySkipped: 'Skipped',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Time Zone',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -688,6 +688,12 @@ const message = {
|
||||
appVersion: 'Versión de la app',
|
||||
webuiPort: 'Puerto WebUI',
|
||||
bridgePort: 'Puerto Bridge',
|
||||
allowedOrigins: 'Direcciones de acceso',
|
||||
allowedOriginsHelper:
|
||||
'Introduce una dirección de acceso completa por línea, por ejemplo http://192.168.1.2:18789. Si no hay una dirección predeterminada configurada, rellénala manualmente.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Introduce al menos una dirección de acceso',
|
||||
allowedOriginsInvalid: 'Usa el formato http(s)://host-o-ip:puerto',
|
||||
provider: 'Proveedor de modelos',
|
||||
apiKey: 'Clave API',
|
||||
baseUrl: 'URL base',
|
||||
@@ -697,14 +703,10 @@ const message = {
|
||||
verifySkipped: 'Omitido',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Zona horaria',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -681,6 +681,12 @@ const message = {
|
||||
appVersion: 'アプリバージョン',
|
||||
webuiPort: 'WebUI ポート',
|
||||
bridgePort: 'Bridge ポート',
|
||||
allowedOrigins: 'アクセスアドレス',
|
||||
allowedOriginsHelper:
|
||||
'1 行に 1 つずつ完全なアクセスアドレスを入力してください。例: http://192.168.1.2:18789。デフォルトのアクセスアドレスが未設定の場合は手動で入力してください。',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: '少なくとも 1 つのアクセスアドレスを入力してください',
|
||||
allowedOriginsInvalid: 'http(s)://host-or-ip:port の形式で入力してください',
|
||||
provider: 'モデルプロバイダー',
|
||||
apiKey: 'API キー',
|
||||
baseUrl: 'ベースURL',
|
||||
@@ -690,14 +696,10 @@ const message = {
|
||||
verifySkipped: 'スキップ済み',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'タイムゾーン',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -673,6 +673,12 @@ const message = {
|
||||
appVersion: '앱 버전',
|
||||
webuiPort: 'WebUI 포트',
|
||||
bridgePort: 'Bridge 포트',
|
||||
allowedOrigins: '접속 주소',
|
||||
allowedOriginsHelper:
|
||||
'한 줄에 하나의 전체 접속 주소를 입력하세요. 예: http://192.168.1.2:18789. 기본 접속 주소가 설정되지 않은 경우 수동으로 입력하세요.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: '접속 주소를 하나 이상 입력하세요',
|
||||
allowedOriginsInvalid: 'http(s)://host-or-ip:port 형식으로 입력하세요',
|
||||
provider: '모델 제공자',
|
||||
apiKey: 'API 키',
|
||||
baseUrl: '기본 URL',
|
||||
@@ -682,14 +688,10 @@ const message = {
|
||||
verifySkipped: '건너뜀',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: '시간대',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -688,6 +688,12 @@ const message = {
|
||||
appVersion: 'Versi aplikasi',
|
||||
webuiPort: 'Port WebUI',
|
||||
bridgePort: 'Port Bridge',
|
||||
allowedOrigins: 'Alamat akses',
|
||||
allowedOriginsHelper:
|
||||
'Masukkan satu alamat akses penuh bagi setiap baris, contohnya http://192.168.1.2:18789. Isikan secara manual jika alamat akses lalai belum dikonfigurasi.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Masukkan sekurang-kurangnya satu alamat akses',
|
||||
allowedOriginsInvalid: 'Gunakan format http(s)://hos-atau-ip:port',
|
||||
provider: 'Penyedia model',
|
||||
apiKey: 'Kunci API',
|
||||
baseUrl: 'URL asas',
|
||||
@@ -697,14 +703,10 @@ const message = {
|
||||
verifySkipped: 'Dilangkau',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Zon Waktu',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -683,6 +683,12 @@ const message = {
|
||||
appVersion: 'Versão do app',
|
||||
webuiPort: 'Porta WebUI',
|
||||
bridgePort: 'Porta Bridge',
|
||||
allowedOrigins: 'Endereços de acesso',
|
||||
allowedOriginsHelper:
|
||||
'Informe um endereço de acesso completo por linha, por exemplo http://192.168.1.2:18789. Preencha manualmente se o endereço padrão não estiver configurado.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Informe pelo menos um endereço de acesso',
|
||||
allowedOriginsInvalid: 'Use o formato http(s)://host-ou-ip:porta',
|
||||
provider: 'Provedor de modelos',
|
||||
apiKey: 'Chave API',
|
||||
baseUrl: 'URL base',
|
||||
@@ -692,14 +698,10 @@ const message = {
|
||||
verifySkipped: 'Ignorado',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Fuso horário',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -680,6 +680,12 @@ const message = {
|
||||
appVersion: 'Версия приложения',
|
||||
webuiPort: 'Порт WebUI',
|
||||
bridgePort: 'Порт Bridge',
|
||||
allowedOrigins: 'Адреса доступа',
|
||||
allowedOriginsHelper:
|
||||
'Указывайте по одному полному адресу доступа в строке, например http://192.168.1.2:18789. Если адрес по умолчанию не настроен, введите его вручную.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'Укажите хотя бы один адрес доступа',
|
||||
allowedOriginsInvalid: 'Используйте формат http(s)://host-or-ip:port',
|
||||
provider: 'Поставщик моделей',
|
||||
apiKey: 'API ключ',
|
||||
baseUrl: 'Базовый URL',
|
||||
@@ -689,14 +695,10 @@ const message = {
|
||||
verifySkipped: 'Пропущено',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Часовой пояс',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -684,6 +684,12 @@ const message = {
|
||||
appVersion: 'Uygulama sürümü',
|
||||
webuiPort: 'WebUI portu',
|
||||
bridgePort: 'Bridge portu',
|
||||
allowedOrigins: 'Erişim adresleri',
|
||||
allowedOriginsHelper:
|
||||
'Her satıra bir tam erişim adresi girin. Örnek: http://192.168.1.2:18789. Varsayılan erişim adresi yapılandırılmamışsa elle girin.',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: 'En az bir erişim adresi girin',
|
||||
allowedOriginsInvalid: 'http(s)://host-veya-ip:port biçimini kullanın',
|
||||
provider: 'Model sağlayıcı',
|
||||
apiKey: 'API anahtarı',
|
||||
baseUrl: 'Temel URL',
|
||||
@@ -693,14 +699,10 @@ const message = {
|
||||
verifySkipped: 'Atlandı',
|
||||
configTitle: 'Configuration',
|
||||
settingsTab: 'Settings',
|
||||
browserTab: 'Browser',
|
||||
securityTab: 'Security',
|
||||
otherTab: 'Other',
|
||||
timeZone: 'Saat Dilimi',
|
||||
browserEnabled: 'Browser Enabled',
|
||||
headless: 'Headless',
|
||||
noSandbox: 'No Sandbox',
|
||||
defaultProfile: 'Default Profile',
|
||||
executablePath: 'Executable Path',
|
||||
switchModelSuccess: 'Model switched successfully',
|
||||
channelsTab: 'Channels',
|
||||
wecom: 'WeCom',
|
||||
|
||||
@@ -648,6 +648,11 @@ const message = {
|
||||
appVersion: '應用版本',
|
||||
webuiPort: 'WebUI 埠',
|
||||
bridgePort: 'Bridge 埠',
|
||||
allowedOrigins: '訪問地址',
|
||||
allowedOriginsHelper: '一行一個完整訪問地址,例如 http://192.168.1.2:18789;未設定預設訪問地址時請手動填寫',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: '請至少填寫一個訪問地址',
|
||||
allowedOriginsInvalid: '訪問地址格式錯誤,請輸入 http(s)://網域或IP:埠',
|
||||
provider: '模型供應商',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
@@ -657,14 +662,10 @@ const message = {
|
||||
verifySkipped: '已跳過',
|
||||
configTitle: '設定',
|
||||
settingsTab: '設定',
|
||||
browserTab: '瀏覽器',
|
||||
securityTab: '安全',
|
||||
otherTab: '其他',
|
||||
timeZone: '時區',
|
||||
browserEnabled: '瀏覽器開關',
|
||||
headless: '無頭模式',
|
||||
noSandbox: '禁用沙箱',
|
||||
defaultProfile: '預設設定檔',
|
||||
executablePath: '瀏覽器可執行路徑',
|
||||
switchModelSuccess: '模型切換成功',
|
||||
channelsTab: '頻道',
|
||||
wecom: '企業微信',
|
||||
|
||||
@@ -647,6 +647,11 @@ const message = {
|
||||
appVersion: '应用版本',
|
||||
webuiPort: 'WebUI 端口',
|
||||
bridgePort: 'Bridge 端口',
|
||||
allowedOrigins: '访问地址',
|
||||
allowedOriginsHelper: '一行一个完整访问地址,例如 http://192.168.1.2:18789;未配置默认访问地址时请手动填写',
|
||||
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
|
||||
allowedOriginsRequired: '请至少填写一个访问地址',
|
||||
allowedOriginsInvalid: '访问地址格式错误,请输入 http(s)://域名或IP:端口',
|
||||
provider: '模型供应商',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
@@ -656,14 +661,10 @@ const message = {
|
||||
verifySkipped: '已跳过',
|
||||
configTitle: '配置',
|
||||
settingsTab: '设置',
|
||||
browserTab: '浏览器',
|
||||
securityTab: '安全',
|
||||
otherTab: '其他',
|
||||
timeZone: '时区',
|
||||
browserEnabled: '浏览器开关',
|
||||
headless: '无头模式',
|
||||
noSandbox: '禁用沙箱',
|
||||
defaultProfile: '默认配置文件',
|
||||
executablePath: '浏览器可执行路径',
|
||||
switchModelSuccess: '模型切换成功',
|
||||
channelsTab: '频道',
|
||||
wecom: '企业微信',
|
||||
|
||||
@@ -9,3 +9,67 @@ export const getAgentProviderDisplayName = (provider: string, displayName?: stri
|
||||
}
|
||||
return displayName || provider;
|
||||
};
|
||||
|
||||
export const buildDefaultAllowedOrigin = (systemIP: string, port?: number | string): string => {
|
||||
const target = String(systemIP || '').trim();
|
||||
if (!target || !port) {
|
||||
return '';
|
||||
}
|
||||
return `http://${target}:${port}`;
|
||||
};
|
||||
|
||||
export const normalizeAllowedOrigin = (value: string): string => {
|
||||
const target = String(value || '').trim();
|
||||
if (!target) {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(target);
|
||||
} catch (error) {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
if (parsed.pathname && parsed.pathname !== '/') {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
if (!parsed.host) {
|
||||
throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid'));
|
||||
}
|
||||
return `${parsed.protocol}//${parsed.host}`;
|
||||
};
|
||||
|
||||
export const parseAllowedOriginsInput = (value: string): string[] => {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const line of String(value || '').split(/\r?\n/)) {
|
||||
const target = line.trim();
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeAllowedOrigin(target);
|
||||
if (seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const validateAllowedOriginsInput = (value: string): string => {
|
||||
try {
|
||||
const parsed = parseAllowedOriginsInput(value);
|
||||
if (parsed.length === 0) {
|
||||
return i18n.global.t('aiTools.agents.allowedOriginsRequired');
|
||||
}
|
||||
return '';
|
||||
} catch (error: any) {
|
||||
return error?.message || i18n.global.t('aiTools.agents.allowedOriginsInvalid');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,22 @@
|
||||
>
|
||||
<el-input-number v-model="form.bridgePort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.agentType === 'openclaw'"
|
||||
:label="$t('aiTools.agents.allowedOrigins')"
|
||||
prop="allowedOrigins"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.allowedOrigins"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('aiTools.agents.allowedOriginsPlaceholder')"
|
||||
@input="handleAllowedOriginsInput"
|
||||
/>
|
||||
<span class="input-help">
|
||||
{{ $t('aiTools.agents.allowedOriginsHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="form-card" v-if="form.agentType === 'openclaw'">
|
||||
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
|
||||
@@ -103,8 +119,14 @@ import { checkNumberRange, Rules } from '@/global/form-rules';
|
||||
import { createAgent, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAppByKey, getAppDetail } from '@/api/modules/app';
|
||||
import { getAgentSettingByKey } from '@/api/modules/setting';
|
||||
import { getRandomStr, newUUID } from '@/utils/util';
|
||||
import { getAgentProviderDisplayName } from '@/utils/agent';
|
||||
import {
|
||||
buildDefaultAllowedOrigin,
|
||||
getAgentProviderDisplayName,
|
||||
parseAllowedOriginsInput,
|
||||
validateAllowedOriginsInput,
|
||||
} from '@/utils/agent';
|
||||
import { App } from '@/api/interface/app';
|
||||
import AdvancedSetting from '@/components/advanced-setting/index.vue';
|
||||
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
@@ -122,6 +144,9 @@ const providerAccountCount = ref<Record<string, number>>({});
|
||||
const manualModel = ref(false);
|
||||
const appInfo = ref<App.AppDTO>();
|
||||
const accountAddRef = ref();
|
||||
const systemIP = ref('');
|
||||
const lastAutoAllowedOrigins = ref('');
|
||||
const allowedOriginsAutoFilled = ref(true);
|
||||
const { isIntl } = useGlobalStore();
|
||||
|
||||
const form = reactive({
|
||||
@@ -130,6 +155,7 @@ const form = reactive({
|
||||
appVersion: '',
|
||||
webUIPort: 18789,
|
||||
bridgePort: 18790,
|
||||
allowedOrigins: '',
|
||||
provider: 'deepseek',
|
||||
accountId: undefined as unknown as number,
|
||||
model: '',
|
||||
@@ -152,37 +178,29 @@ const form = reactive({
|
||||
dockerCompose: '',
|
||||
});
|
||||
|
||||
const validateOpenclawOnly = (field: 'provider' | 'accountId' | 'model' | 'bridgePort') => {
|
||||
return (_rule: any, value: any, callback: (error?: Error) => void) => {
|
||||
if (form.agentType !== 'openclaw') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (field === 'bridgePort') {
|
||||
if (!value || Number(value) <= 0) {
|
||||
callback(new Error('bridge port is required'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback(new Error(`${field} is required`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput],
|
||||
agentType: [Rules.requiredSelect],
|
||||
appVersion: [Rules.requiredSelect],
|
||||
webUIPort: [Rules.requiredInput],
|
||||
bridgePort: [{ validator: validateOpenclawOnly('bridgePort'), trigger: 'blur' }],
|
||||
provider: [{ validator: validateOpenclawOnly('provider'), trigger: 'change' }],
|
||||
accountId: [{ validator: validateOpenclawOnly('accountId'), trigger: 'change' }],
|
||||
model: [{ validator: validateOpenclawOnly('model'), trigger: 'change' }],
|
||||
bridgePort: [Rules.requiredInput],
|
||||
allowedOrigins: [
|
||||
Rules.requiredInput,
|
||||
{
|
||||
validator: (_rule: any, value: any, callback: (error?: Error) => void) => {
|
||||
const message = validateAllowedOriginsInput(String(value || ''));
|
||||
if (message) {
|
||||
callback(new Error(message));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
provider: [Rules.requiredSelect],
|
||||
accountId: [Rules.requiredSelect],
|
||||
model: [Rules.requiredInput],
|
||||
containerName: [Rules.containerName],
|
||||
restartPolicy: [Rules.requiredSelect],
|
||||
cpuQuota: [checkNumberRange(0, 99999)],
|
||||
@@ -192,6 +210,28 @@ const rules = reactive({
|
||||
|
||||
const filteredModels = computed(() => providerModels.value[form.provider] || []);
|
||||
|
||||
const syncAllowedOriginsWithDefault = (force = false) => {
|
||||
if (form.agentType !== 'openclaw') {
|
||||
return;
|
||||
}
|
||||
const defaultOrigin = buildDefaultAllowedOrigin(systemIP.value, form.webUIPort);
|
||||
if (!force && !allowedOriginsAutoFilled.value && form.allowedOrigins !== lastAutoAllowedOrigins.value) {
|
||||
return;
|
||||
}
|
||||
form.allowedOrigins = defaultOrigin;
|
||||
lastAutoAllowedOrigins.value = defaultOrigin;
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
};
|
||||
|
||||
const loadSystemIP = async () => {
|
||||
try {
|
||||
const res = await getAgentSettingByKey('SystemIP');
|
||||
systemIP.value = String(res.data || '').trim();
|
||||
} catch (error) {
|
||||
systemIP.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const loadVersions = async (appKey: 'openclaw' | 'copaw') => {
|
||||
const res = await getAppByKey(appKey);
|
||||
appInfo.value = res.data;
|
||||
@@ -301,11 +341,17 @@ const handleAgentTypeChange = async () => {
|
||||
form.apiType = 'openai-completions';
|
||||
if (form.agentType === 'openclaw') {
|
||||
form.bridgePort = form.bridgePort || 18790;
|
||||
await loadSystemIP();
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
syncAllowedOriginsWithDefault(true);
|
||||
await loadVersions('openclaw');
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
return;
|
||||
}
|
||||
form.allowedOrigins = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
await loadVersions('copaw');
|
||||
};
|
||||
|
||||
@@ -353,6 +399,10 @@ const setDefaultModel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAllowedOriginsInput = () => {
|
||||
allowedOriginsAutoFilled.value = form.allowedOrigins === lastAutoAllowedOrigins.value;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
@@ -368,6 +418,7 @@ const submit = async () => {
|
||||
appVersion: form.appVersion,
|
||||
webUIPort: form.webUIPort,
|
||||
bridgePort: form.agentType === 'openclaw' ? form.bridgePort : undefined,
|
||||
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
|
||||
agentType: form.agentType,
|
||||
provider: form.agentType === 'openclaw' ? form.provider : undefined,
|
||||
model: form.agentType === 'openclaw' ? form.model : undefined,
|
||||
@@ -408,7 +459,10 @@ const submit = async () => {
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
form.token = '';
|
||||
form.allowedOrigins = '';
|
||||
form.dockerCompose = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
};
|
||||
|
||||
const openDrawer = async (agentType?: 'openclaw' | 'copaw') => {
|
||||
@@ -419,12 +473,18 @@ const openDrawer = async (agentType?: 'openclaw' | 'copaw') => {
|
||||
form.agentType = targetType;
|
||||
form.token = getRandomStr(32).toLowerCase();
|
||||
if (form.agentType === 'copaw') {
|
||||
form.allowedOrigins = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
await loadVersions('copaw');
|
||||
providerOptions.value = [];
|
||||
providerModels.value = {};
|
||||
accountOptions.value = [];
|
||||
return;
|
||||
}
|
||||
await loadSystemIP();
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
syncAllowedOriginsWithDefault(true);
|
||||
await loadVersions('openclaw');
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
@@ -464,6 +524,13 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.webUIPort,
|
||||
() => {
|
||||
syncAllowedOriginsWithDefault();
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
open: openDrawer,
|
||||
});
|
||||
|
||||
@@ -37,11 +37,11 @@ const modelRef = ref();
|
||||
const settingsRef = ref();
|
||||
|
||||
const loadSettings = async () => {
|
||||
if (agentId.value <= 0) {
|
||||
if (!currentAgent.value) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await settingsRef.value?.load(agentId.value);
|
||||
await settingsRef.value?.load(currentAgent.value);
|
||||
};
|
||||
|
||||
const loadModel = async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane :label="t('aiTools.agents.browserTab')" name="browser">
|
||||
<BrowserTab ref="browserRef" />
|
||||
<el-tab-pane v-if="agentType === 'openclaw'" :label="t('aiTools.agents.securityTab')" name="security">
|
||||
<SecurityTab ref="securityRef" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('aiTools.agents.otherTab')" name="other">
|
||||
<OtherTab ref="otherRef" />
|
||||
@@ -12,13 +12,15 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BrowserTab from './settings/browser.vue';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import SecurityTab from './settings/security.vue';
|
||||
import OtherTab from './settings/other.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const activeTab = ref('browser');
|
||||
const activeTab = ref('security');
|
||||
const agentId = ref(0);
|
||||
const browserRef = ref();
|
||||
const agentType = ref<AI.AgentItem['agentType']>('openclaw');
|
||||
const securityRef = ref();
|
||||
const otherRef = ref();
|
||||
|
||||
const loadCurrentTab = async () => {
|
||||
@@ -26,8 +28,8 @@ const loadCurrentTab = async () => {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
if (activeTab.value === 'browser') {
|
||||
await browserRef.value?.load(agentId.value);
|
||||
if (activeTab.value === 'security' && agentType.value === 'openclaw') {
|
||||
await securityRef.value?.load(agentId.value);
|
||||
return;
|
||||
}
|
||||
if (activeTab.value === 'other') {
|
||||
@@ -39,9 +41,10 @@ const handleTabClick = async () => {
|
||||
await loadCurrentTab();
|
||||
};
|
||||
|
||||
const load = async (id: number) => {
|
||||
agentId.value = id;
|
||||
activeTab.value = 'browser';
|
||||
const load = async (agent: AI.AgentItem) => {
|
||||
agentId.value = agent.id;
|
||||
agentType.value = agent.agentType;
|
||||
activeTab.value = agent.agentType === 'openclaw' ? 'security' : 'other';
|
||||
await loadCurrentTab();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="t('aiTools.agents.browserEnabled')">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.headless')">
|
||||
<el-switch v-model="form.headless" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.noSandbox')">
|
||||
<el-switch v-model="form.noSandbox" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.defaultProfile')" prop="defaultProfile">
|
||||
<el-input v-model="form.defaultProfile" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.executablePath')">
|
||||
<el-input v-model="form.executablePath" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveConfig">
|
||||
{{ t('commons.button.save') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAgentBrowserConfig, updateAgentBrowserConfig } from '@/api/modules/ai';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const { t } = useI18n();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const agentId = ref(0);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const form = reactive<AI.AgentBrowserConfig>({
|
||||
enabled: true,
|
||||
executablePath: '',
|
||||
headless: true,
|
||||
noSandbox: true,
|
||||
defaultProfile: 'openclaw',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
defaultProfile: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const load = async (id: number) => {
|
||||
agentId.value = id;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAgentBrowserConfig({ agentId: id });
|
||||
Object.assign(form, res.data || {});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!agentId.value || !formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateAgentBrowserConfig({
|
||||
agentId: agentId.value,
|
||||
enabled: form.enabled,
|
||||
headless: form.headless,
|
||||
noSandbox: form.noSandbox,
|
||||
defaultProfile: form.defaultProfile,
|
||||
});
|
||||
MsgSuccess(t('aiTools.agents.saveSuccess'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
load,
|
||||
});
|
||||
</script>
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="t('aiTools.agents.browserEnabled')">
|
||||
<el-switch v-model="form.browserEnabled" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.agents.timeZone')" prop="userTimezone">
|
||||
<el-input v-model="form.userTimezone" />
|
||||
</el-form-item>
|
||||
@@ -28,6 +31,7 @@ const formRef = ref<FormInstance>();
|
||||
|
||||
const form = reactive<AI.AgentOtherConfig>({
|
||||
userTimezone: '',
|
||||
browserEnabled: true,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
@@ -55,6 +59,7 @@ const saveConfig = async () => {
|
||||
await updateAgentOtherConfig({
|
||||
agentId: agentId.value,
|
||||
userTimezone: form.userTimezone,
|
||||
browserEnabled: form.browserEnabled,
|
||||
});
|
||||
MsgSuccess(t('aiTools.agents.saveSuccess'));
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="t('aiTools.agents.allowedOrigins')" prop="allowedOrigins">
|
||||
<el-input
|
||||
v-model="form.allowedOrigins"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="t('aiTools.agents.allowedOriginsPlaceholder')"
|
||||
/>
|
||||
<span class="input-help">
|
||||
{{ t('aiTools.agents.allowedOriginsHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveConfig">
|
||||
{{ t('commons.button.save') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getAgentSecurityConfig, updateAgentSecurityConfig } from '@/api/modules/ai';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { parseAllowedOriginsInput, validateAllowedOriginsInput } from '@/utils/agent';
|
||||
|
||||
const { t } = useI18n();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const agentId = ref(0);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const form = reactive({
|
||||
allowedOrigins: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
allowedOrigins: [
|
||||
{
|
||||
validator: (_rule: any, value: any, callback: (error?: Error) => void) => {
|
||||
const message = validateAllowedOriginsInput(String(value || ''));
|
||||
if (message) {
|
||||
callback(new Error(message));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const load = async (id: number) => {
|
||||
agentId.value = id;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAgentSecurityConfig({ agentId: id });
|
||||
form.allowedOrigins = (res.data?.allowedOrigins || []).join('\n');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!agentId.value || !formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateAgentSecurityConfig({
|
||||
agentId: agentId.value,
|
||||
allowedOrigins: parseAllowedOriginsInput(form.allowedOrigins),
|
||||
});
|
||||
MsgSuccess(t('aiTools.agents.saveSuccess'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
load,
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user