Files
1Panel/agent/app/service/agents_utils.go
CityFun d402f67fc3 feat: Optimize application upgrade logic (#13549)
* feat: Optimize application upgrade logic

* feat: Optimize application upgrade logic

* feat: Optimize application upgrade logic
2026-08-13 18:28:59 +08:00

1630 lines
48 KiB
Go

package service
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
"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/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm"
)
type AgentService struct{}
func NewIAgentService() IAgentService {
return &AgentService{}
}
type resolvedAgentAccountInput struct {
Provider string
APIKey string
BaseURL string
APIType string
AuthMode string
}
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
if excludeID > 0 {
opts = append(opts, repo.WithByNOTID(excludeID))
}
account, err := agentAccountRepo.GetFirst(opts...)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if account != nil && account.ID > 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
var settings []model.Setting
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
return false, err
}
values := make(map[string]string, len(settings))
for _, setting := range settings {
values[setting.Key] = setting.Value
}
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
}
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
agent, err := agentRepo.GetFirst(repo.WithByID(agentID))
if err != nil {
return nil, err
}
if agent.AgentType != constant.AppOpenclaw {
return nil, fmt.Errorf("%s does not support", agent.AgentType)
}
return agent, nil
}
func ensureContainerRunning(containerName string) error {
status, err := checkContainerStatus(containerName)
if err != nil {
return err
}
if status != "running" {
return fmt.Errorf("container %s is not running, please check and retry", containerName)
}
return nil
}
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedAPIType := strings.TrimSpace(apiType)
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
return resolvedAgentAccountInput{}, err
}
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
if err != nil {
if strings.Contains(err.Error(), "base url is required") {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
}
return resolvedAgentAccountInput{}, err
}
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
}
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err
}
}
return resolvedAgentAccountInput{
Provider: provider,
APIKey: resolvedAPIKey,
BaseURL: resolvedBaseURL,
APIType: resolvedAPIType,
AuthMode: resolvedAuthMode,
}, nil
}
func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
if strings.TrimSpace(configPath) == "" {
return nil, buserr.New("ErrRecordNotFound")
}
fileOp := files.NewFileOp()
content, err := fileOp.GetContent(configPath)
if err != nil {
return nil, err
}
conf := map[string]interface{}{}
if err := json.Unmarshal(content, &conf); err != nil {
return nil, err
}
return conf, nil
}
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
ensureGatewaySecurityDefaults(conf)
ensureOpenclawUpdateDefaults(conf)
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
fileOp := files.NewFileOp()
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 parsed.Port() != "" {
normalized += ":" + parsed.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))
}
}
}
return result
}
func setSecurityConfig(conf map[string]interface{}, config dto.AgentSecurityConfig) {
ensureGatewaySecurityDefaults(conf)
gateway := ensureChildMap(conf, "gateway")
controlUi := ensureChildMap(gateway, "controlUi")
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 ensureOpenclawUpdateDefaults(conf map[string]interface{}) {
update := ensureChildMap(conf, "update")
if _, ok := update["checkOnStart"]; !ok {
update["checkOnStart"] = false
}
}
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 extractBrowserConfig(conf map[string]interface{}) browserConfig {
result := browserConfig{
Enabled: true,
ExecutablePath: defaultBrowserExecutablePath,
Headless: true,
NoSandbox: true,
DefaultProfile: defaultBrowserProfile,
}
browser, ok := conf["browser"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := browser["enabled"].(bool); ok {
result.Enabled = enabled
}
if executablePath, ok := browser["executablePath"].(string); ok && strings.TrimSpace(executablePath) != "" {
result.ExecutablePath = executablePath
}
if headless, ok := browser["headless"].(bool); ok {
result.Headless = headless
}
if noSandbox, ok := browser["noSandbox"].(bool); ok {
result.NoSandbox = noSandbox
}
if defaultProfile, ok := browser["defaultProfile"].(string); ok && strings.TrimSpace(defaultProfile) != "" {
result.DefaultProfile = defaultProfile
}
return result
}
func setBrowserConfig(conf map[string]interface{}, config browserConfig) {
browser := ensureChildMap(conf, "browser")
browser["enabled"] = config.Enabled
browser["executablePath"] = defaultBrowserExecutablePath
browser["headless"] = config.Headless
browser["noSandbox"] = config.NoSandbox
if strings.TrimSpace(config.DefaultProfile) == "" {
browser["defaultProfile"] = defaultBrowserProfile
} else {
browser["defaultProfile"] = strings.TrimSpace(config.DefaultProfile)
}
}
func extractOtherConfig(conf map[string]interface{}) dto.AgentOtherConfig {
result := dto.AgentOtherConfig{
UserTimezone: resolveServerTimezone(),
BrowserEnabled: true,
NPMRegistry: defaultOpenclawNPMRegistry,
}
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
}
func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
agents := ensureChildMap(conf, "agents")
defaults := ensureChildMap(agents, "defaults")
timezone := strings.TrimSpace(config.UserTimezone)
if timezone == "" {
timezone = resolveServerTimezone()
}
defaults["userTimezone"] = timezone
setBrowserConfig(conf, browserConfig{
Enabled: config.BrowserEnabled,
ExecutablePath: defaultBrowserExecutablePath,
Headless: true,
NoSandbox: true,
DefaultProfile: defaultBrowserProfile,
})
}
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := agent.AgentType
item := dto.AgentItem{
ID: agent.ID,
Name: agent.Name,
Remark: agent.Remark,
AgentType: agentType,
Provider: agent.Provider,
ProviderName: localizedAgentProviderName(agent.Provider),
Model: agent.Model,
APIType: agent.APIType,
BaseURL: agent.BaseURL,
APIKey: maskKey(agent.APIKey),
Token: agent.Token,
Status: agent.Status,
Message: agent.Message,
AppInstallID: agent.AppInstallID,
WebsiteID: agent.WebsiteID,
AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
}
if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName
item.AppVersion = appInstall.Version
if agentType == constant.AppOpenclaw {
if isOpenclawHTTPSWindowVersion(appInstall.Version) {
item.WebUIPort = appInstall.HttpsPort
} else {
item.WebUIPort = appInstall.HttpPort
}
} else {
item.WebUIPort = appInstall.HttpPort
}
item.Path = appInstall.GetPath()
item.Status = appInstall.Status
item.Message = appInstall.Message
if envMap != nil {
if bridge, ok := envMap["PANEL_APP_PORT_BRIDGE"]; ok {
item.BridgePort = toInt(bridge)
}
}
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
}
return item
}
func localizedAgentProviderName(provider string) string {
if key := providercatalog.DisplayNameKey(provider); key != "" {
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
return name
}
}
return providercatalog.DisplayName(provider)
}
func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
}
func isOpenclawLegacyHTTPVersion(version string) bool {
return !common.CompareAppVersion(version, openclawHTTPSVersion)
}
func isOpenclawHTTPSWindowVersion(version string) bool {
return common.CompareAppVersion(version, openclawHTTPSVersion) && !common.CompareAppVersion(version, openclawHTTPVersion)
}
func isOpenclawCurrentHTTPVersion(version string) bool {
return common.CompareAppVersion(version, openclawHTTPVersion)
}
func shouldMigrateOpenclawHTTPSUpgrade(install *model.AppInstall, fromVersion, toVersion string) bool {
if install == nil || install.App.Key != constant.AppOpenclaw {
return false
}
return isOpenclawLegacyHTTPVersion(fromVersion) && isOpenclawHTTPSWindowVersion(toVersion)
}
func shouldMigrateOpenclawHTTPUpgrade(install *model.AppInstall, fromVersion, toVersion string) bool {
if install == nil || install.App.Key != constant.AppOpenclaw {
return false
}
return !isOpenclawCurrentHTTPVersion(fromVersion) && isOpenclawCurrentHTTPVersion(toVersion)
}
func migrateOpenclawProtocolUpgrade(install *model.AppInstall, fromVersion, toVersion string) error {
systemIP, _ := settingRepo.GetValueByKey("SystemIP")
return migrateOpenclawProtocolUpgradeWithSystemIP(install, fromVersion, toVersion, systemIP)
}
func migrateOpenclawProtocolUpgradeWithSystemIP(install *model.AppInstall, fromVersion, toVersion, systemIP string) error {
if shouldMigrateOpenclawHTTPSUpgrade(install, fromVersion, toVersion) {
return applyOpenclawProtocolUpgradeWithSystemIP(install, toVersion, systemIP, true)
}
if shouldMigrateOpenclawHTTPUpgrade(install, fromVersion, toVersion) {
return applyOpenclawProtocolUpgradeWithSystemIP(install, toVersion, systemIP, false)
}
return nil
}
func applyOpenclawProtocolUpgradeWithSystemIP(install *model.AppInstall, toVersion, systemIP string, useHTTPS bool) error {
if useHTTPS {
migrateOpenclawInstallPortsToHTTPS(install)
} else {
migrateOpenclawInstallPortsToHTTP(install)
}
configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json")
var allowedOrigins []string
if conf, err := readOpenclawConfig(configPath); err == nil {
allowedOrigins = extractSecurityConfig(conf).AllowedOrigins
}
originHost := strings.TrimSpace(systemIP)
if originHost == "" {
originHost = openclawAllowedOriginHost
}
port := install.HttpPort
if useHTTPS {
port = install.HttpsPort
}
if port > 0 {
allowedOrigin, err := buildOpenclawAllowedOrigin(openclawAllowedOriginScheme(toVersion), originHost, port)
if err == nil {
conf, err := readOpenclawConfig(configPath)
if err != nil {
return err
}
allowedOrigins = []string{allowedOrigin}
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
}
}
return migrateOpenclawInstallEnv(install, allowedOrigins)
}
func openclawAllowedOriginScheme(version string) string {
if isOpenclawHTTPSWindowVersion(version) {
return "https"
}
return "http"
}
func migrateOpenclawInstallPortsToHTTPS(install *model.AppInstall) {
if install == nil {
return
}
if install.HttpsPort == 0 && install.HttpPort > 0 {
install.HttpsPort = install.HttpPort
}
if install.HttpPort > 0 {
install.HttpPort = 0
}
}
func migrateOpenclawInstallPortsToHTTP(install *model.AppInstall) {
if install == nil {
return
}
if install.HttpPort == 0 && install.HttpsPort > 0 {
install.HttpPort = install.HttpsPort
}
if install.HttpsPort > 0 {
install.HttpsPort = 0
}
}
func migrateOpenclawInstallEnv(install *model.AppInstall, allowedOrigins []string) error {
if install == nil {
return nil
}
envMap := make(map[string]interface{})
if strings.TrimSpace(install.Env) != "" {
if err := json.Unmarshal([]byte(install.Env), &envMap); err != nil {
return err
}
}
if install.HttpsPort > 0 {
envMap["PANEL_APP_PORT_HTTPS"] = install.HttpsPort
} else {
delete(envMap, "PANEL_APP_PORT_HTTPS")
}
if install.HttpPort > 0 {
envMap["PANEL_APP_PORT_HTTP"] = install.HttpPort
}
if install.HttpPort == 0 {
delete(envMap, "PANEL_APP_PORT_HTTP")
}
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
envMap["ALLOWED_ORIGIN"] = allowedOrigin
} else {
delete(envMap, "ALLOWED_ORIGIN")
}
payload, err := json.Marshal(envMap)
if err != nil {
return err
}
install.Env = string(payload)
return nil
}
func syncOpenclawAllowedOriginEnv(install *model.AppInstall, allowedOrigins []string) error {
if install == nil {
return nil
}
envMap := make(map[string]interface{})
if strings.TrimSpace(install.Env) != "" {
if err := json.Unmarshal([]byte(install.Env), &envMap); err != nil {
return err
}
}
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
envMap["ALLOWED_ORIGIN"] = allowedOrigin
} else {
delete(envMap, "ALLOWED_ORIGIN")
}
payload, err := json.Marshal(envMap)
if err != nil {
return err
}
install.Env = string(payload)
return nil
}
func firstAllowedOrigin(allowedOrigins []string) string {
for _, origin := range allowedOrigins {
trimmed := strings.TrimSpace(origin)
if trimmed != "" {
return trimmed
}
}
return ""
}
func buildOpenclawAllowedOrigin(scheme, host string, port int) (string, error) {
scheme = strings.TrimSpace(strings.ToLower(scheme))
host = strings.TrimSpace(host)
if (scheme != "http" && scheme != "https") || host == "" || port <= 0 {
return "", fmt.Errorf("invalid openclaw allowed origin")
}
if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") && strings.Count(host, ":") > 1 {
host = "[" + host + "]"
}
return normalizeAllowedOrigin(fmt.Sprintf("%s://%s:%d", scheme, host, port))
}
func checkAgentUpgradable(install model.AppInstall) bool {
if install.ID == 0 || install.Version == "" {
return false
}
if install.App.ID == 0 {
return false
}
if ignoreUpdate(install) {
return false
}
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(install.App.ID))
if err != nil || len(details) == 0 {
return false
}
versions := make([]string, 0, len(details))
for _, item := range details {
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(item.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 {
continue
}
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
continue
}
versions = append(versions, item.Version)
}
if len(versions) == 0 {
return false
}
versions = common.GetSortedVersions(versions)
lastVersion := versions[0]
if common.IsCrossVersion(install.Version, lastVersion) {
return install.App.CrossVersionUpdate
}
return common.CompareVersion(lastVersion, install.Version)
}
type openclawConfig struct {
Gateway gatewayConfig `json:"gateway"`
Agents agentsConfig `json:"agents"`
Browser browserConfig `json:"browser"`
Tools toolsConfig `json:"tools"`
Update updateConfig `json:"update"`
Models *modelsConfig `json:"models,omitempty"`
}
type toolsConfig struct {
Profile string `json:"profile,omitempty"`
Sessions toolSessionsConfig `json:"sessions,omitempty"`
}
type toolSessionsConfig struct {
Visibility string `json:"visibility,omitempty"`
}
type updateConfig struct {
CheckOnStart bool `json:"checkOnStart"`
}
type gatewayConfig struct {
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 {
DangerouslyDisableDeviceAuth bool `json:"dangerouslyDisableDeviceAuth"`
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
}
type gatewayAuth struct {
Mode string `json:"mode"`
Token string `json:"token"`
}
type agentsConfig struct {
Defaults agentDefaults `json:"defaults"`
}
type agentDefaults struct {
UserTimezone string `json:"userTimezone,omitempty"`
Model modelRef `json:"model"`
Models map[string]map[string]interface{} `json:"models,omitempty"`
}
type modelRef struct {
Primary string `json:"primary"`
Fallbacks []string `json:"fallbacks,omitempty"`
}
type modelsConfig struct {
Mode string `json:"mode,omitempty"`
Providers map[string]modelProvider `json:"providers,omitempty"`
}
type modelProvider struct {
ApiKey string `json:"apiKey,omitempty"`
BaseUrl string `json:"baseUrl,omitempty"`
Api string `json:"api,omitempty"`
AuthHeader bool `json:"authHeader,omitempty"`
Models []modelEntry `json:"models,omitempty"`
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Input []string `json:"input,omitempty"`
}
func requiresOpenclawProviderModels(provider string) bool {
return provider != "gemini"
}
func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig) error {
if models == nil {
delete(conf, "models")
return nil
}
modelsMap, err := structToMap(models)
if err != nil {
return err
}
conf["models"] = modelsMap
return nil
}
type browserConfig struct {
Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"`
Headless bool `json:"headless"`
NoSandbox bool `json:"noSandbox"`
DefaultProfile string `json:"defaultProfile"`
}
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
}
if account == nil {
return fmt.Errorf("account is required")
}
if strings.TrimSpace(modelName) == "" {
return fmt.Errorf("model is required")
}
if strings.TrimSpace(token) == "" {
return fmt.Errorf("gateway token is required")
}
fileOp := files.NewFileOp()
if !fileOp.Stat(confDir) {
if err := fileOp.CreateDir(confDir, constant.DirPerm); err != nil {
return err
}
}
primaryModel, defaultsModels, models, err := buildOpenclawModelsFromAccount(account, modelName)
if err != nil {
return err
}
resolvedFallbacks, err := resolveOpenclawFallbackModels(account, modelName, fallbacks)
if err != nil {
return err
}
cfg := openclawConfig{
Gateway: gatewayConfig{
Mode: "local",
Bind: "lan",
Port: openclawGatewayPort,
Auth: gatewayAuth{
Mode: "token",
Token: token,
},
ControlUi: gatewayControlUi{
DangerouslyDisableDeviceAuth: true,
AllowedOrigins: append([]string(nil), allowedOrigins...),
},
TrustedProxies: []string{openclawTrustedProxyLoopback},
},
Agents: agentsConfig{
Defaults: agentDefaults{
UserTimezone: resolveServerTimezone(),
Model: modelRef{Primary: primaryModel, Fallbacks: resolvedFallbacks},
Models: defaultsModels,
},
},
Browser: browserConfig{
Enabled: true,
ExecutablePath: defaultBrowserExecutablePath,
Headless: true,
NoSandbox: true,
DefaultProfile: defaultBrowserProfile,
},
Tools: toolsConfig{
Profile: defaultToolsProfile,
Sessions: toolSessionsConfig{
Visibility: defaultToolsSessionVisibility,
},
},
Update: updateConfig{
CheckOnStart: false,
},
Models: models,
}
configPath := path.Join(confDir, "openclaw.json")
conf := map[string]interface{}{}
if fileOp.Stat(configPath) {
existing, err := readOpenclawConfig(configPath)
if err != nil {
return err
}
conf = existing
}
if len(conf) == 0 {
initial, err := structToMap(cfg)
if err != nil {
return err
}
conf = initial
} else {
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
return err
}
if _, ok := conf["browser"]; !ok {
browserMap, err := structToMap(cfg.Browser)
if err != nil {
return err
}
conf["browser"] = browserMap
}
toolsMap := ensureChildMap(conf, "tools")
if profile, ok := toolsMap["profile"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", profile)) == "" {
toolsMap["profile"] = defaultToolsProfile
}
sessionsMap := ensureChildMap(toolsMap, "sessions")
if visibility, ok := sessionsMap["visibility"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", visibility)) == "" {
sessionsMap["visibility"] = defaultToolsSessionVisibility
}
agentsMap := ensureChildMap(conf, "agents")
defaultsMap := ensureChildMap(agentsMap, "defaults")
if tz, ok := defaultsMap["userTimezone"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", tz)) == "" {
defaultsMap["userTimezone"] = resolveServerTimezone()
}
modelMap := ensureChildMap(defaultsMap, "model")
modelMap["primary"] = cfg.Agents.Defaults.Model.Primary
if len(cfg.Agents.Defaults.Model.Fallbacks) > 0 {
modelMap["fallbacks"] = cfg.Agents.Defaults.Model.Fallbacks
} else {
delete(modelMap, "fallbacks")
}
if cfg.Agents.Defaults.Models != nil {
defaultsMap["models"] = cfg.Agents.Defaults.Models
}
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"
}
authMap["token"] = token
}
if allowedOrigins != nil {
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
}
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
envMap := map[string]string{
"OPENCLAW_GATEWAY_TOKEN": token,
}
order := []string{"OPENCLAW_GATEWAY_TOKEN"}
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey
order = append(order, envKey)
}
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
}
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return nil, err
}
return resolveOpenclawFallbackModelsFromPool(account, accountModels, primaryModel, fallbackIDs, true)
}
func resolveOpenclawFallbackModelsFromPool(account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string, fallbackIDs []string, strict bool) ([]string, error) {
if len(fallbackIDs) == 0 {
return nil, nil
}
result := make([]string, 0, len(fallbackIDs))
seen := make(map[string]struct{}, len(fallbackIDs))
for _, item := range fallbackIDs {
target := strings.TrimSpace(item)
if target == "" {
continue
}
accountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, target)
if err != nil {
if strict {
return nil, err
}
continue
}
if sameProviderModelID(account.Provider, accountModel.ID, primaryModel) {
if strict {
return nil, buserr.New("ErrAgentFallbackModelPrimary")
}
continue
}
resolvedPrimary, _, _, _, err := buildOpenclawAccountModelConfig(account, accountModel)
if err != nil {
return nil, err
}
if _, ok := seen[resolvedPrimary]; ok {
if strict {
return nil, buserr.New("ErrAgentFallbackModelDuplicate")
}
continue
}
seen[resolvedPrimary] = struct{}{}
result = append(result, resolvedPrimary)
}
return result, nil
}
func extractOpenclawPrimaryModelID(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) string {
modelMap := openclawModelRefMap(conf)
primary := strings.TrimSpace(fmt.Sprintf("%v", modelMap["primary"]))
if primary == "" {
return ""
}
resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels)
if err != nil {
return ""
}
return resolved[primary]
}
func extractOpenclawFallbackModelIDs(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string) []string {
modelMap := openclawModelRefMap(conf)
raw, ok := modelMap["fallbacks"].([]interface{})
if !ok || len(raw) == 0 {
return nil
}
resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels)
if err != nil {
return nil
}
result := make([]string, 0, len(raw))
seen := make(map[string]struct{}, len(raw))
for _, item := range raw {
target := strings.TrimSpace(fmt.Sprintf("%v", item))
modelID, ok := resolved[target]
if !ok || sameProviderModelID(account.Provider, modelID, primaryModel) {
continue
}
if _, ok := seen[modelID]; ok {
continue
}
seen[modelID] = struct{}{}
result = append(result, modelID)
}
return result
}
func openclawModelRefMap(conf map[string]interface{}) map[string]interface{} {
agentsMap, ok := conf["agents"].(map[string]interface{})
if !ok {
return nil
}
defaultsMap, ok := agentsMap["defaults"].(map[string]interface{})
if !ok {
return nil
}
modelMap, ok := defaultsMap["model"].(map[string]interface{})
if !ok {
return nil
}
return modelMap
}
func buildOpenclawResolvedModelIDMap(account *model.AgentAccount, accountModels []dto.AgentAccountModel) (map[string]string, error) {
result := make(map[string]string, len(accountModels))
for _, item := range accountModels {
resolvedPrimary, _, _, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
return nil, err
}
result[resolvedPrimary] = item.ID
}
return result, nil
}
func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error {
if appInstall == nil {
return fmt.Errorf("app install is required")
}
confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
return err
}
dataDir := path.Join(appInstall.GetPath(), "data")
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
}
func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel string) (string, map[string]map[string]interface{}, *modelsConfig, error) {
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return "", nil, nil, err
}
if len(accountModels) == 0 {
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
}
if selectedModel == "" {
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
}
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, selectedModel)
if err != nil {
return "", nil, nil, err
}
selectedModel = selectedAccountModel.ID
providerKey := ""
providerCfg := modelProvider{}
entries := make([]modelEntry, 0, len(accountModels))
primaryModel := ""
defaultsModels := make(map[string]map[string]interface{}, len(accountModels))
for _, item := range accountModels {
resolvedPrimary, entry, key, baseCfg, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
return "", nil, nil, err
}
if providerKey == "" {
providerKey = key
providerCfg.ApiKey = baseCfg.ApiKey
providerCfg.BaseUrl = baseCfg.BaseUrl
providerCfg.Api = baseCfg.Api
providerCfg.AuthHeader = baseCfg.AuthHeader
}
entries = append(entries, entry)
defaultsModels[resolvedPrimary] = map[string]interface{}{}
if sameProviderModelID(account.Provider, item.ID, selectedModel) {
primaryModel = resolvedPrimary
}
}
if primaryModel == "" {
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
}
if !requiresOpenclawProviderModels(account.Provider) {
return primaryModel, defaultsModels, nil, nil
}
providerCfg.Models = entries
return primaryModel, defaultsModels, &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
providerKey: providerCfg,
},
}, nil
}
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey)
if err != nil {
return "", modelEntry{}, "", modelProvider{}, err
}
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
ApiKey: providerPatch.APIKey,
BaseUrl: providerPatch.BaseURL,
Api: providerPatch.APIType,
AuthHeader: providerPatch.AuthHeader,
}, nil
}
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
name := strings.TrimSpace(model.Name)
if name == "" {
name = strings.TrimSpace(modelID)
}
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name}
if openclawVisionModelPattern.MatchString(modelID) {
entry.Input = []string{"text", "image"}
}
return entry
}
type openclawAccountModelRuntime struct {
StoredModel string
PrimaryModel string
APIType string
}
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
primaryModel, _, _, _, err := buildOpenclawAccountModelConfig(account, model)
if err != nil {
return openclawAccountModelRuntime{}, err
}
return openclawAccountModelRuntime{
StoredModel: model.ID,
PrimaryModel: primaryModel,
APIType: account.APIType,
}, nil
}
func resolveOpenclawAccountModelRuntimeByID(account *model.AgentAccount, modelID string) (openclawAccountModelRuntime, error) {
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return openclawAccountModelRuntime{}, err
}
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, modelID)
if err != nil {
return openclawAccountModelRuntime{}, err
}
return buildOpenclawAccountModelRuntime(account, selectedAccountModel)
}
func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, fmt.Errorf("account is required")
}
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
}
if len(requested) > 0 {
return normalizeAgentAccountModels(account, requested)
}
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
if len(defaultModels) == 0 {
if requiresInitialAgentAccountModels(account.Provider) {
return nil, buserr.New("ErrAgentAccountModelsRequired")
}
return nil, nil
}
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
for _, item := range defaultModels {
requested = append(requested, dto.AgentAccountModel{
ID: item.ID,
Name: item.Name,
})
}
return normalizeAgentAccountModels(account, requested)
}
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
for _, modelID := range modelIDs {
models = append(models, dto.AgentAccountModel{
ID: modelID,
Name: modelID,
})
}
return models
}
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
if err != nil {
return err
}
for index := range rows {
order := index + 1
if rows[index].SortOrder == order {
continue
}
rows[index].SortOrder = order
if err := agentAccountModelRepo.Save(&rows[index]); err != nil {
return err
}
}
return nil
}
func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, fmt.Errorf("account is required")
}
return listPersistedAgentAccountModels(account.ID)
}
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, fmt.Errorf("account is required")
}
meta, ok := providercatalog.Get(account.Provider)
if !ok || len(meta.Models) == 0 {
return append([]dto.AgentAccountModel(nil), existing...), nil
}
requested := append([]dto.AgentAccountModel(nil), existing...)
seen := make(map[string]struct{}, len(existing))
for _, item := range existing {
target := strings.TrimSpace(item.ID)
if target == "" {
continue
}
seen[target] = struct{}{}
}
for _, item := range meta.Models {
target := strings.TrimSpace(item.ID)
if _, ok := seen[target]; ok {
continue
}
requested = append(requested, dto.AgentAccountModel{
ID: item.ID,
Name: item.Name,
})
}
if len(requested) == len(existing) {
return append([]dto.AgentAccountModel(nil), existing...), nil
}
return normalizeAgentAccountModels(account, requested)
}
func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, error) {
if accountID == 0 {
return nil, nil
}
rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
if err != nil {
return nil, err
}
result := make([]dto.AgentAccountModel, 0, len(rows))
for _, row := range rows {
result = append(result, dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
})
}
return result, nil
}
func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, models []dto.AgentAccountModel) error {
if err := tx.Where("account_id = ?", accountID).Delete(&model.AgentAccountModel{}).Error; err != nil {
return err
}
for index, item := range models {
record := &model.AgentAccountModel{
AccountID: accountID,
Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name),
SortOrder: index + 1,
}
if err := tx.Create(record).Error; err != nil {
return err
}
}
return nil
}
func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
requested := append([]dto.AgentAccountModel(nil), models...)
if len(requested) == 0 {
return nil, fmt.Errorf("model is required")
}
normalized := make([]dto.AgentAccountModel, 0, len(requested))
seen := make(map[string]struct{}, len(requested))
for _, item := range requested {
normalizedItem, err := normalizeAgentAccountModel(account, item)
if err != nil {
return nil, err
}
if strings.TrimSpace(normalizedItem.ID) == "" {
continue
}
if _, ok := seen[normalizedItem.ID]; ok {
continue
}
seen[normalizedItem.ID] = struct{}{}
normalized = append(normalized, normalizedItem)
}
if len(normalized) == 0 {
return nil, fmt.Errorf("model is required")
}
return normalized, nil
}
func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAccountModel) (dto.AgentAccountModel, error) {
modelID := strings.TrimSpace(model.ID)
if modelID == "" {
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
}
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
name := strings.TrimSpace(model.Name)
if name == "" {
name = modelID
}
return dto.AgentAccountModel{
ID: modelID,
Name: name,
}, nil
}
func requiresInitialAgentAccountModels(provider string) bool {
switch provider {
case "custom", "vllm", "ollama":
return true
default:
return false
}
}
func sameProviderModelID(provider, left, right string) bool {
leftTrimmed := strings.TrimSpace(left)
rightTrimmed := strings.TrimSpace(right)
if leftTrimmed == rightTrimmed {
return true
}
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
return leftComparable != "" && leftComparable == rightComparable
}
func findAgentAccountModelForProvider(provider string, models []dto.AgentAccountModel, modelID string) (dto.AgentAccountModel, bool) {
for _, item := range models {
if sameProviderModelID(provider, item.ID, modelID) {
return item, true
}
}
return dto.AgentAccountModel{}, false
}
func requireAgentAccountModelForProvider(provider string, models []dto.AgentAccountModel, modelID string) (dto.AgentAccountModel, error) {
selectedAccountModel, ok := findAgentAccountModelForProvider(provider, models, modelID)
if !ok {
return dto.AgentAccountModel{}, buserr.New("ErrAgentModelNotInAccount")
}
return selectedAccountModel, nil
}
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
if len(models) == 0 {
return "", buserr.New("ErrAgentAccountModelsRequired")
}
if strings.TrimSpace(requested) == "" {
return models[0].ID, nil
}
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
if !ok {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return selected.ID, nil
}
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
if account == nil || account.ID == 0 {
return nil
}
agents, err := agentRepo.List(repo.WithByAccountID(account.ID))
if err != nil {
return err
}
for _, agent := range agents {
if _, ok := findAgentAccountModelForProvider(account.Provider, models, agent.Model); !ok {
return buserr.WithName("ErrAgentModelInUse", agent.Name)
}
}
return nil
}
func resolveServerTimezone() string {
timezone := strings.TrimSpace(common.LoadTimeZoneByCmd())
if timezone == "" {
return defaultUserTimezone
}
if _, err := time.LoadLocation(timezone); err != nil {
return defaultUserTimezone
}
return timezone
}
func extractStringList(value interface{}) []string {
switch values := value.(type) {
case []interface{}:
result := make([]string, 0, len(values))
for _, value := range values {
text := strings.TrimSpace(fmt.Sprintf("%v", value))
if text == "" {
continue
}
result = append(result, text)
}
return result
default:
return []string{}
}
}
func ensureChildMap(parent map[string]interface{}, key string) map[string]interface{} {
if child, ok := parent[key].(map[string]interface{}); ok {
return child
}
child := map[string]interface{}{}
parent[key] = child
return child
}
func structToMap(value interface{}) (map[string]interface{}, error) {
payload, err := json.Marshal(value)
if err != nil {
return nil, err
}
result := map[string]interface{}{}
if err := json.Unmarshal(payload, &result); err != nil {
return nil, err
}
return result, nil
}
func readInstallEnv(envStr string) map[string]interface{} {
if strings.TrimSpace(envStr) == "" {
return nil
}
data := map[string]interface{}{}
if err := json.Unmarshal([]byte(envStr), &data); err != nil {
return nil
}
return data
}
const (
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
)
type agentDashboardAuth struct {
Username string
Password string
}
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
auth := agentDashboardAuth{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
if auth.Username == "" {
auth.Username = "admin"
}
if auth.Password == "" {
auth.Password = common.RandStr(8)
}
return auth
}
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
switch agentType {
case constant.AppHermesAgent:
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
default:
return "", "", false
}
}
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
values := map[string]string{
usernameKey: auth.Username,
passwordKey: auth.Password,
}
order := []string{usernameKey, passwordKey}
if agentType == constant.AppCopaw {
values[qwenPawAuthEnabledEnvKey] = "true"
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
}
return upsertAgentEnv(envPath, values, order, overwrite)
}
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return agentDashboardAuth{}, err
}
return agentDashboardAuth{
Username: strings.TrimSpace(envMap[usernameKey]),
Password: strings.TrimSpace(envMap[passwordKey]),
}, nil
}
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
if appInstall == nil || appInstall.ID == 0 {
return agentDashboardAuth{}
}
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
if err != nil {
return agentDashboardAuth{}
}
return auth
}
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string {
trim := strings.TrimSpace(value)
if len(trim) <= 6 {
return trim
}
return fmt.Sprintf("%s****%s", trim[:3], trim[len(trim)-3:])
}
func toInt(value interface{}) int {
switch v := value.(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
case string:
if v == "" {
return 0
}
parsed, _ := strconv.Atoi(v)
return parsed
default:
return 0
}
}
func generateToken() string {
bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return hex.EncodeToString(bytes)
}
func asyncReportAIProviderInstall(provider string) {
if global.CONF.Base.Mode != "stable" || provider == "" {
return
}
go func(provider string) {
query := url.Values{}
query.Set("product", "ai-provider")
query.Set("type", "install")
query.Set("version", provider)
reqURL := "https://community.fit2cloud.com/installation-analytics?" + query.Encode()
_, _, _ = req_helper.HandleRequest(reqURL, http.MethodGet, constant.TimeOut5s)
}(provider)
}