diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 86fc60d58298..b87a19ae280e 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -76,6 +76,11 @@ const ( defaultToolsSessionVisibility = "all" maxCommunityAIAgents = int64(5) openclawPluginBaseDir = "/home/node/.openclaw/extensions" + openclawGatewayPort = 18789 + openclawCaddyPort = 8443 + openclawCaddyDataPerm = 0777 + openclawCaddyLoopbackAddress = "https://127.0.0.1:8443" + openclawTrustedProxyLoopback = "127.0.0.1/32" ) func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { @@ -86,14 +91,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { if err := checkPortExist(req.WebUIPort); err != nil { return nil, err } - if agentType == constant.AppOpenclaw { - if req.BridgePort <= 0 { - return nil, fmt.Errorf("bridge port is required") - } - if err := checkPortExist(req.BridgePort); err != nil { - return nil, err - } - } if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 { return nil, buserr.New("ErrNameIsExist") } @@ -209,12 +206,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { } params := map[string]interface{}{ - "PANEL_APP_PORT_HTTP": req.WebUIPort, - constant.CPUS: "0", - constant.MemoryLimit: "0", - constant.HostIP: "", + constant.CPUS: "0", + constant.MemoryLimit: "0", + constant.HostIP: "", } if agentType == constant.AppOpenclaw { + params["PANEL_APP_PORT_HTTPS"] = req.WebUIPort params["PROVIDER"] = provider params["MODEL"] = runtimeModel params["API_TYPE"] = apiType @@ -223,7 +220,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { params["BASE_URL"] = baseURL params["API_KEY"] = apiKey params["OPENCLAW_GATEWAY_TOKEN"] = token - params["PANEL_APP_PORT_BRIDGE"] = req.BridgePort + } else { + params["PANEL_APP_PORT_HTTP"] = req.WebUIPort } if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { @@ -898,6 +896,9 @@ func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { return err } + if err := writeOpenclawCaddyfile(agent.ConfigPath, allowedOrigins); err != nil { + return err + } return nil } @@ -1011,6 +1012,7 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) { } func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error { + ensureGatewaySecurityDefaults(conf) payload, err := json.MarshalIndent(conf, "", " ") if err != nil { return err @@ -1060,14 +1062,14 @@ func normalizeAllowedOrigin(origin string) (string, error) { if pathValue := strings.TrimSpace(parsed.EscapedPath()); pathValue != "" && pathValue != "/" { return "", fmt.Errorf("invalid allowed origin: %s", origin) } + if parsed.Port() == "" { + 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 - } + normalized := "https://" + host + ":" + parsed.Port() return normalized, nil } @@ -1099,18 +1101,60 @@ func extractSecurityConfig(conf map[string]interface{}) dto.AgentSecurityConfig } func setSecurityConfig(conf map[string]interface{}, config dto.AgentSecurityConfig) { + ensureGatewaySecurityDefaults(conf) 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") } +} + +func ensureGatewaySecurityDefaults(conf map[string]interface{}) { + gateway := ensureChildMap(conf, "gateway") + controlUi := ensureChildMap(gateway, "controlUi") + if _, ok := controlUi["dangerouslyDisableDeviceAuth"]; !ok { + controlUi["dangerouslyDisableDeviceAuth"] = true + } delete(controlUi, "dangerouslyAllowHostHeaderOriginFallback") + setTrustedProxies(gateway) +} + +func setTrustedProxies(gateway map[string]interface{}) { + proxies := make([]string, 0, 4) + seen := map[string]struct{}{} + switch values := gateway["trustedProxies"].(type) { + case []interface{}: + for _, value := range values { + text := strings.TrimSpace(fmt.Sprintf("%v", value)) + if text == "" { + continue + } + if _, ok := seen[text]; ok { + continue + } + seen[text] = struct{}{} + proxies = append(proxies, text) + } + case []string: + for _, value := range values { + text := strings.TrimSpace(value) + if text == "" { + continue + } + if _, ok := seen[text]; ok { + continue + } + seen[text] = struct{}{} + proxies = append(proxies, text) + } + } + if _, ok := seen[openclawTrustedProxyLoopback]; !ok { + proxies = append(proxies, openclawTrustedProxyLoopback) + } + gateway["trustedProxies"] = proxies } func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig { @@ -1531,7 +1575,11 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map if appInstall != nil && appInstall.ID > 0 { item.Container = appInstall.ContainerName item.AppVersion = appInstall.Version - item.WebUIPort = appInstall.HttpPort + if agentType == constant.AppOpenclaw { + item.WebUIPort = appInstall.HttpsPort + } else { + item.WebUIPort = appInstall.HttpPort + } item.Path = appInstall.GetPath() item.Status = appInstall.Status item.Message = appInstall.Message @@ -1544,6 +1592,82 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map return item } +func writeOpenclawCaddyfile(configPath string, allowedOrigins []string) error { + content, err := buildOpenclawCaddyfile(allowedOrigins) + if err != nil { + return err + } + dataDir := path.Dir(path.Dir(configPath)) + caddyDir := path.Join(dataDir, "caddy") + fileOp := files.NewFileOp() + if !fileOp.Stat(caddyDir) { + if err := fileOp.CreateDir(caddyDir, constant.DirPerm); err != nil { + return err + } + } + caddyDataDir := path.Join(caddyDir, "data") + if !fileOp.Stat(caddyDataDir) { + if err := fileOp.CreateDir(caddyDataDir, constant.DirPerm); err != nil { + return err + } + } + if err := fileOp.ChmodR(caddyDataDir, openclawCaddyDataPerm, false); err != nil { + return err + } + return fileOp.SaveFile(path.Join(caddyDir, "Caddyfile"), content, 0644) +} + +func buildOpenclawCaddyfile(allowedOrigins []string) (string, error) { + if len(allowedOrigins) == 0 { + return "", fmt.Errorf("allowed origins is required") + } + addresses := make([]string, 0, len(allowedOrigins)) + seen := make(map[string]struct{}, len(allowedOrigins)) + for _, origin := range allowedOrigins { + normalized, err := normalizeAllowedOrigin(origin) + if err != nil { + return "", err + } + parsed, err := url.Parse(normalized) + if err != nil { + return "", err + } + host := parsed.Hostname() + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + address := fmt.Sprintf("https://%s:%d", host, openclawCaddyPort) + if _, ok := seen[address]; ok { + continue + } + seen[address] = struct{}{} + addresses = append(addresses, address) + } + if len(addresses) == 0 { + return "", fmt.Errorf("allowed origins is required") + } + if _, ok := seen[openclawCaddyLoopbackAddress]; !ok { + addresses = append(addresses, openclawCaddyLoopbackAddress) + } + content := `{ + admin off + auto_https disable_redirects + default_sni 127.0.0.1 + skip_install_trust + storage file_system { + root /data/caddy + } +} + +` + strings.Join(addresses, ", ") + ` { + bind 0.0.0.0 + tls internal + reverse_proxy 127.0.0.1:` + strconv.Itoa(openclawGatewayPort) + ` +} +` + return content, nil +} + func checkAgentUpgradable(install model.AppInstall) bool { if install.ID == 0 || install.Version == "" || install.Version == "latest" { return false @@ -1651,11 +1775,12 @@ type toolSessionsConfig struct { } type gatewayConfig struct { - Mode string `json:"mode"` - Bind string `json:"bind"` - Port int `json:"port"` - Auth gatewayAuth `json:"auth"` - ControlUi gatewayControlUi `json:"controlUi"` + Mode string `json:"mode"` + Bind string `json:"bind"` + Port int `json:"port"` + Auth gatewayAuth `json:"auth"` + ControlUi gatewayControlUi `json:"controlUi"` + TrustedProxies []string `json:"trustedProxies,omitempty"` } type gatewayControlUi struct { @@ -1739,7 +1864,7 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens Gateway: gatewayConfig{ Mode: "local", Bind: "lan", - Port: 18789, + Port: openclawGatewayPort, Auth: gatewayAuth{ Mode: "token", Token: token, @@ -1748,6 +1873,7 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens DangerouslyDisableDeviceAuth: true, AllowedOrigins: append([]string(nil), allowedOrigins...), }, + TrustedProxies: []string{openclawTrustedProxyLoopback}, }, Agents: agentsConfig{ Defaults: agentDefaults{ @@ -1830,7 +1956,17 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens modelMap := ensureChildMap(defaultsMap, "model") modelMap["primary"] = cfg.Agents.Defaults.Model.Primary + ensureGatewaySecurityDefaults(conf) gatewayMap := ensureChildMap(conf, "gateway") + if _, ok := gatewayMap["mode"]; !ok { + gatewayMap["mode"] = "local" + } + if _, ok := gatewayMap["bind"]; !ok { + gatewayMap["bind"] = "lan" + } + if _, ok := gatewayMap["port"]; !ok { + gatewayMap["port"] = openclawGatewayPort + } authMap := ensureChildMap(gatewayMap, "auth") if _, ok := authMap["mode"]; !ok { authMap["mode"] = "token" @@ -1843,6 +1979,11 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens if err := writeOpenclawConfigRaw(configPath, conf); err != nil { return err } + if allowedOrigins != nil { + if err := writeOpenclawCaddyfile(configPath, allowedOrigins); err != nil { + return err + } + } envPath := path.Join(confDir, ".env") lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 416db0b064c3..9c72009e5ab3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -679,13 +679,13 @@ const message = { copawType: 'CoPaw', 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', + 'Enter one full HTTPS access address per line, for example https://192.168.1.2:18789. Fill it manually if the default access address is not configured.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'Enter at least one access address', allowedOriginsInvalid: 'Use the format http(s)://host-or-ip:port', + allowedOriginsHttpsOnly: 'Access addresses must start with https://', provider: 'Provider', apiKey: 'API Key', baseUrl: 'Base URL', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 2d4dca2bd98b..6c6c391bc67f 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -687,13 +687,13 @@ const message = { copawType: 'CoPaw', 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', + 'Introduce una dirección de acceso HTTPS completa por línea, por ejemplo https://192.168.1.2:18789. Si no hay una dirección predeterminada configurada, rellénala manualmente.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'Introduce al menos una dirección de acceso', allowedOriginsInvalid: 'Usa el formato http(s)://host-o-ip:puerto', + allowedOriginsHttpsOnly: 'Las direcciones de acceso deben comenzar con https://', provider: 'Proveedor de modelos', apiKey: 'Clave API', baseUrl: 'URL base', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index b7d01ea1380b..aa2b65ada4b6 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -680,13 +680,13 @@ const message = { copawType: 'CoPaw', appVersion: 'アプリバージョン', webuiPort: 'WebUI ポート', - bridgePort: 'Bridge ポート', allowedOrigins: 'アクセスアドレス', allowedOriginsHelper: - '1 行に 1 つずつ完全なアクセスアドレスを入力してください。例: http://192.168.1.2:18789。デフォルトのアクセスアドレスが未設定の場合は手動で入力してください。', - allowedOriginsPlaceholder: 'http://192.168.1.2:18789', + '1 行に 1 つずつ完全な HTTPS アクセスアドレスを入力してください。例: https://192.168.1.2:18789。デフォルトのアクセスアドレスが未設定の場合は手動で入力してください。', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: '少なくとも 1 つのアクセスアドレスを入力してください', allowedOriginsInvalid: 'http(s)://host-or-ip:port の形式で入力してください', + allowedOriginsHttpsOnly: 'アクセスアドレスは https:// で始めてください', provider: 'モデルプロバイダー', apiKey: 'API キー', baseUrl: 'ベースURL', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index b87cf98e87d8..2ba1008cb71c 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -672,13 +672,13 @@ const message = { copawType: 'CoPaw', appVersion: '앱 버전', webuiPort: 'WebUI 포트', - bridgePort: 'Bridge 포트', allowedOrigins: '접속 주소', allowedOriginsHelper: - '한 줄에 하나의 전체 접속 주소를 입력하세요. 예: http://192.168.1.2:18789. 기본 접속 주소가 설정되지 않은 경우 수동으로 입력하세요.', - allowedOriginsPlaceholder: 'http://192.168.1.2:18789', + '한 줄에 하나의 전체 HTTPS 접속 주소를 입력하세요. 예: https://192.168.1.2:18789. 기본 접속 주소가 설정되지 않은 경우 수동으로 입력하세요.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: '접속 주소를 하나 이상 입력하세요', allowedOriginsInvalid: 'http(s)://host-or-ip:port 형식으로 입력하세요', + allowedOriginsHttpsOnly: '접속 주소는 https:// 로 시작해야 합니다', provider: '모델 제공자', apiKey: 'API 키', baseUrl: '기본 URL', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index d012f6183c3b..6a38bad94b72 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -687,13 +687,13 @@ const message = { copawType: 'CoPaw', 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', + 'Masukkan satu alamat akses HTTPS penuh bagi setiap baris, contohnya https://192.168.1.2:18789. Isikan secara manual jika alamat akses lalai belum dikonfigurasi.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'Masukkan sekurang-kurangnya satu alamat akses', allowedOriginsInvalid: 'Gunakan format http(s)://hos-atau-ip:port', + allowedOriginsHttpsOnly: 'Alamat akses mesti bermula dengan https://', provider: 'Penyedia model', apiKey: 'Kunci API', baseUrl: 'URL asas', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 372bcf2365f5..e69ac9e48181 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -682,13 +682,13 @@ const message = { copawType: 'CoPaw', 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', + 'Informe um endereço de acesso HTTPS completo por linha, por exemplo https://192.168.1.2:18789. Preencha manualmente se o endereço padrão não estiver configurado.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'Informe pelo menos um endereço de acesso', allowedOriginsInvalid: 'Use o formato http(s)://host-ou-ip:porta', + allowedOriginsHttpsOnly: 'Os endereços de acesso devem começar com https://', provider: 'Provedor de modelos', apiKey: 'Chave API', baseUrl: 'URL base', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index ab9737119023..41c0fa1e9c33 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -679,13 +679,13 @@ const message = { copawType: 'CoPaw', appVersion: 'Версия приложения', webuiPort: 'Порт WebUI', - bridgePort: 'Порт Bridge', allowedOrigins: 'Адреса доступа', allowedOriginsHelper: - 'Указывайте по одному полному адресу доступа в строке, например http://192.168.1.2:18789. Если адрес по умолчанию не настроен, введите его вручную.', - allowedOriginsPlaceholder: 'http://192.168.1.2:18789', + 'Указывайте по одному полному HTTPS-адресу доступа в строке, например https://192.168.1.2:18789. Если адрес по умолчанию не настроен, введите его вручную.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'Укажите хотя бы один адрес доступа', allowedOriginsInvalid: 'Используйте формат http(s)://host-or-ip:port', + allowedOriginsHttpsOnly: 'Адреса доступа должны начинаться с https://', provider: 'Поставщик моделей', apiKey: 'API ключ', baseUrl: 'Базовый URL', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index eb95c0a6c140..5fb133e7f12f 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -683,13 +683,13 @@ const message = { copawType: 'CoPaw', 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', + 'Her satıra bir tam HTTPS erişim adresi girin. Örnek: https://192.168.1.2:18789. Varsayılan erişim adresi yapılandırılmamışsa elle girin.', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: 'En az bir erişim adresi girin', allowedOriginsInvalid: 'http(s)://host-veya-ip:port biçimini kullanın', + allowedOriginsHttpsOnly: 'Erişim adresleri https:// ile başlamalıdır', provider: 'Model sağlayıcı', apiKey: 'API anahtarı', baseUrl: 'Temel URL', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index c33f25d78d8f..861190afc032 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -647,12 +647,13 @@ const message = { copawType: 'CoPaw', appVersion: '應用版本', webuiPort: 'WebUI 埠', - bridgePort: 'Bridge 埠', allowedOrigins: '訪問地址', - allowedOriginsHelper: '一行一個完整訪問地址,例如 http://192.168.1.2:18789;未設定預設訪問地址時請手動填寫', - allowedOriginsPlaceholder: 'http://192.168.1.2:18789', + allowedOriginsHelper: + '一行一個完整 HTTPS 訪問地址,例如 https://192.168.1.2:18789;未設定預設訪問地址時請手動填寫', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: '請至少填寫一個訪問地址', allowedOriginsInvalid: '訪問地址格式錯誤,請輸入 http(s)://網域或IP:埠', + allowedOriginsHttpsOnly: '訪問地址必須以 https:// 開頭', provider: '模型供應商', apiKey: 'API Key', baseUrl: 'Base URL', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index cb3ebc00b6e1..69db5b395b93 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -646,12 +646,13 @@ const message = { copawType: 'CoPaw', appVersion: '应用版本', webuiPort: 'WebUI 端口', - bridgePort: 'Bridge 端口', allowedOrigins: '访问地址', - allowedOriginsHelper: '一行一个完整访问地址,例如 http://192.168.1.2:18789;未配置默认访问地址时请手动填写', - allowedOriginsPlaceholder: 'http://192.168.1.2:18789', + allowedOriginsHelper: + '一行一个完整 HTTPS 访问地址,例如 https://192.168.1.2:18789;未配置默认访问地址时请手动填写', + allowedOriginsPlaceholder: 'https://192.168.1.2:18789', allowedOriginsRequired: '请至少填写一个访问地址', allowedOriginsInvalid: '访问地址格式错误,请输入 http(s)://域名或IP:端口', + allowedOriginsHttpsOnly: '访问地址必须以 https:// 开头', provider: '模型供应商', apiKey: 'API Key', baseUrl: 'Base URL', diff --git a/frontend/src/utils/agent.ts b/frontend/src/utils/agent.ts index 8472ba09ad76..c752ccd69b26 100644 --- a/frontend/src/utils/agent.ts +++ b/frontend/src/utils/agent.ts @@ -1,5 +1,9 @@ import i18n from '@/lang'; +interface AllowedOriginOptions { + httpsOnly?: boolean; +} + export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => { if (provider === 'custom' || displayName === 'Custom') { return i18n.global.t('container.custom'); @@ -15,10 +19,11 @@ export const buildDefaultAllowedOrigin = (systemIP: string, port?: number | stri if (!target || !port) { return ''; } - return `http://${target}:${port}`; + const host = target.includes(':') && !target.startsWith('[') && !target.endsWith(']') ? `[${target}]` : target; + return `https://${host}:${port}`; }; -export const normalizeAllowedOrigin = (value: string): string => { +export const normalizeAllowedOrigin = (value: string, options: AllowedOriginOptions = {}): string => { const target = String(value || '').trim(); if (!target) { throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid')); @@ -29,6 +34,9 @@ export const normalizeAllowedOrigin = (value: string): string => { } catch (error) { throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid')); } + if (options.httpsOnly && parsed.protocol !== 'https:') { + throw new Error(i18n.global.t('aiTools.agents.allowedOriginsHttpsOnly')); + } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid')); } @@ -38,13 +46,13 @@ export const normalizeAllowedOrigin = (value: string): string => { if (parsed.pathname && parsed.pathname !== '/') { throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid')); } - if (!parsed.host) { + if (!parsed.host || !parsed.port) { throw new Error(i18n.global.t('aiTools.agents.allowedOriginsInvalid')); } - return `${parsed.protocol}//${parsed.host}`; + return `https://${parsed.host}`; }; -export const parseAllowedOriginsInput = (value: string): string[] => { +export const parseAllowedOriginsInput = (value: string, options: AllowedOriginOptions = {}): string[] => { const result: string[] = []; const seen = new Set(); for (const line of String(value || '').split(/\r?\n/)) { @@ -52,7 +60,7 @@ export const parseAllowedOriginsInput = (value: string): string[] => { if (!target) { continue; } - const normalized = normalizeAllowedOrigin(target); + const normalized = normalizeAllowedOrigin(target, options); if (seen.has(normalized)) { continue; } @@ -62,9 +70,9 @@ export const parseAllowedOriginsInput = (value: string): string[] => { return result; }; -export const validateAllowedOriginsInput = (value: string): string => { +export const validateAllowedOriginsInput = (value: string, options: AllowedOriginOptions = {}): string => { try { - const parsed = parseAllowedOriginsInput(value); + const parsed = parseAllowedOriginsInput(value, options); if (parsed.length === 0) { return i18n.global.t('aiTools.agents.allowedOriginsRequired'); } diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index 55c9e3ce8395..ccc4a8697533 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -19,13 +19,6 @@ - - - { form.baseURL = ''; form.apiType = 'openai-completions'; if (form.agentType === 'openclaw') { - form.bridgePort = form.bridgePort || 18790; await loadSystemIP(); allowedOriginsAutoFilled.value = true; syncAllowedOriginsWithDefault(true); @@ -417,7 +407,6 @@ const submit = async () => { name: form.name, 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, diff --git a/frontend/src/views/ai/agents/agent/config/tabs/settings/security.vue b/frontend/src/views/ai/agents/agent/config/tabs/settings/security.vue index b7e9dab1d79f..2a1ee24e8cd9 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/settings/security.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/settings/security.vue @@ -41,7 +41,7 @@ const rules = reactive({ allowedOrigins: [ { validator: (_rule: any, value: any, callback: (error?: Error) => void) => { - const message = validateAllowedOriginsInput(String(value || '')); + const message = validateAllowedOriginsInput(String(value || ''), { httpsOnly: true }); if (message) { callback(new Error(message)); return; @@ -73,7 +73,7 @@ const saveConfig = async () => { try { await updateAgentSecurityConfig({ agentId: agentId.value, - allowedOrigins: parseAllowedOriginsInput(form.allowedOrigins), + allowedOrigins: parseAllowedOriginsInput(form.allowedOrigins, { httpsOnly: true }), }); MsgSuccess(t('aiTools.agents.saveSuccess')); } finally { diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index 009de3c1f200..1e19d6b8513f 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -75,11 +75,6 @@ {{ $t('aiTools.agents.webuiPort') }}: {{ row.webUIPort }} -
- - {{ $t('aiTools.agents.bridgePort') }} {{ row.bridgePort }} - -
@@ -154,6 +149,7 @@ import PortJumpDialog from '@/components/port-jump/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue'; import { getAgentProviderDisplayName } from '@/utils/agent'; import { routerToFileWithPath } from '@/utils/router'; +import { compareVersion } from '@/utils/version'; import NoApp from '@/views/app-store/apps/no-app/index.vue'; import openclawIcon from '@/assets/images/ai-agent-openclaw.svg'; import copawIcon from '@/assets/images/ai-agent-copaw.svg'; @@ -321,11 +317,24 @@ const openWorkDir = (row: AI.AgentItem) => { routerToFileWithPath(`${row.path}/data`); }; +const isOpenClawHttpsVersion = (version: string) => { + const target = String(version || '') + .trim() + .toLowerCase(); + if (!target || target === 'latest') { + return true; + } + if (!/\d/.test(target)) { + return true; + } + return compareVersion(target, '2026.3.12'); +}; + const jumpWebUI = (row: AI.AgentItem) => { if (dialogPortJumpRef.value?.acceptParams) { dialogPortJumpRef.value.acceptParams({ port: row.webUIPort, - protocol: 'http', + protocol: row.agentType === 'openclaw' && isOpenClawHttpsVersion(row.appVersion) ? 'https' : 'http', path: row.agentType === 'copaw' ? undefined : '/', hash: row.agentType === 'copaw' ? undefined : `token=${row.token}`, });