diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go
index 7d3e19a5a..fb60daecc 100644
--- a/agent/app/dto/agents.go
+++ b/agent/app/dto/agents.go
@@ -8,15 +8,9 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
- AgentType string `json:"agentType"`
- Provider string `json:"provider"`
+ AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw"`
Model string `json:"model"`
- APIType string `json:"apiType"`
- MaxTokens int `json:"maxTokens"`
- ContextWindow int `json:"contextWindow"`
AccountID uint `json:"accountId"`
- APIKey string `json:"apiKey"`
- BaseURL string `json:"baseURL"`
Token string `json:"token"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
@@ -110,27 +104,20 @@ type AgentAccountCreateReq struct {
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
- Model string `json:"model"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
- MaxTokens int `json:"maxTokens"`
- ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
}
type AgentAccountUpdateReq struct {
- ID uint `json:"id" validate:"required"`
- Name string `json:"name" validate:"required"`
- APIKey string `json:"apiKey" validate:"required"`
- RememberAPIKey bool `json:"rememberApiKey"`
- BaseURL string `json:"baseURL"`
- Model string `json:"model"`
- Models []AgentAccountModel `json:"models"`
- APIType string `json:"apiType"`
- MaxTokens int `json:"maxTokens"`
- ContextWindow int `json:"contextWindow"`
- Remark string `json:"remark"`
- SyncAgents bool `json:"syncAgents"`
+ ID uint `json:"id" validate:"required"`
+ Name string `json:"name" validate:"required"`
+ APIKey string `json:"apiKey" validate:"required"`
+ RememberAPIKey bool `json:"rememberApiKey"`
+ BaseURL string `json:"baseURL"`
+ APIType string `json:"apiType"`
+ Remark string `json:"remark"`
+ SyncAgents bool `json:"syncAgents"`
}
type AgentAccountVerifyReq struct {
@@ -157,11 +144,8 @@ type AgentAccountInfo struct {
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
- Model string `json:"model"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
- MaxTokens int `json:"maxTokens"`
- ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go
index e2d9dd5e1..463e76edd 100644
--- a/agent/app/model/agent_account.go
+++ b/agent/app/model/agent_account.go
@@ -6,11 +6,7 @@ type AgentAccount struct {
Name string `json:"name"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
- Model string `json:"model"`
- Models string `json:"models" gorm:"type:text"`
APIType string `json:"apiType"`
- MaxTokens int `json:"maxTokens"`
- ContextWindow int `json:"contextWindow"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go
index 9f1d729e2..3358a6906 100644
--- a/agent/app/provider/catalog.go
+++ b/agent/app/provider/catalog.go
@@ -20,7 +20,6 @@ type Meta struct {
DefaultBaseURL string
EnvKey string
Models []Model
- Enabled bool
}
var catalog = map[string]Meta{
@@ -30,14 +29,12 @@ var catalog = map[string]Meta{
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
- Enabled: true,
Models: []Model{},
},
"ollama": {
Key: "ollama",
DisplayName: "Ollama",
Sort: 15,
- Enabled: true,
},
"vllm": {
Key: "vllm",
@@ -45,7 +42,6 @@ var catalog = map[string]Meta{
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
- Enabled: true,
Models: []Model{},
},
"deepseek": {
@@ -54,7 +50,6 @@ var catalog = map[string]Meta{
Sort: 25,
DefaultBaseURL: "https://api.deepseek.com/v1",
EnvKey: "DEEPSEEK_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"},
@@ -67,7 +62,6 @@ var catalog = map[string]Meta{
Sort: 30,
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
EnvKey: "QWEN_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max"},
@@ -85,7 +79,6 @@ var catalog = map[string]Meta{
Sort: 35,
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "ark-coding-plan/doubao-seed-2.0-code", Name: "Doubao-Seed-2.0-Code"},
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao-Seed-Code"},
@@ -101,7 +94,6 @@ var catalog = map[string]Meta{
Sort: 40,
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5"},
{ID: "zai/glm-4.7", Name: "GLM-4.7"},
@@ -115,7 +107,6 @@ var catalog = map[string]Meta{
Sort: 45,
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
EnvKey: "MINIMAX_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5"},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
@@ -127,7 +118,6 @@ var catalog = map[string]Meta{
Sort: 50,
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
@@ -140,7 +130,6 @@ var catalog = map[string]Meta{
Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5"},
},
@@ -151,7 +140,6 @@ var catalog = map[string]Meta{
Sort: 55,
DefaultBaseURL: "https://api.openai.com/v1",
EnvKey: "OPENAI_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
@@ -167,7 +155,6 @@ var catalog = map[string]Meta{
Sort: 56,
DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
@@ -179,7 +166,6 @@ var catalog = map[string]Meta{
Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
@@ -194,7 +180,6 @@ var catalog = map[string]Meta{
Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
@@ -207,7 +192,6 @@ var catalog = map[string]Meta{
Sort: 70,
DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY",
- Enabled: true,
Models: []Model{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
@@ -232,11 +216,6 @@ func All() map[string]Meta {
return result
}
-func IsEnabled(key string) bool {
- meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
- return ok && meta.Enabled
-}
-
func DefaultBaseURL(key string) (string, bool) {
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
diff --git a/agent/app/provider/verify.go b/agent/app/provider/verify.go
index fa4bf3cb2..863ca1e95 100644
--- a/agent/app/provider/verify.go
+++ b/agent/app/provider/verify.go
@@ -18,6 +18,10 @@ type VerifyRequest struct {
Body []byte
}
+const (
+ defaultVerifyTimeout = 30 * time.Second
+)
+
func SkipVerification(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "custom", "vllm", "ollama", "kimi-coding":
@@ -42,7 +46,7 @@ func VerifyAccount(provider, baseURL, apiKey string) error {
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
- resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
+ resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
@@ -53,6 +57,10 @@ func VerifyAccount(provider, baseURL, apiKey string) error {
return nil
}
+func verifyTimeout() time.Duration {
+ return defaultVerifyTimeout
+}
+
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
provider = strings.ToLower(strings.TrimSpace(provider))
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go
index 90b66b38c..12391dd6d 100644
--- a/agent/app/service/agents.go
+++ b/agent/app/service/agents.go
@@ -2,38 +2,26 @@ package service
import (
"context"
- "crypto/rand"
- "encoding/hex"
"encoding/json"
"fmt"
- "net/http"
- "net/url"
"path"
"sort"
- "strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"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/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
- "github.com/1Panel-dev/1Panel/agent/utils/common"
- "github.com/1Panel-dev/1Panel/agent/utils/files"
- openclawutil "github.com/1Panel-dev/1Panel/agent/utils/openclaw"
- "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
-type AgentService struct{}
-
type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
@@ -43,7 +31,7 @@ type IAgentService interface {
GetProviders() ([]dto.ProviderInfo, error)
CreateAccount(req dto.AgentAccountCreateReq) error
UpdateAccount(req dto.AgentAccountUpdateReq) error
- SyncAgentsByAccountID(accountID uint) error
+ SyncAgentsByAccount(account *model.AgentAccount) error
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
@@ -70,10 +58,6 @@ type IAgentService interface {
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
}
-func NewIAgentService() IAgentService {
- return &AgentService{}
-}
-
const (
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
defaultBrowserProfile = "openclaw"
@@ -89,10 +73,7 @@ const (
)
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
- agentType := normalizeAgentType(req.AgentType)
- if !isSupportedAgentType(agentType) {
- return nil, fmt.Errorf("agent type is invalid")
- }
+ agentType := req.AgentType
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
@@ -111,11 +92,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
return nil, buserr.WithMap("ErrAgentLimitReached", map[string]interface{}{"max": maxCommunityAIAgents}, nil)
}
}
- appKey := constant.AppOpenclaw
- if agentType == constant.AppCopaw {
- appKey = constant.AppCopaw
- }
- app, err := appRepo.GetFirst(appRepo.WithKey(appKey))
+ app, err := appRepo.GetFirst(appRepo.WithKey(agentType))
if err != nil || app.ID == 0 {
return nil, buserr.New("ErrRecordNotFound")
}
@@ -136,6 +113,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
configPath := ""
storedModel := ""
var allowedOrigins []string
+ var account *model.AgentAccount
+ var installHooks *appInstallHooks
if agentType == constant.AppOpenclaw {
var err error
@@ -146,71 +125,38 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
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")
- }
if req.AccountID == 0 {
return nil, buserr.New("ErrAgentAccountRequired")
}
- account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
+ account, err = agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil {
return nil, err
}
- if !account.Verified && !providercatalog.SkipVerification(account.Provider) {
+ if !account.Verified {
return nil, buserr.New("ErrAgentAccountNotVerified")
}
- if account.Provider != "" && provider != "" && account.Provider != provider {
- return nil, buserr.New("ErrAgentProviderMismatch")
- }
- provider = strings.ToLower(strings.TrimSpace(account.Provider))
+ provider = account.Provider
baseURL = strings.TrimSpace(account.BaseURL)
- if baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(provider); ok {
- baseURL = defaultURL
- }
- }
- if provider == "ollama" && baseURL == "" {
- return nil, buserr.New("ErrAgentBaseURLRequired")
- }
- if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
- return nil, buserr.New("ErrAgentApiKeyRequired")
- }
- accountModels, err := loadAgentAccountModels(account)
- if err != nil {
- return nil, err
- }
- storedModel = strings.TrimSpace(req.Model)
- if storedModel == "" {
- storedModel = strings.TrimSpace(account.Model)
- }
- if storedModel == "" && len(accountModels) > 0 {
- storedModel = strings.TrimSpace(accountModels[0].ID)
- }
- if storedModel == "" {
- return nil, buserr.New("ErrAgentModelNotInAccount")
- }
- selectedAccountModel, ok := findAgentAccountModelForProvider(provider, accountModels, storedModel)
- if !ok {
- return nil, buserr.New("ErrAgentModelNotInAccount")
- }
- storedModel = strings.TrimSpace(selectedAccountModel.ID)
- apiType, maxTokens, contextWindow = resolveRuntimeParams(
- provider,
- account.APIType,
- selectedAccountModel.MaxTokens,
- selectedAccountModel.ContextWindow,
- )
- runtimeModel, err = buildOpenclawPrimaryModel(account, storedModel)
+ resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model)
if err != nil {
return nil, err
}
+ storedModel = resolvedRuntime.StoredModel
+ apiType = resolvedRuntime.APIType
+ maxTokens = resolvedRuntime.MaxTokens
+ contextWindow = resolvedRuntime.ContextWindow
+ runtimeModel = resolvedRuntime.PrimaryModel
apiKey = account.APIKey
accountID = account.ID
token = strings.TrimSpace(req.Token)
if token == "" {
token = generateToken()
}
+ installHooks = &appInstallHooks{
+ AfterCopyData: func(appInstall *model.AppInstall) error {
+ return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
+ },
+ }
}
params := map[string]interface{}{
@@ -257,7 +203,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
DockerCompose: req.DockerCompose,
},
}
- appInstall, err := NewIAppService().Install(installReq, false)
+ appInstall, err := AppService{}.installWithHooks(installReq, false, installHooks)
if err != nil {
return nil, err
}
@@ -284,16 +230,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if err := agentRepo.Create(agent); err != nil {
return nil, err
}
- if agentType == constant.AppOpenclaw {
- go a.writeConfigWithRetry(
- appInstall,
- accountID,
- storedModel,
- token,
- agent.ID,
- allowedOrigins,
- )
- }
item := buildAgentItem(agent, appInstall, nil)
return &item, nil
@@ -320,9 +256,6 @@ func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, erro
}
func (a AgentService) Delete(req dto.AgentDeleteReq) error {
- if req.ID == 0 {
- return buserr.New("ErrAgentIDRequired")
- }
agent, err := agentRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@@ -339,7 +272,6 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error {
if err := NewIAppInstalledService().Operate(operate); err != nil {
return err
}
- go a.waitAndDeleteAgent(agent.ID, agent.AppInstallID)
return nil
}
@@ -348,21 +280,10 @@ func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
if err != nil {
return err
}
- if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
+ if agent.AgentType == constant.AppCopaw {
return fmt.Errorf("copaw does not support token")
}
- configPath := strings.TrimSpace(agent.ConfigPath)
- if configPath == "" && agent.AppInstallID > 0 {
- install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
- if err != nil {
- return err
- }
- configPath = path.Join(install.GetPath(), "data", "conf", "openclaw.json")
- }
- if configPath == "" {
- return buserr.New("ErrRecordNotFound")
- }
- conf, err := readOpenclawConfig(configPath)
+ conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
@@ -376,13 +297,10 @@ func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
authMap["mode"] = "token"
}
authMap["token"] = newToken
- if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
+ if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
return err
}
agent.Token = newToken
- if agent.ConfigPath == "" {
- agent.ConfigPath = configPath
- }
return agentRepo.Save(agent)
}
@@ -391,75 +309,29 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if err != nil {
return err
}
- if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
+ if agent.AgentType == constant.AppCopaw {
return fmt.Errorf("copaw does not support model config")
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil {
return err
}
- if !account.Verified && !providercatalog.SkipVerification(account.Provider) {
- return buserr.New("ErrAgentAccountNotVerified")
- }
- provider := strings.ToLower(strings.TrimSpace(account.Provider))
- if !isSupportedAgentProvider(provider) {
- return buserr.New("ErrAgentProviderNotSupported")
- }
- modelName := strings.TrimSpace(req.Model)
- if modelName == "" {
- return buserr.New("ErrAgentProviderMismatch")
- }
- if provider != "custom" && provider != "vllm" && !modelMatchesProvider(provider, modelName) {
- return buserr.New("ErrAgentProviderMismatch")
- }
- baseURL := strings.TrimSpace(account.BaseURL)
- if baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(provider); ok {
- baseURL = defaultURL
- }
- }
- if provider == "ollama" && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
- if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
- return buserr.New("ErrAgentApiKeyRequired")
- }
- accountModels, err := loadAgentAccountModels(account)
+ resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model)
if err != nil {
return err
}
- selectedAccountModel, ok := findAgentAccountModelForProvider(provider, accountModels, modelName)
- if !ok {
- return buserr.New("ErrAgentModelNotInAccount")
- }
- modelName = strings.TrimSpace(selectedAccountModel.ID)
- apiType, maxTokens, contextWindow := resolveRuntimeParams(
- provider,
- account.APIType,
- selectedAccountModel.MaxTokens,
- selectedAccountModel.ContextWindow,
- )
- confDir := ""
- if agent.ConfigPath != "" {
- confDir = path.Dir(agent.ConfigPath)
- } else if agent.AppInstallID > 0 {
- install, errGet := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
- if errGet == nil {
- confDir = path.Join(install.GetPath(), "data", "conf")
- }
- }
- if confDir == "" {
- return buserr.New("ErrRecordNotFound")
- }
+ modelName := resolvedRuntime.StoredModel
+ apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
+ confDir := path.Dir(agent.ConfigPath)
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil {
return err
}
- agent.Provider = provider
+ agent.Provider = account.Provider
agent.Model = modelName
agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
- agent.BaseURL = baseURL
+ agent.BaseURL = account.BaseURL
agent.APIKey = account.APIKey
agent.AccountID = account.ID
return agentRepo.Save(agent)
@@ -484,80 +356,38 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
}
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
- provider := strings.ToLower(strings.TrimSpace(req.Provider))
- if !isSupportedAgentProvider(provider) {
- return buserr.New("ErrAgentProviderNotSupported")
- }
- apiKey := strings.TrimSpace(req.APIKey)
- if apiKey == "" {
- return buserr.New("ErrAgentApiKeyRequired")
- }
- baseURL := strings.TrimSpace(req.BaseURL)
- if fixedURL, ok := fixedProviderBaseURL(provider); ok {
- baseURL = fixedURL
- }
- if (provider == "custom" || provider == "vllm") && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
- if provider != "custom" && provider != "vllm" && baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(provider); ok {
- baseURL = defaultURL
- }
- }
- if provider == "ollama" && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
+ provider := req.Provider
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
return buserr.New("ErrRecordExist")
}
- modelName := strings.TrimSpace(req.Model)
- apiType := normalizeAPIType(req.APIType)
- if provider == "custom" || provider == "vllm" {
- if !isSupportedAPIType(apiType) {
- return fmt.Errorf("apiType is invalid")
- }
- }
- if provider == "ollama" {
- if !isSupportedOllamaAPIType(apiType) {
- return fmt.Errorf("apiType is invalid")
- }
- }
- if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil {
+ resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL, req.APIType, "")
+ if err != nil {
return err
}
- verified := !providercatalog.SkipVerification(provider)
- _, maxTokens, contextWindow := resolveRuntimeParams(provider, apiType, req.MaxTokens, req.ContextWindow)
account := &model.AgentAccount{
- Provider: provider,
+ Provider: resolvedInput.Provider,
Name: req.Name,
- APIKey: apiKey,
+ APIKey: resolvedInput.APIKey,
RememberAPIKey: req.RememberAPIKey,
- BaseURL: baseURL,
- Model: "",
- Models: "",
- APIType: apiType,
- MaxTokens: 0,
- ContextWindow: 0,
- Verified: verified,
+ BaseURL: resolvedInput.BaseURL,
+ APIType: resolvedInput.APIType,
+ Verified: true,
Remark: req.Remark,
}
- if provider == "custom" || provider == "vllm" || provider == "ollama" {
- account.MaxTokens = maxTokens
- account.ContextWindow = contextWindow
- }
- if err := agentAccountRepo.Create(account); err != nil {
- return err
- }
- initialModels, err := buildInitialAgentAccountModels(account, req.Models, modelName)
+ initialModels, err := buildInitialAgentAccountModels(account, req.Models)
if err != nil {
- _ = agentAccountRepo.DeleteByID(account.ID)
return err
}
- if len(initialModels) > 0 {
- if err := replacePersistedAgentAccountModels(account.ID, initialModels); err != nil {
- _ = agentAccountRepo.DeleteByID(account.ID)
+ if err := global.DB.Transaction(func(tx *gorm.DB) error {
+ if err := tx.Create(account).Error; err != nil {
return err
}
+ if len(initialModels) == 0 {
+ return nil
+ }
+ return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels)
+ }); err != nil {
+ return err
}
asyncReportAIProviderInstall(provider)
return nil
@@ -568,93 +398,22 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if err != nil {
return err
}
- provider := strings.ToLower(strings.TrimSpace(account.Provider))
- baseURL := strings.TrimSpace(req.BaseURL)
- if fixedURL, ok := fixedProviderBaseURL(provider); ok {
- baseURL = fixedURL
- }
- if (provider == "custom" || provider == "vllm") && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
- if provider != "custom" && provider != "vllm" && baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(provider); ok {
- baseURL = defaultURL
- }
- }
- if provider == "ollama" && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
- apiType := normalizeAPIType(req.APIType)
- rawAPIType := strings.TrimSpace(req.APIType)
- if (provider == "custom" || provider == "vllm") && !isSupportedAPIType(apiType) {
- return fmt.Errorf("apiType is invalid")
- }
- if provider == "ollama" {
- if rawAPIType == "" {
- apiType = normalizeAPIType(account.APIType)
- if !isSupportedOllamaAPIType(apiType) {
- apiType = "openai-responses"
- }
- } else if !isSupportedOllamaAPIType(apiType) {
- return fmt.Errorf("apiType is invalid")
- }
- }
- if provider != "custom" && provider != "vllm" && provider != "ollama" {
- apiType = normalizeAPIType(account.APIType)
- }
- _, maxTokens, contextWindow := resolveRuntimeParams(provider, apiType, req.MaxTokens, req.ContextWindow)
- if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil {
+ provider := account.Provider
+ resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL, req.APIType, account.APIType)
+ if err != nil {
return err
}
- verified := !providercatalog.SkipVerification(provider)
- account.Provider = provider
- account.APIKey = req.APIKey
- account.BaseURL = baseURL
- account.APIType = apiType
- account.MaxTokens = 0
- account.ContextWindow = 0
- if provider == "custom" || provider == "vllm" || provider == "ollama" {
- account.MaxTokens = maxTokens
- account.ContextWindow = contextWindow
- }
account.Name = req.Name
- account.APIKey = req.APIKey
+ account.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey
- account.BaseURL = baseURL
- account.APIType = apiType
+ account.BaseURL = resolvedInput.BaseURL
+ account.APIType = resolvedInput.APIType
account.Remark = req.Remark
- account.Verified = verified
+ account.Verified = true
- var nextAccountModels []dto.AgentAccountModel
- if len(req.Models) > 0 || strings.TrimSpace(req.Model) != "" {
- accountModels, _, err := normalizeAgentAccountModels(account, req.Models, req.Model, true)
- if err != nil {
- return err
- }
- nextAccountModels = accountModels
- if err := ensureAccountModelsNotBound(account, nextAccountModels); err != nil {
- return err
- }
- }
- if err := agentAccountRepo.Save(account); err != nil {
+ if err := global.DB.Save(account).Error; err != nil {
return err
}
- if len(nextAccountModels) > 0 {
- if err := replacePersistedAgentAccountModels(account.ID, nextAccountModels); err != nil {
- return err
- }
- } else if shouldRefreshAccountModelRuntimeLimits(provider) {
- accountModels, err := loadAgentAccountModels(account)
- if err != nil {
- return err
- }
- if len(accountModels) > 0 {
- accountModels = refreshAccountModelRuntimeLimits(account, accountModels)
- if err := replacePersistedAgentAccountModels(account.ID, accountModels); err != nil {
- return err
- }
- }
- }
if req.SyncAgents {
if err := a.syncAgentsByAccount(account); err != nil {
return err
@@ -689,11 +448,8 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
APIKey: apiKey,
RememberAPIKey: item.RememberAPIKey,
BaseURL: item.BaseURL,
- Model: "",
Models: nil,
APIType: item.APIType,
- MaxTokens: item.MaxTokens,
- ContextWindow: item.ContextWindow,
Verified: item.Verified,
Remark: item.Remark,
CreatedAt: item.CreatedAt,
@@ -837,48 +593,22 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
return a.syncAgentsByAccount(account)
}
-func (a AgentService) SyncAgentsByAccountID(accountID uint) error {
- if accountID == 0 {
+func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
+ if account == nil || account.ID == 0 {
return nil
}
- account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID))
- if err != nil {
- return err
- }
return a.syncAgentsByAccount(account)
}
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
- provider := strings.ToLower(strings.TrimSpace(req.Provider))
- if !isSupportedAgentProvider(provider) {
- return buserr.New("ErrAgentProviderNotSupported")
+ resolvedVerification, err := resolveAgentAccountVerification(req.Provider, req.APIKey, req.BaseURL)
+ if err != nil {
+ return err
}
- apiKey := strings.TrimSpace(req.APIKey)
- if apiKey == "" {
- return buserr.New("ErrAgentApiKeyRequired")
- }
- baseURL := strings.TrimSpace(req.BaseURL)
- if fixedURL, ok := fixedProviderBaseURL(provider); ok {
- baseURL = fixedURL
- }
- if baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(provider); ok {
- baseURL = defaultURL
- }
- }
- if provider == "ollama" && baseURL == "" {
- return buserr.New("ErrAgentBaseURLRequired")
- }
- if providercatalog.SkipVerification(provider) {
- return nil
- }
- return providercatalog.VerifyAccount(provider, baseURL, apiKey)
+ return verifyResolvedAgentAccount(resolvedVerification)
}
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
- if req.ID == 0 {
- return buserr.New("ErrAgentAccountIDRequired")
- }
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
return buserr.New("ErrAgentAccountBound")
}
@@ -899,13 +629,9 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
- dmPolicy := req.DmPolicy
- if dmPolicy == "" {
- dmPolicy = "pairing"
- }
setFeishuConfig(conf, dto.AgentFeishuConfig{
Enabled: req.Enabled,
- DmPolicy: dmPolicy,
+ DmPolicy: req.DmPolicy,
BotName: req.BotName,
AppID: req.AppID,
AppSecret: req.AppSecret,
@@ -926,13 +652,9 @@ func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.Ag
func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
- dmPolicy := req.DmPolicy
- if dmPolicy == "" {
- dmPolicy = "pairing"
- }
setTelegramConfig(conf, dto.AgentTelegramConfig{
Enabled: req.Enabled,
- DmPolicy: dmPolicy,
+ DmPolicy: req.DmPolicy,
BotToken: req.BotToken,
Proxy: req.Proxy,
})
@@ -951,18 +673,10 @@ func (a AgentService) GetDiscordConfig(req dto.AgentDiscordConfigReq) (*dto.Agen
func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error {
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
- dmPolicy := req.DmPolicy
- if dmPolicy == "" {
- dmPolicy = "pairing"
- }
- groupPolicy := req.GroupPolicy
- if groupPolicy == "" {
- groupPolicy = "open"
- }
setDiscordConfig(conf, dto.AgentDiscordConfig{
Enabled: req.Enabled,
- DmPolicy: dmPolicy,
- GroupPolicy: groupPolicy,
+ DmPolicy: req.DmPolicy,
+ GroupPolicy: req.GroupPolicy,
Token: req.Token,
Proxy: req.Proxy,
})
@@ -1057,7 +771,7 @@ func (a AgentService) GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.Ag
if err != nil {
return nil, err
}
- if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
+ if agent.AgentType == constant.AppCopaw {
return nil, fmt.Errorf("copaw does not support security config")
}
conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -1073,7 +787,7 @@ func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq)
if err != nil {
return err
}
- if normalizeAgentType(agent.AgentType) == constant.AppCopaw {
+ if agent.AgentType == constant.AppCopaw {
return fmt.Errorf("copaw does not support security config")
}
allowedOrigins, err := normalizeAllowedOrigins(req.AllowedOrigins)
@@ -1134,13 +848,7 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
if err != nil {
return err
}
- channelType := strings.ToLower(strings.TrimSpace(req.Type))
- if channelType == "" {
- channelType = "feishu"
- }
- if channelType != "feishu" && channelType != "telegram" && channelType != "discord" && channelType != "wecom" {
- return fmt.Errorf("unsupported channel type: %s", channelType)
- }
+ channelType := req.Type
if err := cmd.RunDefaultBashCf(
"docker exec %s openclaw pairing approve %s %q",
install.ContainerName,
@@ -1190,522 +898,6 @@ func (a AgentService) mutateAgentConfig(agentID uint, fn func(agent *model.Agent
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
-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))
- }
- }
- 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) {
- 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 extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
- result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"}
- channels, ok := conf["channels"].(map[string]interface{})
- if !ok {
- return result
- }
- feishu, ok := channels["feishu"].(map[string]interface{})
- if !ok {
- return result
- }
- if enabled, ok := feishu["enabled"].(bool); ok {
- result.Enabled = enabled
- }
- if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
- result.DmPolicy = dmPolicy
- }
- accounts, ok := feishu["accounts"].(map[string]interface{})
- if !ok {
- return result
- }
- main, ok := accounts["main"].(map[string]interface{})
- if !ok {
- return result
- }
- if appID, ok := main["appId"].(string); ok {
- result.AppID = appID
- }
- if appSecret, ok := main["appSecret"].(string); ok {
- result.AppSecret = appSecret
- }
- if botName, ok := main["botName"].(string); ok {
- result.BotName = botName
- }
- return result
-}
-
-func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) {
- channels := ensureChildMap(conf, "channels")
- feishu := ensureChildMap(channels, "feishu")
- feishu["enabled"] = config.Enabled
- feishu["dmPolicy"] = config.DmPolicy
-
- accounts := ensureChildMap(feishu, "accounts")
- main := ensureChildMap(accounts, "main")
- main["appId"] = config.AppID
- main["appSecret"] = config.AppSecret
- main["botName"] = config.BotName
-
- if strings.EqualFold(config.DmPolicy, "open") {
- feishu["allowFrom"] = []string{"*"}
- }
-}
-
-func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) {
- plugins := ensureChildMap(conf, "plugins")
- entries := ensureChildMap(plugins, "entries")
- feishu := ensureChildMap(entries, "feishu")
- feishu["enabled"] = enabled
-}
-
-func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig {
- result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"}
- channels, ok := conf["channels"].(map[string]interface{})
- if !ok {
- return result
- }
- telegram, ok := channels["telegram"].(map[string]interface{})
- if !ok {
- return result
- }
- if enabled, ok := telegram["enabled"].(bool); ok {
- result.Enabled = enabled
- }
- if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
- result.DmPolicy = dmPolicy
- }
- if botToken, ok := telegram["botToken"].(string); ok {
- result.BotToken = botToken
- }
- if proxy, ok := telegram["proxy"].(string); ok {
- result.Proxy = proxy
- }
- return result
-}
-
-func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) {
- channels := ensureChildMap(conf, "channels")
- telegram := map[string]interface{}{
- "enabled": config.Enabled,
- "dmPolicy": config.DmPolicy,
- "botToken": config.BotToken,
- }
- if strings.EqualFold(config.DmPolicy, "open") {
- telegram["allowFrom"] = []string{"*"}
- }
- if strings.TrimSpace(config.Proxy) != "" {
- telegram["proxy"] = strings.TrimSpace(config.Proxy)
- }
- channels["telegram"] = telegram
-}
-
-func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
- result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"}
- channels, ok := conf["channels"].(map[string]interface{})
- if !ok {
- return result
- }
- discord, ok := channels["discord"].(map[string]interface{})
- if !ok {
- return result
- }
- if enabled, ok := discord["enabled"].(bool); ok {
- result.Enabled = enabled
- }
- if token, ok := discord["token"].(string); ok {
- result.Token = token
- }
- if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
- result.GroupPolicy = groupPolicy
- }
- if proxy, ok := discord["proxy"].(string); ok {
- result.Proxy = proxy
- }
- if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" {
- result.DmPolicy = policy
- return result
- }
- // backward compatibility: old nested style
- dm, ok := discord["dm"].(map[string]interface{})
- if ok {
- if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" {
- result.DmPolicy = policy
- }
- }
- return result
-}
-
-func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) {
- channels := ensureChildMap(conf, "channels")
- discord := ensureChildMap(channels, "discord")
- discord["enabled"] = config.Enabled
- discord["token"] = config.Token
- discord["dmPolicy"] = config.DmPolicy
- discord["groupPolicy"] = config.GroupPolicy
- if strings.EqualFold(config.DmPolicy, "open") {
- discord["allowFrom"] = []string{"*"}
- } else {
- delete(discord, "allowFrom")
- }
- if strings.TrimSpace(config.Proxy) != "" {
- discord["proxy"] = strings.TrimSpace(config.Proxy)
- } else {
- delete(discord, "proxy")
- }
- delete(discord, "dm")
-}
-
-func 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 extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
- result := dto.AgentQQBotConfig{Enabled: true}
- channels, ok := conf["channels"].(map[string]interface{})
- if !ok {
- return result
- }
- qqbot, ok := channels["qqbot"].(map[string]interface{})
- if !ok {
- return result
- }
- if enabled, ok := qqbot["enabled"].(bool); ok {
- result.Enabled = enabled
- }
- if appID, ok := qqbot["appId"].(string); ok {
- result.AppID = appID
- }
- if clientSecret, ok := qqbot["clientSecret"].(string); ok {
- result.ClientSecret = clientSecret
- }
- return result
-}
-
-func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
- result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"}
- channels, ok := conf["channels"].(map[string]interface{})
- if !ok {
- return result
- }
- wecom, ok := channels["wecom"].(map[string]interface{})
- if !ok {
- return result
- }
- if enabled, ok := wecom["enabled"].(bool); ok {
- result.Enabled = enabled
- }
- if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
- result.DmPolicy = strings.TrimSpace(dmPolicy)
- }
- if botID, ok := wecom["botId"].(string); ok {
- result.BotID = botID
- }
- if secret, ok := wecom["secret"].(string); ok {
- result.Secret = secret
- }
- return result
-}
-
-func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
- channels := ensureChildMap(conf, "channels")
- wecom := ensureChildMap(channels, "wecom")
- wecom["enabled"] = config.Enabled
- wecom["botId"] = strings.TrimSpace(config.BotID)
- wecom["secret"] = strings.TrimSpace(config.Secret)
- wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy)
- if strings.EqualFold(config.DmPolicy, "open") {
- wecom["allowFrom"] = []string{"*"}
- } else {
- wecom["allowFrom"] = []string{}
- }
-
- plugins := ensureChildMap(conf, "plugins")
- entries := ensureChildMap(plugins, "entries")
- wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
- wecomEntry["enabled"] = config.Enabled
-}
-
-func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
- channels := ensureChildMap(conf, "channels")
- qqbot := ensureChildMap(channels, "qqbot")
- qqbot["enabled"] = config.Enabled
- qqbot["allowFrom"] = []string{"*"}
- qqbot["appId"] = strings.TrimSpace(config.AppID)
- qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret)
-
- plugins := ensureChildMap(conf, "plugins")
- entries := ensureChildMap(plugins, "entries")
- qqbotEntry := ensureChildMap(entries, "qqbot")
- qqbotEntry["enabled"] = config.Enabled
-}
-
-func resolvePluginMeta(pluginType string) (string, string, error) {
- switch strings.ToLower(strings.TrimSpace(pluginType)) {
- case "qqbot":
- return "@sliverp/qqbot@latest", "qqbot", nil
- case "wecom":
- return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
- default:
- return "", "", fmt.Errorf("unsupported plugin type")
- }
-}
-
-func checkPluginInstalled(containerName, pluginType string) (bool, error) {
- _, pluginDir, err := resolvePluginMeta(pluginType)
- if err != nil {
- return false, err
- }
- if strings.TrimSpace(containerName) == "" {
- return false, buserr.New("ErrRecordNotFound")
- }
- pluginPath := path.Join(openclawPluginBaseDir, pluginDir)
- mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
- if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil {
- return false, nil
- }
- return true, nil
-}
-
-func extractOtherConfig(conf map[string]interface{}) dto.AgentOtherConfig {
- 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
-}
-
-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 (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
agents, err := agentRepo.List(repo.WithByAccountID(account.ID))
if err != nil {
@@ -1720,42 +912,23 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
}
baseURL := resolveAccountBaseURL(account)
for _, agent := range agents {
- confDir := ""
- if agent.ConfigPath != "" {
- confDir = path.Dir(agent.ConfigPath)
- } else if agent.AppInstallID > 0 {
- install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
- if err == nil {
- confDir = path.Join(install.GetPath(), "data", "conf")
- }
- }
- if confDir == "" {
- continue
- }
+ confDir := path.Dir(agent.ConfigPath)
modelName := strings.TrimSpace(agent.Model)
var selectedAccountModel dto.AgentAccountModel
- var ok bool
if modelName != "" {
- selectedAccountModel, ok = findAgentAccountModelForProvider(account.Provider, accountModels, modelName)
- if !ok {
+ selectedAccountModel, err = requireAgentAccountModelForProvider(account.Provider, accountModels, modelName)
+ if err != nil {
return buserr.WithName("ErrAgentModelInUse", agent.Name)
}
} else {
- modelName = strings.TrimSpace(account.Model)
- if modelName != "" {
- selectedAccountModel, ok = findAgentAccountModelForProvider(account.Provider, accountModels, modelName)
- }
- if !ok {
- selectedAccountModel = accountModels[0]
- }
+ selectedAccountModel = accountModels[0]
}
- modelName = strings.TrimSpace(selectedAccountModel.ID)
- apiType, maxTokens, contextWindow := resolveRuntimeParams(
- account.Provider,
- account.APIType,
- selectedAccountModel.MaxTokens,
- selectedAccountModel.ContextWindow,
- )
+ resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel)
+ if err != nil {
+ return err
+ }
+ modelName = resolvedRuntime.StoredModel
+ apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil {
return err
}
@@ -1770,1559 +943,3 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
}
return nil
}
-
-func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
- agentType := normalizeAgentType(agent.AgentType)
- if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw {
- agentType = constant.AppCopaw
- }
- item := dto.AgentItem{
- ID: agent.ID,
- Name: agent.Name,
- AgentType: agentType,
- Provider: agent.Provider,
- ProviderName: providerDisplayName(agent.Provider),
- Model: agent.Model,
- APIType: agent.APIType,
- MaxTokens: agent.MaxTokens,
- ContextWindow: agent.ContextWindow,
- BaseURL: agent.BaseURL,
- APIKey: maskKey(agent.APIKey),
- Token: agent.Token,
- Status: agent.Status,
- Message: agent.Message,
- AppInstallID: agent.AppInstallID,
- 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 isOpenclawHTTPSVersion(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)
- }
- }
- }
- return item
-}
-
-func isOpenclawHTTPSVersion(version string) bool {
- target := strings.TrimSpace(strings.ToLower(version))
- if target == "" || target == "latest" {
- return true
- }
- if !strings.ContainsAny(target, "0123456789") {
- return true
- }
- return common.CompareAppVersion(target, openclawHTTPSVersion)
-}
-
-func shouldMigrateOpenclawHTTPSUpgrade(install *model.AppInstall, fromVersion, toVersion string) bool {
- if install == nil || install.App.Key != constant.AppOpenclaw {
- return false
- }
- return !isOpenclawHTTPSVersion(fromVersion) && isOpenclawHTTPSVersion(toVersion)
-}
-
-func migrateOpenclawHTTPSUpgrade(install *model.AppInstall, fromVersion, toVersion string) error {
- systemIP, _ := settingRepo.GetValueByKey("SystemIP")
- return migrateOpenclawHTTPSUpgradeWithSystemIP(install, fromVersion, toVersion, systemIP)
-}
-
-func migrateOpenclawHTTPSUpgradeWithSystemIP(install *model.AppInstall, fromVersion, toVersion, systemIP string) error {
- if !shouldMigrateOpenclawHTTPSUpgrade(install, fromVersion, toVersion) {
- return nil
- }
- migrateOpenclawInstallPorts(install)
- if err := openclawutil.WriteCatchAllCaddyfile(install.GetPath()); err != nil {
- return err
- }
- 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
- }
- if install.HttpsPort > 0 {
- allowedOrigin, err := buildOpenclawAllowedOrigin(originHost, install.HttpsPort)
- 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 migrateOpenclawInstallPorts(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 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
- }
- if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
- envMap["ALLOWED_ORIGIN"] = allowedOrigin
- }
- delete(envMap, "PANEL_APP_PORT_HTTP")
- 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(host string, port int) (string, error) {
- host = strings.TrimSpace(host)
- if 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("https://%s:%d", host, port))
-}
-
-func checkAgentUpgradable(install model.AppInstall) bool {
- if install.ID == 0 || install.Version == "" || install.Version == "latest" {
- return false
- }
- if install.App.ID == 0 {
- 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)
-}
-
-func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) {
- if appInstallID == 0 {
- _ = agentRepo.DeleteByID(agentID)
- return
- }
- for i := 0; i < 180; i++ {
- _, err := appInstallRepo.GetFirst(repo.WithByID(appInstallID))
- if err != nil {
- _ = agentRepo.DeleteByID(agentID)
- return
- }
- time.Sleep(2 * time.Second)
- }
-}
-
-func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, accountID uint, modelName, token string, agentID uint, allowedOrigins []string) {
- if appInstall == nil {
- return
- }
- fileOp := files.NewFileOp()
- composePath := appInstall.GetComposePath()
- for i := 0; i < 60; i++ {
- if fileOp.Stat(composePath) {
- break
- }
- time.Sleep(time.Second)
- }
- confDir := path.Join(appInstall.GetPath(), "data", "conf")
- account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID))
- if err != nil {
- global.LOG.Errorf("load agent account failed: %v", err)
- return
- }
- if err := writeOpenclawConfig(confDir, account, modelName, 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 {
- agent.Message = err.Error()
- agent.Status = constant.StatusError
- _ = agentRepo.Save(agent)
- }
- return
- }
- dataDir := path.Join(appInstall.GetPath(), "data")
- for i := 0; i < 60; i++ {
- if fileOp.Stat(dataDir) {
- if err := fileOp.ChownR(dataDir, "1000", "1000", true); err != nil {
- global.LOG.Errorf("chown data dir failed: %v", err)
- agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID))
- if errGet == nil && agent != nil {
- agent.Message = err.Error()
- agent.Status = constant.StatusError
- _ = agentRepo.Save(agent)
- }
- }
- break
- }
- time.Sleep(time.Second)
- }
-}
-
-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"`
-}
-
-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"`
- Models []modelEntry `json:"models,omitempty"`
-}
-
-type modelEntry struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Reasoning bool `json:"reasoning"`
- Input []string `json:"input"`
- ContextWindow int `json:"contextWindow"`
- MaxTokens int `json:"maxTokens"`
- Cost modelCost `json:"cost"`
-}
-
-type modelCost struct {
- Input float64 `json:"input"`
- Output float64 `json:"output"`
- CacheRead float64 `json:"cacheRead"`
- CacheWrite float64 `json:"cacheWrite"`
-}
-
-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) 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
- }
-
- cfg := openclawConfig{
- Gateway: gatewayConfig{
- Mode: "local",
- Bind: "loopback",
- 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},
- 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 cfg.Models != nil {
- modelsMap, err := structToMap(cfg.Models)
- if err != nil {
- return err
- }
- conf["models"] = modelsMap
- }
- 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 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"] = "loopback"
- }
- 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
- }
- envPath := path.Join(confDir, ".env")
- lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
- if envKey := providerEnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" {
- lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
- }
- content := strings.Join(lines, "\n") + "\n"
- return fileOp.SaveFile(envPath, content, 0600)
-}
-
-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, fmt.Errorf("model is required")
- }
- selectedModel = strings.TrimSpace(selectedModel)
- if selectedModel == "" {
- selectedModel = strings.TrimSpace(account.Model)
- }
- if selectedModel == "" {
- selectedModel = strings.TrimSpace(accountModels[0].ID)
- }
- if selectedModel == "" {
- return "", nil, nil, fmt.Errorf("model is required")
- }
- selectedAccountModel, ok := findAgentAccountModelForProvider(account.Provider, accountModels, selectedModel)
- if !ok {
- return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
- }
- selectedModel = strings.TrimSpace(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 := buildOpenclawCatalogModel(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
- }
- 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")
- }
- providerCfg.Models = entries
- return primaryModel, defaultsModels, &modelsConfig{
- Mode: "merge",
- Providers: map[string]modelProvider{
- providerKey: providerCfg,
- },
- }, nil
-}
-
-func buildOpenclawCatalogModel(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
- primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.MaxTokens, model.ContextWindow)
- if err != nil {
- return "", modelEntry{}, "", modelProvider{}, err
- }
- if strings.TrimSpace(model.Name) != "" {
- inferredEntry.Name = strings.TrimSpace(model.Name)
- }
- if len(model.Input) > 0 {
- inferredEntry.Input = sanitizeAgentAccountModelInputs(model.Input)
- }
- inferredEntry.Reasoning = model.Reasoning
- if model.ContextWindow > 0 {
- inferredEntry.ContextWindow = model.ContextWindow
- }
- if model.MaxTokens > 0 {
- inferredEntry.MaxTokens = model.MaxTokens
- }
- return primaryModel, inferredEntry, providerKey, providerCfg, nil
-}
-
-func buildOpenclawPrimaryModel(account *model.AgentAccount, modelID string) (string, error) {
- models, err := loadAgentAccountModels(account)
- if err != nil {
- return "", err
- }
- item, ok := findAgentAccountModelForProvider(account.Provider, models, modelID)
- if !ok {
- return "", buserr.New("ErrAgentModelNotInAccount")
- }
- primaryModel, _, _, _, err := inferOpenclawCatalogModel(account, item.ID, item.MaxTokens, item.ContextWindow)
- if err != nil {
- return "", err
- }
- return primaryModel, nil
-}
-
-func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) {
- baseURL := resolveAccountBaseURL(account)
- resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow)
- patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey)
- if err != nil {
- return "", modelEntry{}, "", modelProvider{}, err
- }
- if patch.Models == nil {
- return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is required")
- }
- modelsCfg, err := mapToModelsConfig(patch.Models)
- if err != nil {
- return "", modelEntry{}, "", modelProvider{}, err
- }
- for key, providerCfg := range modelsCfg.Providers {
- if len(providerCfg.Models) == 0 {
- continue
- }
- return patch.PrimaryModel, providerCfg.Models[0], key, modelProvider{
- ApiKey: providerCfg.ApiKey,
- BaseUrl: providerCfg.BaseUrl,
- Api: providerCfg.Api,
- }, nil
- }
- return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is invalid")
-}
-
-func resolveAccountBaseURL(account *model.AgentAccount) string {
- baseURL := strings.TrimSpace(account.BaseURL)
- if baseURL == "" {
- if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok {
- baseURL = defaultURL
- }
- }
- return baseURL
-}
-
-func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto.AgentAccountModel, legacyModel string) ([]dto.AgentAccountModel, error) {
- if account == nil {
- return nil, fmt.Errorf("account is required")
- }
- if len(requested) > 0 || strings.TrimSpace(legacyModel) != "" {
- models, _, err := normalizeAgentAccountModels(account, requested, legacyModel, true)
- if err != nil {
- return nil, err
- }
- return models, nil
- }
- meta, ok := providercatalog.Get(account.Provider)
- if !ok || len(meta.Models) == 0 {
- return nil, nil
- }
- requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
- for _, item := range meta.Models {
- requested = append(requested, dto.AgentAccountModel{
- ID: item.ID,
- Name: item.Name,
- ContextWindow: item.ContextWindow,
- MaxTokens: item.MaxTokens,
- Reasoning: item.Reasoning,
- Input: append([]string(nil), item.Input...),
- })
- }
- models, _, err := normalizeAgentAccountModels(account, requested, "", true)
- if err != nil {
- return nil, err
- }
- return models, nil
-}
-
-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) {
- models, err := ensurePersistedAgentAccountModels(account)
- if err != nil {
- return nil, err
- }
- return models, nil
-}
-
-func LoadLegacyAgentAccountModelsForMigration(account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
- if account == nil {
- return nil, fmt.Errorf("account is required")
- }
- if !hasLegacyAgentAccountModels(account) {
- return nil, nil
- }
- models, _, err := loadLegacyAgentAccountModelCatalog(account)
- if err != nil {
- if strings.TrimSpace(err.Error()) == "model is required" {
- return nil, nil
- }
- return nil, err
- }
- return models, nil
-}
-
-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 {
- if strings.TrimSpace(item.ID) == "" {
- continue
- }
- seen[strings.TrimSpace(item.ID)] = struct{}{}
- }
- for _, item := range meta.Models {
- if _, ok := seen[strings.TrimSpace(item.ID)]; ok {
- continue
- }
- requested = append(requested, dto.AgentAccountModel{
- ID: item.ID,
- Name: item.Name,
- ContextWindow: item.ContextWindow,
- MaxTokens: item.MaxTokens,
- Reasoning: item.Reasoning,
- Input: append([]string(nil), item.Input...),
- })
- }
- if len(requested) == len(existing) {
- return append([]dto.AgentAccountModel(nil), existing...), nil
- }
- normalized, _, err := normalizeAgentAccountModels(account, requested, "", true)
- if err != nil {
- return nil, err
- }
- return normalized, nil
-}
-
-func hasLegacyAgentAccountModels(account *model.AgentAccount) bool {
- if account == nil {
- return false
- }
- if strings.TrimSpace(account.Models) != "" || strings.TrimSpace(account.Model) != "" {
- return true
- }
- if account.ID > 0 {
- if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil {
- for _, agent := range agents {
- if strings.TrimSpace(agent.Model) != "" {
- return true
- }
- }
- }
- }
- if definitions, ok := providerDefinitions()[strings.ToLower(strings.TrimSpace(account.Provider))]; ok {
- return len(definitions.Models) > 0
- }
- return false
-}
-
-func ensurePersistedAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
- if account == nil {
- return nil, fmt.Errorf("account is required")
- }
- models, err := listPersistedAgentAccountModels(account.ID)
- if err != nil {
- return nil, err
- }
- if len(models) > 0 {
- return models, nil
- }
- if !hasLegacyAgentAccountModels(account) {
- return nil, nil
- }
- legacyModels, _, err := loadLegacyAgentAccountModelCatalog(account)
- if err != nil {
- if strings.TrimSpace(err.Error()) == "model is required" {
- return nil, nil
- }
- return nil, err
- }
- if len(legacyModels) == 0 {
- return nil, nil
- }
- if account.ID == 0 {
- return legacyModels, nil
- }
- if err := replacePersistedAgentAccountModels(account.ID, legacyModels); err != nil {
- return nil, err
- }
- return listPersistedAgentAccountModels(account.ID)
-}
-
-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 {
- inputs := []string{}
- if strings.TrimSpace(row.Input) != "" {
- _ = json.Unmarshal([]byte(row.Input), &inputs)
- }
- result = append(result, dto.AgentAccountModel{
- RecordID: row.ID,
- ID: strings.TrimSpace(row.Model),
- Name: strings.TrimSpace(row.Name),
- ContextWindow: row.ContextWindow,
- MaxTokens: row.MaxTokens,
- Reasoning: row.Reasoning,
- Input: sanitizeAgentAccountModelInputs(inputs),
- })
- }
- return result, nil
-}
-
-func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccountModel) error {
- return global.DB.Transaction(func(tx *gorm.DB) error {
- if err := tx.Where("account_id = ?", accountID).Delete(&model.AgentAccountModel{}).Error; err != nil {
- return err
- }
- for index, item := range models {
- inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
- if err != nil {
- return err
- }
- record := &model.AgentAccountModel{
- AccountID: accountID,
- Model: strings.TrimSpace(item.ID),
- Name: strings.TrimSpace(item.Name),
- ContextWindow: item.ContextWindow,
- MaxTokens: item.MaxTokens,
- Reasoning: item.Reasoning,
- Input: string(inputPayload),
- SortOrder: index + 1,
- }
- if err := tx.Create(record).Error; err != nil {
- return err
- }
- }
- return nil
- })
-}
-
-func loadLegacyAgentAccountModelCatalog(account *model.AgentAccount) ([]dto.AgentAccountModel, string, error) {
- if account == nil {
- return nil, "", fmt.Errorf("account is required")
- }
- models, err := parseAgentAccountModels(account.Models)
- if err != nil {
- return nil, "", err
- }
- normalized, _, err := normalizeAgentAccountModels(account, models, account.Model, true)
- if err != nil {
- return nil, "", err
- }
- _, defaultModel, err := normalizeAgentAccountModels(account, normalized, account.Model, true)
- if err != nil {
- return nil, "", err
- }
- return normalized, defaultModel, nil
-}
-
-func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel, defaultModel string, allowFallbackDefault bool) ([]dto.AgentAccountModel, string, error) {
- requested := append([]dto.AgentAccountModel(nil), models...)
- if len(requested) == 0 {
- if strings.TrimSpace(defaultModel) != "" {
- requested = []dto.AgentAccountModel{{ID: defaultModel}}
- } else {
- requested = buildLegacyAgentAccountModels(account)
- }
- }
- 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")
- }
-
- resolvedDefault := strings.TrimSpace(defaultModel)
- if resolvedDefault != "" {
- defaultItem, err := normalizeAgentAccountModel(account, dto.AgentAccountModel{ID: resolvedDefault})
- if err == nil {
- resolvedDefault = defaultItem.ID
- }
- }
- if resolvedDefault == "" && allowFallbackDefault {
- resolvedDefault = normalized[0].ID
- }
- if _, ok := findAgentAccountModelForProvider(account.Provider, normalized, resolvedDefault); !ok {
- if allowFallbackDefault {
- resolvedDefault = normalized[0].ID
- } else {
- return nil, "", buserr.New("ErrAgentModelNotInAccount")
- }
- }
- return normalized, resolvedDefault, 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")
- }
- primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, model.MaxTokens, model.ContextWindow)
- if err != nil {
- return dto.AgentAccountModel{}, err
- }
- name := strings.TrimSpace(model.Name)
- if name == "" {
- name = strings.TrimSpace(inferredEntry.Name)
- }
- reasoning := model.Reasoning
- if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 {
- reasoning = inferredEntry.Reasoning
- }
- inputs := sanitizeAgentAccountModelInputs(model.Input)
- if len(inputs) == 0 {
- inputs = sanitizeAgentAccountModelInputs(inferredEntry.Input)
- }
- contextWindow := model.ContextWindow
- if contextWindow <= 0 {
- contextWindow = inferredEntry.ContextWindow
- }
- maxTokens := model.MaxTokens
- if maxTokens <= 0 {
- maxTokens = inferredEntry.MaxTokens
- }
- return dto.AgentAccountModel{
- ID: normalizeAgentAccountModelID(account.Provider, primaryModel, modelID),
- Name: name,
- ContextWindow: contextWindow,
- MaxTokens: maxTokens,
- Reasoning: reasoning,
- Input: inputs,
- }, nil
-}
-
-func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) string {
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "custom", "vllm":
- target := requestedID
- if strings.TrimSpace(target) == "" {
- target = primaryModel
- }
- return normalizeCustomModel(target)
- case "ollama":
- target := strings.TrimSpace(primaryModel)
- if strings.HasPrefix(target, "ollama/") {
- return target
- }
- target = strings.TrimSpace(requestedID)
- if strings.HasPrefix(target, "ollama/") {
- return target
- }
- target = strings.TrimLeft(strings.TrimSpace(target), "/")
- if target == "" {
- target = strings.TrimLeft(strings.TrimSpace(primaryModel), "/")
- }
- if target == "" {
- return ""
- }
- return "ollama/" + target
- default:
- target := strings.TrimSpace(requestedID)
- if target == "" {
- target = strings.TrimSpace(primaryModel)
- }
- if target == "" {
- return ""
- }
- prefix := poolModelPrefix(provider)
- if strings.Contains(target, "/") {
- parts := strings.SplitN(target, "/", 2)
- targetPrefix := strings.ToLower(strings.TrimSpace(parts[0]))
- targetModel := strings.TrimSpace(parts[1])
- if targetModel == "" {
- return strings.TrimSpace(target)
- }
- for _, item := range supportedProviderModelPrefixes(provider) {
- if item == targetPrefix {
- if prefix != "" {
- return prefix + "/" + targetModel
- }
- return strings.TrimSpace(target)
- }
- }
- return strings.TrimSpace(target)
- }
- target = strings.TrimLeft(strings.TrimSpace(target), "/")
- if prefix == "" {
- return target
- }
- return prefix + "/" + target
- }
-}
-
-func buildLegacyAgentAccountModels(account *model.AgentAccount) []dto.AgentAccountModel {
- modelIDs := make([]string, 0, 4)
- seen := make(map[string]struct{}, 4)
- appendModel := func(value string) {
- target := strings.TrimSpace(value)
- if target == "" {
- return
- }
- if _, ok := seen[target]; ok {
- return
- }
- seen[target] = struct{}{}
- modelIDs = append(modelIDs, target)
- }
- appendModel(account.Model)
- if account.ID > 0 {
- if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil {
- for _, agent := range agents {
- appendModel(agent.Model)
- }
- }
- }
- if definitions, ok := providerDefinitions()[strings.ToLower(strings.TrimSpace(account.Provider))]; ok && len(definitions.Models) > 0 {
- for _, item := range definitions.Models {
- appendModel(item.ID)
- }
- }
- models := make([]dto.AgentAccountModel, 0, len(modelIDs))
- for _, modelID := range modelIDs {
- models = append(models, dto.AgentAccountModel{ID: modelID})
- }
- return models
-}
-
-func parseAgentAccountModels(value string) ([]dto.AgentAccountModel, error) {
- trim := strings.TrimSpace(value)
- if trim == "" {
- return nil, nil
- }
- var models []dto.AgentAccountModel
- if err := json.Unmarshal([]byte(trim), &models); err != nil {
- return nil, err
- }
- return models, nil
-}
-
-func marshalAgentAccountModels(models []dto.AgentAccountModel) (string, error) {
- payload, err := json.Marshal(models)
- if err != nil {
- return "", err
- }
- return string(payload), nil
-}
-
-func sanitizeAgentAccountModelInputs(values []string) []string {
- result := make([]string, 0, len(values))
- seen := make(map[string]struct{}, len(values))
- for _, value := range values {
- normalized := strings.ToLower(strings.TrimSpace(value))
- if normalized != "text" && normalized != "image" {
- continue
- }
- if _, ok := seen[normalized]; ok {
- continue
- }
- seen[normalized] = struct{}{}
- result = append(result, normalized)
- }
- if len(result) == 0 {
- return []string{"text"}
- }
- return result
-}
-
-func shouldRefreshAccountModelRuntimeLimits(provider string) bool {
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "custom", "vllm", "ollama":
- return true
- default:
- return false
- }
-}
-
-func refreshAccountModelRuntimeLimits(account *model.AgentAccount, models []dto.AgentAccountModel) []dto.AgentAccountModel {
- refreshed := make([]dto.AgentAccountModel, 0, len(models))
- for _, item := range models {
- next := item
- next.MaxTokens = account.MaxTokens
- next.ContextWindow = account.ContextWindow
- refreshed = append(refreshed, next)
- }
- return refreshed
-}
-
-func normalizeComparableProviderModelID(provider, modelID string) string {
- target := strings.TrimSpace(modelID)
- if target == "" {
- return ""
- }
- if !strings.Contains(target, "/") {
- return target
- }
- parts := strings.SplitN(target, "/", 2)
- prefix := strings.ToLower(strings.TrimSpace(parts[0]))
- model := strings.TrimSpace(parts[1])
- if model == "" {
- return target
- }
- for _, item := range supportedProviderModelPrefixes(provider) {
- if item == prefix {
- return model
- }
- }
- return target
-}
-
-func sameProviderModelID(provider, left, right string) bool {
- leftTrimmed := strings.TrimSpace(left)
- rightTrimmed := strings.TrimSpace(right)
- if leftTrimmed == rightTrimmed {
- return true
- }
- leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
- rightComparable := normalizeComparableProviderModelID(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 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 strings.TrimSpace(agent.Model) == "" {
- continue
- }
- 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 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 mapToModelsConfig(value map[string]interface{}) (*modelsConfig, error) {
- payload, err := json.Marshal(value)
- if err != nil {
- return nil, err
- }
- result := &modelsConfig{}
- if err := json.Unmarshal(payload, result); err != nil {
- return nil, err
- }
- return result, nil
-}
-
-func providerEnvKey(provider string) string {
- return providercatalog.EnvKey(provider)
-}
-
-type providerDefinition struct {
- Sort uint
- DisplayName string
- BaseURL string
- Models []dto.ProviderModelInfo
-}
-
-func providerDefinitions() map[string]providerDefinition {
- definitions := map[string]providerDefinition{}
- for key, meta := range providercatalog.All() {
- if !meta.Enabled {
- continue
- }
- models := make([]dto.ProviderModelInfo, 0, len(meta.Models))
- for _, m := range meta.Models {
- models = append(models, dto.ProviderModelInfo{
- ID: m.ID,
- Name: m.Name,
- ContextWindow: m.ContextWindow,
- MaxTokens: m.MaxTokens,
- Reasoning: m.Reasoning,
- Input: append([]string(nil), m.Input...),
- })
- }
- definitions[key] = providerDefinition{
- Sort: meta.Sort,
- DisplayName: meta.DisplayName,
- BaseURL: meta.DefaultBaseURL,
- Models: models,
- }
- }
- return definitions
-}
-
-func providerDefaultBaseURL(provider string) (string, bool) {
- return providercatalog.DefaultBaseURL(provider)
-}
-
-func fixedProviderBaseURL(provider string) (string, bool) {
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "bailian-coding-plan":
- return providerDefaultBaseURL(provider)
- case "ark-coding-plan":
- return providerDefaultBaseURL(provider)
- default:
- return "", false
- }
-}
-
-func isSupportedAgentProvider(provider string) bool {
- return providercatalog.IsEnabled(provider)
-}
-
-func providerDisplayName(provider string) string {
- return providercatalog.DisplayName(provider)
-}
-
-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
-}
-
-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 normalizeCustomModel(modelName string) string {
- trim := strings.TrimSpace(modelName)
- trim = strings.TrimLeft(trim, "/")
- if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
- if strings.EqualFold(parts[0], "custom") {
- return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
- }
- }
- return trim
-}
-
-func normalizeBailianCodingPlanModelID(modelID string) string {
- trim := strings.TrimSpace(modelID)
- switch strings.ToLower(trim) {
- case "minimax-m2.5", "minimax m2.5", "minimax/minimax-m2.5", "minimax/minimax m2.5":
- return "MiniMax/MiniMax-M2.5"
- default:
- return trim
- }
-}
-
-func normalizeArkCodingPlanModelID(modelID string) string {
- return strings.ToLower(strings.TrimSpace(modelID))
-}
-
-func zaiModelDisplayName(modelID string) string {
- switch strings.ToLower(strings.TrimSpace(modelID)) {
- case "glm-5":
- return "GLM-5"
- case "glm-4.7":
- return "GLM-4.7"
- case "glm-4.7-flash":
- return "GLM-4.7-Flash"
- case "glm-4.7-flashx":
- return "GLM-4.7-FlashX"
- default:
- return strings.TrimSpace(modelID)
- }
-}
-
-func bailianPrimaryModelID(modelID string) string {
- trim := strings.TrimSpace(modelID)
- if trim == "" {
- return ""
- }
- parts := strings.Split(trim, "/")
- for i := len(parts) - 1; i >= 0; i-- {
- part := strings.TrimSpace(parts[i])
- if part != "" {
- return part
- }
- }
- return trim
-}
-
-func normalizeAgentType(agentType string) string {
- trim := strings.ToLower(strings.TrimSpace(agentType))
- if trim == "" {
- return constant.AppOpenclaw
- }
- return trim
-}
-
-func modelMatchesProvider(provider, modelName string) bool {
- target := strings.TrimSpace(modelName)
- for _, prefix := range supportedProviderModelPrefixes(provider) {
- if prefix != "" && strings.HasPrefix(target, prefix+"/") {
- return true
- }
- }
- return false
-}
-
-func runtimeProviderModelPrefix(provider string) string {
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "gemini":
- return "google"
- case "minimax":
- return "minimax-portal"
- case "kimi":
- return "moonshot"
- default:
- return strings.ToLower(strings.TrimSpace(provider))
- }
-}
-
-func poolModelPrefix(provider string) string {
- target := strings.ToLower(strings.TrimSpace(provider))
- if definitions, ok := providerDefinitions()[target]; ok && len(definitions.Models) > 0 {
- parts := strings.SplitN(strings.TrimSpace(definitions.Models[0].ID), "/", 2)
- if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
- return strings.ToLower(strings.TrimSpace(parts[0]))
- }
- }
- return target
-}
-
-func supportedProviderModelPrefixes(provider string) []string {
- values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
- result := make([]string, 0, len(values))
- seen := make(map[string]struct{}, len(values))
- for _, value := range values {
- target := strings.ToLower(strings.TrimSpace(value))
- if target == "" {
- continue
- }
- if _, ok := seen[target]; ok {
- continue
- }
- seen[target] = struct{}{}
- result = append(result, target)
- }
- return result
-}
-
-func isSupportedAgentType(agentType string) bool {
- switch normalizeAgentType(agentType) {
- case constant.AppOpenclaw, constant.AppCopaw:
- return true
- default:
- return false
- }
-}
-
-func normalizeAPIType(apiType string) string {
- trim := strings.ToLower(strings.TrimSpace(apiType))
- if trim == "" {
- return "openai-completions"
- }
- return trim
-}
-
-func isSupportedAPIType(apiType string) bool {
- switch normalizeAPIType(apiType) {
- case "openai-completions", "openai-responses", "anthropic-messages":
- return true
- default:
- return false
- }
-}
-
-func isSupportedOllamaAPIType(apiType string) bool {
- switch normalizeAPIType(apiType) {
- case "openai-completions", "openai-responses":
- return true
- default:
- return false
- }
-}
-
-func resolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
- resolvedAPI := normalizeAPIType(apiType)
- if provider == "ollama" && !isSupportedOllamaAPIType(resolvedAPI) {
- resolvedAPI = "openai-responses"
- }
- resolvedMaxTokens := maxTokens
- resolvedContextWindow := contextWindow
- if resolvedMaxTokens <= 0 {
- switch provider {
- case "deepseek":
- resolvedMaxTokens = 8192
- case "zai":
- resolvedMaxTokens = 131072
- case "openrouter":
- resolvedMaxTokens = 8192
- case "minimax", "kimi-coding", "custom":
- resolvedMaxTokens = 8192
- default:
- resolvedMaxTokens = 8192
- }
- }
- if resolvedContextWindow <= 0 {
- switch provider {
- case "deepseek":
- resolvedContextWindow = 128000
- case "zai":
- resolvedContextWindow = 204800
- case "openrouter":
- resolvedContextWindow = 128000
- case "minimax", "kimi-coding":
- resolvedContextWindow = 200000
- case "custom", "vllm":
- resolvedContextWindow = 128000
- default:
- resolvedContextWindow = 256000
- }
- }
- return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
-}
-
-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" {
- return
- }
- provider = strings.ToLower(strings.TrimSpace(provider))
- if 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)
-}
diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go
new file mode 100644
index 000000000..b234164e9
--- /dev/null
+++ b/agent/app/service/agents_utils.go
@@ -0,0 +1,2006 @@
+package service
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "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/utils/cmd"
+ "github.com/1Panel-dev/1Panel/agent/utils/common"
+ "github.com/1Panel-dev/1Panel/agent/utils/files"
+ openclawutil "github.com/1Panel-dev/1Panel/agent/utils/openclaw"
+ "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
+ "gorm.io/gorm"
+)
+
+type AgentService struct{}
+
+func NewIAgentService() IAgentService {
+ return &AgentService{}
+}
+
+type resolvedAgentAccountVerification struct {
+ Provider string
+ APIKey string
+ BaseURL string
+}
+
+type resolvedAgentAccountInput struct {
+ Provider string
+ APIKey string
+ BaseURL string
+ APIType string
+}
+
+func resolveAgentAccountVerification(provider, apiKey, baseURL string) (resolvedAgentAccountVerification, error) {
+ resolvedAPIKey := strings.TrimSpace(apiKey)
+ if resolvedAPIKey == "" {
+ return resolvedAgentAccountVerification{}, buserr.New("ErrAgentApiKeyRequired")
+ }
+ resolvedBaseURL := strings.TrimSpace(baseURL)
+ if fixedURL, ok := fixedProviderBaseURL(provider); ok {
+ resolvedBaseURL = fixedURL
+ }
+ if (provider == "custom" || provider == "vllm") && resolvedBaseURL == "" {
+ return resolvedAgentAccountVerification{}, buserr.New("ErrAgentBaseURLRequired")
+ }
+ if provider != "custom" && provider != "vllm" && resolvedBaseURL == "" {
+ if defaultURL, ok := providerDefaultBaseURL(provider); ok {
+ resolvedBaseURL = defaultURL
+ }
+ }
+ if provider == "ollama" && resolvedBaseURL == "" {
+ return resolvedAgentAccountVerification{}, buserr.New("ErrAgentBaseURLRequired")
+ }
+ return resolvedAgentAccountVerification{
+ Provider: provider,
+ APIKey: resolvedAPIKey,
+ BaseURL: resolvedBaseURL,
+ }, nil
+}
+
+func verifyResolvedAgentAccount(input resolvedAgentAccountVerification) error {
+ if providercatalog.SkipVerification(input.Provider) {
+ return nil
+ }
+ return providercatalog.VerifyAccount(input.Provider, input.BaseURL, input.APIKey)
+}
+
+func resolveAgentAccountAPIType(provider, apiType, fallbackAPIType string) (string, error) {
+ resolvedAPIType := normalizeAPIType(apiType)
+ if provider == "custom" || provider == "vllm" {
+ if !isSupportedAPIType(resolvedAPIType) {
+ return "", fmt.Errorf("apiType is invalid")
+ }
+ return resolvedAPIType, nil
+ }
+ if provider == "ollama" {
+ if apiType == "" && fallbackAPIType != "" {
+ resolvedAPIType = normalizeAPIType(fallbackAPIType)
+ if !isSupportedOllamaAPIType(resolvedAPIType) {
+ resolvedAPIType = "openai-responses"
+ }
+ return resolvedAPIType, nil
+ }
+ if !isSupportedOllamaAPIType(resolvedAPIType) {
+ return "", fmt.Errorf("apiType is invalid")
+ }
+ return resolvedAPIType, nil
+ }
+ if fallbackAPIType != "" {
+ return normalizeAPIType(fallbackAPIType), nil
+ }
+ return resolvedAPIType, nil
+}
+
+func resolveAgentAccountInput(provider, apiKey, baseURL, apiType, fallbackAPIType string) (resolvedAgentAccountInput, error) {
+ resolvedVerification, err := resolveAgentAccountVerification(provider, apiKey, baseURL)
+ if err != nil {
+ return resolvedAgentAccountInput{}, err
+ }
+ if err := verifyResolvedAgentAccount(resolvedVerification); err != nil {
+ return resolvedAgentAccountInput{}, err
+ }
+ resolvedAPIType, err := resolveAgentAccountAPIType(provider, apiType, fallbackAPIType)
+ if err != nil {
+ return resolvedAgentAccountInput{}, err
+ }
+ return resolvedAgentAccountInput{
+ Provider: provider,
+ APIKey: resolvedVerification.APIKey,
+ BaseURL: resolvedVerification.BaseURL,
+ APIType: resolvedAPIType,
+ }, 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))
+ }
+ }
+ 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) {
+ 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 extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
+ result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"}
+ channels, ok := conf["channels"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ feishu, ok := channels["feishu"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if enabled, ok := feishu["enabled"].(bool); ok {
+ result.Enabled = enabled
+ }
+ if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
+ result.DmPolicy = dmPolicy
+ }
+ accounts, ok := feishu["accounts"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ main, ok := accounts["main"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if appID, ok := main["appId"].(string); ok {
+ result.AppID = appID
+ }
+ if appSecret, ok := main["appSecret"].(string); ok {
+ result.AppSecret = appSecret
+ }
+ if botName, ok := main["botName"].(string); ok {
+ result.BotName = botName
+ }
+ return result
+}
+
+func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) {
+ channels := ensureChildMap(conf, "channels")
+ feishu := ensureChildMap(channels, "feishu")
+ feishu["enabled"] = config.Enabled
+ feishu["dmPolicy"] = config.DmPolicy
+
+ accounts := ensureChildMap(feishu, "accounts")
+ main := ensureChildMap(accounts, "main")
+ main["appId"] = config.AppID
+ main["appSecret"] = config.AppSecret
+ main["botName"] = config.BotName
+
+ if strings.EqualFold(config.DmPolicy, "open") {
+ feishu["allowFrom"] = []string{"*"}
+ }
+}
+
+func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) {
+ plugins := ensureChildMap(conf, "plugins")
+ entries := ensureChildMap(plugins, "entries")
+ feishu := ensureChildMap(entries, "feishu")
+ feishu["enabled"] = enabled
+}
+
+func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig {
+ result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"}
+ channels, ok := conf["channels"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ telegram, ok := channels["telegram"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if enabled, ok := telegram["enabled"].(bool); ok {
+ result.Enabled = enabled
+ }
+ if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
+ result.DmPolicy = dmPolicy
+ }
+ if botToken, ok := telegram["botToken"].(string); ok {
+ result.BotToken = botToken
+ }
+ if proxy, ok := telegram["proxy"].(string); ok {
+ result.Proxy = proxy
+ }
+ return result
+}
+
+func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) {
+ channels := ensureChildMap(conf, "channels")
+ telegram := map[string]interface{}{
+ "enabled": config.Enabled,
+ "dmPolicy": config.DmPolicy,
+ "botToken": config.BotToken,
+ }
+ if strings.EqualFold(config.DmPolicy, "open") {
+ telegram["allowFrom"] = []string{"*"}
+ }
+ if strings.TrimSpace(config.Proxy) != "" {
+ telegram["proxy"] = strings.TrimSpace(config.Proxy)
+ }
+ channels["telegram"] = telegram
+}
+
+func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
+ result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"}
+ channels, ok := conf["channels"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ discord, ok := channels["discord"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if enabled, ok := discord["enabled"].(bool); ok {
+ result.Enabled = enabled
+ }
+ if token, ok := discord["token"].(string); ok {
+ result.Token = token
+ }
+ if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" {
+ result.GroupPolicy = groupPolicy
+ }
+ if proxy, ok := discord["proxy"].(string); ok {
+ result.Proxy = proxy
+ }
+ if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" {
+ result.DmPolicy = policy
+ return result
+ }
+ // backward compatibility: old nested style
+ dm, ok := discord["dm"].(map[string]interface{})
+ if ok {
+ if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" {
+ result.DmPolicy = policy
+ }
+ }
+ return result
+}
+
+func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) {
+ channels := ensureChildMap(conf, "channels")
+ discord := ensureChildMap(channels, "discord")
+ discord["enabled"] = config.Enabled
+ discord["token"] = config.Token
+ discord["dmPolicy"] = config.DmPolicy
+ discord["groupPolicy"] = config.GroupPolicy
+ if strings.EqualFold(config.DmPolicy, "open") {
+ discord["allowFrom"] = []string{"*"}
+ } else {
+ delete(discord, "allowFrom")
+ }
+ if strings.TrimSpace(config.Proxy) != "" {
+ discord["proxy"] = strings.TrimSpace(config.Proxy)
+ } else {
+ delete(discord, "proxy")
+ }
+ delete(discord, "dm")
+}
+
+func 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 extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
+ result := dto.AgentQQBotConfig{Enabled: true}
+ channels, ok := conf["channels"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ qqbot, ok := channels["qqbot"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if enabled, ok := qqbot["enabled"].(bool); ok {
+ result.Enabled = enabled
+ }
+ if appID, ok := qqbot["appId"].(string); ok {
+ result.AppID = appID
+ }
+ if clientSecret, ok := qqbot["clientSecret"].(string); ok {
+ result.ClientSecret = clientSecret
+ }
+ return result
+}
+
+func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
+ result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"}
+ channels, ok := conf["channels"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ wecom, ok := channels["wecom"].(map[string]interface{})
+ if !ok {
+ return result
+ }
+ if enabled, ok := wecom["enabled"].(bool); ok {
+ result.Enabled = enabled
+ }
+ if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
+ result.DmPolicy = strings.TrimSpace(dmPolicy)
+ }
+ if botID, ok := wecom["botId"].(string); ok {
+ result.BotID = botID
+ }
+ if secret, ok := wecom["secret"].(string); ok {
+ result.Secret = secret
+ }
+ return result
+}
+
+func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
+ channels := ensureChildMap(conf, "channels")
+ wecom := ensureChildMap(channels, "wecom")
+ wecom["enabled"] = config.Enabled
+ wecom["botId"] = strings.TrimSpace(config.BotID)
+ wecom["secret"] = strings.TrimSpace(config.Secret)
+ wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy)
+ if strings.EqualFold(config.DmPolicy, "open") {
+ wecom["allowFrom"] = []string{"*"}
+ } else {
+ wecom["allowFrom"] = []string{}
+ }
+
+ plugins := ensureChildMap(conf, "plugins")
+ entries := ensureChildMap(plugins, "entries")
+ wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
+ wecomEntry["enabled"] = config.Enabled
+}
+
+func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
+ channels := ensureChildMap(conf, "channels")
+ qqbot := ensureChildMap(channels, "qqbot")
+ qqbot["enabled"] = config.Enabled
+ qqbot["allowFrom"] = []string{"*"}
+ qqbot["appId"] = strings.TrimSpace(config.AppID)
+ qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret)
+
+ plugins := ensureChildMap(conf, "plugins")
+ entries := ensureChildMap(plugins, "entries")
+ qqbotEntry := ensureChildMap(entries, "qqbot")
+ qqbotEntry["enabled"] = config.Enabled
+}
+
+func resolvePluginMeta(pluginType string) (string, string, error) {
+ switch pluginType {
+ case "qqbot":
+ return "@sliverp/qqbot@latest", "qqbot", nil
+ case "wecom":
+ return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
+ default:
+ return "", "", fmt.Errorf("unsupported plugin type")
+ }
+}
+
+func checkPluginInstalled(containerName, pluginType string) (bool, error) {
+ _, pluginDir, err := resolvePluginMeta(pluginType)
+ if err != nil {
+ return false, err
+ }
+ if strings.TrimSpace(containerName) == "" {
+ return false, buserr.New("ErrRecordNotFound")
+ }
+ pluginPath := path.Join(openclawPluginBaseDir, pluginDir)
+ mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
+ if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil {
+ return false, nil
+ }
+ return true, nil
+}
+
+func extractOtherConfig(conf map[string]interface{}) dto.AgentOtherConfig {
+ 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
+}
+
+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 := normalizeAgentType(agent.AgentType)
+ if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw {
+ agentType = constant.AppCopaw
+ }
+ item := dto.AgentItem{
+ ID: agent.ID,
+ Name: agent.Name,
+ AgentType: agentType,
+ Provider: agent.Provider,
+ ProviderName: providerDisplayName(agent.Provider),
+ Model: agent.Model,
+ APIType: agent.APIType,
+ MaxTokens: agent.MaxTokens,
+ ContextWindow: agent.ContextWindow,
+ BaseURL: agent.BaseURL,
+ APIKey: maskKey(agent.APIKey),
+ Token: agent.Token,
+ Status: agent.Status,
+ Message: agent.Message,
+ AppInstallID: agent.AppInstallID,
+ 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 isOpenclawHTTPSVersion(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)
+ }
+ }
+ }
+ return item
+}
+
+func isOpenclawHTTPSVersion(version string) bool {
+ target := strings.TrimSpace(strings.ToLower(version))
+ if target == "" || target == "latest" {
+ return true
+ }
+ if !strings.ContainsAny(target, "0123456789") {
+ return true
+ }
+ return common.CompareAppVersion(target, openclawHTTPSVersion)
+}
+
+func shouldMigrateOpenclawHTTPSUpgrade(install *model.AppInstall, fromVersion, toVersion string) bool {
+ if install == nil || install.App.Key != constant.AppOpenclaw {
+ return false
+ }
+ return !isOpenclawHTTPSVersion(fromVersion) && isOpenclawHTTPSVersion(toVersion)
+}
+
+func migrateOpenclawHTTPSUpgrade(install *model.AppInstall, fromVersion, toVersion string) error {
+ systemIP, _ := settingRepo.GetValueByKey("SystemIP")
+ return migrateOpenclawHTTPSUpgradeWithSystemIP(install, fromVersion, toVersion, systemIP)
+}
+
+func migrateOpenclawHTTPSUpgradeWithSystemIP(install *model.AppInstall, fromVersion, toVersion, systemIP string) error {
+ if !shouldMigrateOpenclawHTTPSUpgrade(install, fromVersion, toVersion) {
+ return nil
+ }
+ migrateOpenclawInstallPorts(install)
+ if err := openclawutil.WriteCatchAllCaddyfile(install.GetPath()); err != nil {
+ return err
+ }
+ 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
+ }
+ if install.HttpsPort > 0 {
+ allowedOrigin, err := buildOpenclawAllowedOrigin(originHost, install.HttpsPort)
+ 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 migrateOpenclawInstallPorts(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 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
+ }
+ if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
+ envMap["ALLOWED_ORIGIN"] = allowedOrigin
+ }
+ delete(envMap, "PANEL_APP_PORT_HTTP")
+ 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(host string, port int) (string, error) {
+ host = strings.TrimSpace(host)
+ if 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("https://%s:%d", host, port))
+}
+
+func checkAgentUpgradable(install model.AppInstall) bool {
+ if install.ID == 0 || install.Version == "" || install.Version == "latest" {
+ return false
+ }
+ if install.App.ID == 0 {
+ 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"`
+}
+
+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"`
+ Models []modelEntry `json:"models,omitempty"`
+}
+
+type modelEntry struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Reasoning bool `json:"reasoning"`
+ Input []string `json:"input"`
+ ContextWindow int `json:"contextWindow"`
+ MaxTokens int `json:"maxTokens"`
+ Cost modelCost `json:"cost"`
+}
+
+type modelCost struct {
+ Input float64 `json:"input"`
+ Output float64 `json:"output"`
+ CacheRead float64 `json:"cacheRead"`
+ CacheWrite float64 `json:"cacheWrite"`
+}
+
+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) 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
+ }
+
+ cfg := openclawConfig{
+ Gateway: gatewayConfig{
+ Mode: "local",
+ Bind: "loopback",
+ 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},
+ 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 cfg.Models != nil {
+ modelsMap, err := structToMap(cfg.Models)
+ if err != nil {
+ return err
+ }
+ conf["models"] = modelsMap
+ }
+ 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 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"] = "loopback"
+ }
+ 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
+ }
+ envPath := path.Join(confDir, ".env")
+ lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
+ if envKey := providerEnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" {
+ lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
+ }
+ content := strings.Join(lines, "\n") + "\n"
+ return fileOp.SaveFile(envPath, content, 0600)
+}
+
+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); 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, fmt.Errorf("model is required")
+ }
+ selectedModel = strings.TrimSpace(selectedModel)
+ if selectedModel == "" {
+ selectedModel = strings.TrimSpace(accountModels[0].ID)
+ }
+ if selectedModel == "" {
+ return "", nil, nil, fmt.Errorf("model is required")
+ }
+ 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 := buildOpenclawCatalogModel(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
+ }
+ 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")
+ }
+ providerCfg.Models = entries
+ return primaryModel, defaultsModels, &modelsConfig{
+ Mode: "merge",
+ Providers: map[string]modelProvider{
+ providerKey: providerCfg,
+ },
+ }, nil
+}
+
+func buildOpenclawCatalogModel(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
+ primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.MaxTokens, model.ContextWindow)
+ if err != nil {
+ return "", modelEntry{}, "", modelProvider{}, err
+ }
+ if strings.TrimSpace(model.Name) != "" {
+ inferredEntry.Name = strings.TrimSpace(model.Name)
+ }
+ if len(model.Input) > 0 {
+ inferredEntry.Input = sanitizeAgentAccountModelInputs(model.Input)
+ }
+ inferredEntry.Reasoning = model.Reasoning
+ if model.ContextWindow > 0 {
+ inferredEntry.ContextWindow = model.ContextWindow
+ }
+ if model.MaxTokens > 0 {
+ inferredEntry.MaxTokens = model.MaxTokens
+ }
+ return primaryModel, inferredEntry, providerKey, providerCfg, nil
+}
+
+type openclawAccountModelRuntime struct {
+ StoredModel string
+ PrimaryModel string
+ APIType string
+ MaxTokens int
+ ContextWindow int
+}
+
+func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
+ apiType, maxTokens, contextWindow := resolveRuntimeParams(
+ account.Provider,
+ account.APIType,
+ model.MaxTokens,
+ model.ContextWindow,
+ )
+ primaryModel, _, _, _, err := buildOpenclawCatalogModel(account, model)
+ if err != nil {
+ return openclawAccountModelRuntime{}, err
+ }
+ return openclawAccountModelRuntime{
+ StoredModel: model.ID,
+ PrimaryModel: primaryModel,
+ APIType: apiType,
+ MaxTokens: maxTokens,
+ ContextWindow: contextWindow,
+ }, 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 inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) {
+ baseURL := resolveAccountBaseURL(account)
+ resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow)
+ patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey)
+ if err != nil {
+ return "", modelEntry{}, "", modelProvider{}, err
+ }
+ if patch.Models == nil {
+ return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is required")
+ }
+ modelsCfg, err := mapToModelsConfig(patch.Models)
+ if err != nil {
+ return "", modelEntry{}, "", modelProvider{}, err
+ }
+ for key, providerCfg := range modelsCfg.Providers {
+ if len(providerCfg.Models) == 0 {
+ continue
+ }
+ return patch.PrimaryModel, providerCfg.Models[0], key, modelProvider{
+ ApiKey: providerCfg.ApiKey,
+ BaseUrl: providerCfg.BaseUrl,
+ Api: providerCfg.Api,
+ }, nil
+ }
+ return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is invalid")
+}
+
+func resolveAccountBaseURL(account *model.AgentAccount) string {
+ baseURL := strings.TrimSpace(account.BaseURL)
+ if baseURL == "" {
+ if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok {
+ baseURL = defaultURL
+ }
+ }
+ return baseURL
+}
+
+func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
+ if account == nil {
+ return nil, fmt.Errorf("account is required")
+ }
+ if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
+ return nil, buserr.New("ErrAgentAccountSingleInitialModel")
+ }
+ if len(requested) > 0 {
+ models, _, err := normalizeAgentAccountModels(account, requested, "", true)
+ if err != nil {
+ return nil, err
+ }
+ return models, nil
+ }
+ meta, ok := providercatalog.Get(account.Provider)
+ if !ok || len(meta.Models) == 0 {
+ if requiresInitialAgentAccountModels(account.Provider) {
+ return nil, buserr.New("ErrAgentAccountModelsRequired")
+ }
+ return nil, nil
+ }
+ requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
+ for _, item := range meta.Models {
+ requested = append(requested, dto.AgentAccountModel{
+ ID: item.ID,
+ Name: item.Name,
+ ContextWindow: item.ContextWindow,
+ MaxTokens: item.MaxTokens,
+ Reasoning: item.Reasoning,
+ Input: append([]string(nil), item.Input...),
+ })
+ }
+ models, _, err := normalizeAgentAccountModels(account, requested, "", true)
+ if err != nil {
+ return nil, err
+ }
+ return models, nil
+}
+
+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 {
+ if strings.TrimSpace(item.ID) == "" {
+ continue
+ }
+ seen[strings.TrimSpace(item.ID)] = struct{}{}
+ }
+ for _, item := range meta.Models {
+ if _, ok := seen[strings.TrimSpace(item.ID)]; ok {
+ continue
+ }
+ requested = append(requested, dto.AgentAccountModel{
+ ID: item.ID,
+ Name: item.Name,
+ ContextWindow: item.ContextWindow,
+ MaxTokens: item.MaxTokens,
+ Reasoning: item.Reasoning,
+ Input: append([]string(nil), item.Input...),
+ })
+ }
+ if len(requested) == len(existing) {
+ return append([]dto.AgentAccountModel(nil), existing...), nil
+ }
+ normalized, _, err := normalizeAgentAccountModels(account, requested, "", true)
+ if err != nil {
+ return nil, err
+ }
+ return normalized, nil
+}
+
+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 {
+ inputs := []string{}
+ if strings.TrimSpace(row.Input) != "" {
+ _ = json.Unmarshal([]byte(row.Input), &inputs)
+ }
+ result = append(result, dto.AgentAccountModel{
+ RecordID: row.ID,
+ ID: strings.TrimSpace(row.Model),
+ Name: strings.TrimSpace(row.Name),
+ ContextWindow: row.ContextWindow,
+ MaxTokens: row.MaxTokens,
+ Reasoning: row.Reasoning,
+ Input: sanitizeAgentAccountModelInputs(inputs),
+ })
+ }
+ 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 {
+ inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
+ if err != nil {
+ return err
+ }
+ record := &model.AgentAccountModel{
+ AccountID: accountID,
+ Model: strings.TrimSpace(item.ID),
+ Name: strings.TrimSpace(item.Name),
+ ContextWindow: item.ContextWindow,
+ MaxTokens: item.MaxTokens,
+ Reasoning: item.Reasoning,
+ Input: string(inputPayload),
+ SortOrder: index + 1,
+ }
+ if err := tx.Create(record).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccountModel) error {
+ return global.DB.Transaction(func(tx *gorm.DB) error {
+ return replacePersistedAgentAccountModelsWithTx(tx, accountID, models)
+ })
+}
+
+func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel, defaultModel string, allowFallbackDefault bool) ([]dto.AgentAccountModel, string, error) {
+ requested := append([]dto.AgentAccountModel(nil), models...)
+ if len(requested) == 0 {
+ if strings.TrimSpace(defaultModel) != "" {
+ requested = []dto.AgentAccountModel{{ID: defaultModel}}
+ } else {
+ requested = buildLegacyAgentAccountModels(account)
+ }
+ }
+ 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")
+ }
+
+ resolvedDefault := strings.TrimSpace(defaultModel)
+ if resolvedDefault != "" {
+ defaultItem, err := normalizeAgentAccountModel(account, dto.AgentAccountModel{ID: resolvedDefault})
+ if err == nil {
+ resolvedDefault = defaultItem.ID
+ }
+ }
+ if resolvedDefault == "" && allowFallbackDefault {
+ resolvedDefault = normalized[0].ID
+ }
+ if _, ok := findAgentAccountModelForProvider(account.Provider, normalized, resolvedDefault); !ok {
+ if allowFallbackDefault {
+ resolvedDefault = normalized[0].ID
+ } else {
+ return nil, "", buserr.New("ErrAgentModelNotInAccount")
+ }
+ }
+ return normalized, resolvedDefault, 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")
+ }
+ primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, model.MaxTokens, model.ContextWindow)
+ if err != nil {
+ return dto.AgentAccountModel{}, err
+ }
+ name := strings.TrimSpace(model.Name)
+ if name == "" {
+ name = strings.TrimSpace(inferredEntry.Name)
+ }
+ reasoning := model.Reasoning
+ if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 {
+ reasoning = inferredEntry.Reasoning
+ }
+ inputs := sanitizeAgentAccountModelInputs(model.Input)
+ if len(inputs) == 0 {
+ inputs = sanitizeAgentAccountModelInputs(inferredEntry.Input)
+ }
+ contextWindow := model.ContextWindow
+ if contextWindow <= 0 {
+ contextWindow = inferredEntry.ContextWindow
+ }
+ maxTokens := model.MaxTokens
+ if maxTokens <= 0 {
+ maxTokens = inferredEntry.MaxTokens
+ }
+ return dto.AgentAccountModel{
+ ID: normalizeAgentAccountModelID(account.Provider, primaryModel, modelID),
+ Name: name,
+ ContextWindow: contextWindow,
+ MaxTokens: maxTokens,
+ Reasoning: reasoning,
+ Input: inputs,
+ }, nil
+}
+
+func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) string {
+ switch provider {
+ case "custom", "vllm":
+ target := requestedID
+ if strings.TrimSpace(target) == "" {
+ target = primaryModel
+ }
+ return normalizeCustomModel(target)
+ case "ollama":
+ target := strings.TrimSpace(primaryModel)
+ if strings.HasPrefix(target, "ollama/") {
+ return target
+ }
+ target = strings.TrimSpace(requestedID)
+ if strings.HasPrefix(target, "ollama/") {
+ return target
+ }
+ target = strings.TrimLeft(strings.TrimSpace(target), "/")
+ if target == "" {
+ target = strings.TrimLeft(strings.TrimSpace(primaryModel), "/")
+ }
+ if target == "" {
+ return ""
+ }
+ return "ollama/" + target
+ default:
+ target := strings.TrimSpace(requestedID)
+ if target == "" {
+ target = strings.TrimSpace(primaryModel)
+ }
+ if target == "" {
+ return ""
+ }
+ prefix := poolModelPrefix(provider)
+ if strings.Contains(target, "/") {
+ parts := strings.SplitN(target, "/", 2)
+ targetPrefix := parts[0]
+ targetModel := strings.TrimSpace(parts[1])
+ if targetModel == "" {
+ return strings.TrimSpace(target)
+ }
+ for _, item := range supportedProviderModelPrefixes(provider) {
+ if item == targetPrefix {
+ if prefix != "" {
+ return prefix + "/" + targetModel
+ }
+ return strings.TrimSpace(target)
+ }
+ }
+ return strings.TrimSpace(target)
+ }
+ target = strings.TrimLeft(strings.TrimSpace(target), "/")
+ if prefix == "" {
+ return target
+ }
+ return prefix + "/" + target
+ }
+}
+
+func buildLegacyAgentAccountModels(account *model.AgentAccount) []dto.AgentAccountModel {
+ modelIDs := make([]string, 0, 4)
+ seen := make(map[string]struct{}, 4)
+ appendModel := func(value string) {
+ target := strings.TrimSpace(value)
+ if target == "" {
+ return
+ }
+ if _, ok := seen[target]; ok {
+ return
+ }
+ seen[target] = struct{}{}
+ modelIDs = append(modelIDs, target)
+ }
+ if account.ID > 0 {
+ if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil {
+ for _, agent := range agents {
+ appendModel(agent.Model)
+ }
+ }
+ }
+ if definitions, ok := providerDefinitions()[account.Provider]; ok && len(definitions.Models) > 0 {
+ for _, item := range definitions.Models {
+ appendModel(item.ID)
+ }
+ }
+ models := make([]dto.AgentAccountModel, 0, len(modelIDs))
+ for _, modelID := range modelIDs {
+ models = append(models, dto.AgentAccountModel{ID: modelID})
+ }
+ return models
+}
+
+func sanitizeAgentAccountModelInputs(values []string) []string {
+ result := make([]string, 0, len(values))
+ seen := make(map[string]struct{}, len(values))
+ for _, value := range values {
+ normalized := value
+ if normalized != "text" && normalized != "image" {
+ continue
+ }
+ if _, ok := seen[normalized]; ok {
+ continue
+ }
+ seen[normalized] = struct{}{}
+ result = append(result, normalized)
+ }
+ if len(result) == 0 {
+ return []string{"text"}
+ }
+ return result
+}
+
+func requiresInitialAgentAccountModels(provider string) bool {
+ switch provider {
+ case "custom", "vllm", "ollama":
+ return true
+ default:
+ return false
+ }
+}
+
+func normalizeComparableProviderModelID(provider, modelID string) string {
+ target := strings.TrimSpace(modelID)
+ if target == "" {
+ return ""
+ }
+ if !strings.Contains(target, "/") {
+ return target
+ }
+ parts := strings.SplitN(target, "/", 2)
+ prefix := parts[0]
+ model := strings.TrimSpace(parts[1])
+ if model == "" {
+ return target
+ }
+ for _, item := range supportedProviderModelPrefixes(provider) {
+ if item == prefix {
+ return model
+ }
+ }
+ return target
+}
+
+func sameProviderModelID(provider, left, right string) bool {
+ leftTrimmed := strings.TrimSpace(left)
+ rightTrimmed := strings.TrimSpace(right)
+ if leftTrimmed == rightTrimmed {
+ return true
+ }
+ leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
+ rightComparable := normalizeComparableProviderModelID(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 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 strings.TrimSpace(agent.Model) == "" {
+ continue
+ }
+ 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 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 mapToModelsConfig(value map[string]interface{}) (*modelsConfig, error) {
+ payload, err := json.Marshal(value)
+ if err != nil {
+ return nil, err
+ }
+ result := &modelsConfig{}
+ if err := json.Unmarshal(payload, result); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func providerEnvKey(provider string) string {
+ return providercatalog.EnvKey(provider)
+}
+
+type providerDefinition struct {
+ Sort uint
+ DisplayName string
+ BaseURL string
+ Models []dto.ProviderModelInfo
+}
+
+func providerDefinitions() map[string]providerDefinition {
+ definitions := map[string]providerDefinition{}
+ for key, meta := range providercatalog.All() {
+ models := make([]dto.ProviderModelInfo, 0, len(meta.Models))
+ for _, m := range meta.Models {
+ models = append(models, dto.ProviderModelInfo{
+ ID: m.ID,
+ Name: m.Name,
+ ContextWindow: m.ContextWindow,
+ MaxTokens: m.MaxTokens,
+ Reasoning: m.Reasoning,
+ Input: append([]string(nil), m.Input...),
+ })
+ }
+ definitions[key] = providerDefinition{
+ Sort: meta.Sort,
+ DisplayName: meta.DisplayName,
+ BaseURL: meta.DefaultBaseURL,
+ Models: models,
+ }
+ }
+ return definitions
+}
+
+func providerDefaultBaseURL(provider string) (string, bool) {
+ return providercatalog.DefaultBaseURL(provider)
+}
+
+func fixedProviderBaseURL(provider string) (string, bool) {
+ switch provider {
+ case "bailian-coding-plan":
+ return providerDefaultBaseURL(provider)
+ case "ark-coding-plan":
+ return providerDefaultBaseURL(provider)
+ default:
+ return "", false
+ }
+}
+
+func providerDisplayName(provider string) string {
+ return providercatalog.DisplayName(provider)
+}
+
+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
+}
+
+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 normalizeCustomModel(modelName string) string {
+ trim := strings.TrimSpace(modelName)
+ trim = strings.TrimLeft(trim, "/")
+ if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
+ if strings.EqualFold(parts[0], "custom") {
+ return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
+ }
+ }
+ return trim
+}
+
+func normalizeAgentType(agentType string) string {
+ trim := agentType
+ if trim == "" {
+ return constant.AppOpenclaw
+ }
+ return trim
+}
+
+func modelMatchesProvider(provider, modelName string) bool {
+ target := strings.TrimSpace(modelName)
+ for _, prefix := range supportedProviderModelPrefixes(provider) {
+ if prefix != "" && strings.HasPrefix(target, prefix+"/") {
+ return true
+ }
+ }
+ return false
+}
+
+func runtimeProviderModelPrefix(provider string) string {
+ switch provider {
+ case "gemini":
+ return "google"
+ case "minimax":
+ return "minimax-portal"
+ case "kimi":
+ return "moonshot"
+ default:
+ return provider
+ }
+}
+
+func poolModelPrefix(provider string) string {
+ target := provider
+ if definitions, ok := providerDefinitions()[target]; ok && len(definitions.Models) > 0 {
+ parts := strings.SplitN(strings.TrimSpace(definitions.Models[0].ID), "/", 2)
+ if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
+ return parts[0]
+ }
+ }
+ return target
+}
+
+func supportedProviderModelPrefixes(provider string) []string {
+ values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
+ result := make([]string, 0, len(values))
+ seen := make(map[string]struct{}, len(values))
+ for _, value := range values {
+ target := value
+ if target == "" {
+ continue
+ }
+ if _, ok := seen[target]; ok {
+ continue
+ }
+ seen[target] = struct{}{}
+ result = append(result, target)
+ }
+ return result
+}
+
+func normalizeAPIType(apiType string) string {
+ trim := apiType
+ if trim == "" {
+ return "openai-completions"
+ }
+ return trim
+}
+
+func isSupportedAPIType(apiType string) bool {
+ switch normalizeAPIType(apiType) {
+ case "openai-completions", "openai-responses", "anthropic-messages":
+ return true
+ default:
+ return false
+ }
+}
+
+func isSupportedOllamaAPIType(apiType string) bool {
+ switch normalizeAPIType(apiType) {
+ case "openai-completions", "openai-responses":
+ return true
+ default:
+ return false
+ }
+}
+
+func resolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
+ resolvedAPI := normalizeAPIType(apiType)
+ if provider == "ollama" && !isSupportedOllamaAPIType(resolvedAPI) {
+ resolvedAPI = "openai-responses"
+ }
+ resolvedMaxTokens := maxTokens
+ resolvedContextWindow := contextWindow
+ if resolvedMaxTokens <= 0 {
+ switch provider {
+ case "deepseek":
+ resolvedMaxTokens = 8192
+ case "zai":
+ resolvedMaxTokens = 131072
+ case "openrouter":
+ resolvedMaxTokens = 8192
+ case "minimax", "kimi-coding", "custom":
+ resolvedMaxTokens = 8192
+ default:
+ resolvedMaxTokens = 8192
+ }
+ }
+ if resolvedContextWindow <= 0 {
+ switch provider {
+ case "deepseek":
+ resolvedContextWindow = 128000
+ case "zai":
+ resolvedContextWindow = 204800
+ case "openrouter":
+ resolvedContextWindow = 128000
+ case "minimax", "kimi-coding":
+ resolvedContextWindow = 200000
+ case "custom", "vllm":
+ resolvedContextWindow = 128000
+ default:
+ resolvedContextWindow = 256000
+ }
+ }
+ return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
+}
+
+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" {
+ return
+ }
+ provider = provider
+ if 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)
+}
diff --git a/agent/app/service/app.go b/agent/app/service/app.go
index 4f0e32fb0..cc8d77932 100644
--- a/agent/app/service/app.go
+++ b/agent/app/service/app.go
@@ -57,6 +57,10 @@ type IAppService interface {
GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
}
+type appInstallHooks struct {
+ AfterCopyData func(appInstall *model.AppInstall) error
+}
+
func NewIAppService() IAppService {
return &AppService{}
}
@@ -340,6 +344,10 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
}
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
+ return a.installWithHooks(req, executeScript, nil)
+}
+
+func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript bool, hooks *appInstallHooks) (appInstall *model.AppInstall, err error) {
if err = docker.CreateDefaultDockerNetwork(); err != nil {
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
return
@@ -537,6 +545,11 @@ func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (a
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
+ if hooks != nil && hooks.AfterCopyData != nil {
+ if err = hooks.AfterCopyData(appInstall); err != nil {
+ return err
+ }
+ }
if executeScript {
if err = runScript(t, appInstall, "init"); err != nil {
return err
diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml
index 4d29312b3..519802aa3 100644
--- a/agent/i18n/lang/en.yaml
+++ b/agent/i18n/lang/en.yaml
@@ -44,7 +44,8 @@ ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified'
-ErrAgentProviderMismatch: 'Agent provider mismatch'
+ErrAgentAccountModelsRequired: 'Please configure at least one model'
+ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation'
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
ErrAgentBaseURLRequired: 'Base URL is required'
diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml
index ce03ef564..972bbb02c 100644
--- a/agent/i18n/lang/es-ES.yaml
+++ b/agent/i18n/lang/es-ES.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
-ErrAgentProviderMismatch: 'Proveedor de agente no coincide'
+ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
+ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta'
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base requerida'
diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml
index b43bd405f..3738bb6dd 100644
--- a/agent/i18n/lang/ja.yaml
+++ b/agent/i18n/lang/ja.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です'
-ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
+ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
+ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
ErrAgentBaseURLRequired: 'ベースURLが必要です'
diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml
index 9a9d4336c..8ca73c112 100644
--- a/agent/i18n/lang/ko.yaml
+++ b/agent/i18n/lang/ko.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
-ErrAgentProviderMismatch: '에이전트 공급자 불일치'
+ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
+ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
ErrAgentBaseURLRequired: '기본 URL 필요'
diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml
index 09f1a1fb5..2cbeeb005 100644
--- a/agent/i18n/lang/ms.yaml
+++ b/agent/i18n/lang/ms.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
-ErrAgentProviderMismatch: 'Penyedia ejen tidak sepadan'
+ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
+ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun'
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
ErrAgentBaseURLRequired: 'URL asas diperlukan'
diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml
index 13fa48cbe..12aea75d7 100644
--- a/agent/i18n/lang/pt-BR.yaml
+++ b/agent/i18n/lang/pt-BR.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
-ErrAgentProviderMismatch: 'Provedor de agente diferente'
+ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
+ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta'
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base obrigatória'
diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml
index 95bb6814d..66e516dc0 100644
--- a/agent/i18n/lang/ru.yaml
+++ b/agent/i18n/lang/ru.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступн
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
-ErrAgentProviderMismatch: 'Провайдер не совпадает'
+ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
+ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
ErrAgentBaseURLRequired: 'Нужен базовый URL'
diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml
index 7e460c93e..d65dec112 100644
--- a/agent/i18n/lang/tr.yaml
+++ b/agent/i18n/lang/tr.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
-ErrAgentProviderMismatch: 'Ajans sağlayıcısı eşleşmiyor'
+ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
+ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir'
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}'
ErrAgentBaseURLRequired: 'Temel URL gerekli'
diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml
index 2bc157695..144e80a82 100644
--- a/agent/i18n/lang/zh-Hant.yaml
+++ b/agent/i18n/lang/zh-Hant.yaml
@@ -39,7 +39,8 @@ ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }}
ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
-ErrAgentProviderMismatch: '帳號提供商不符,請重試'
+ErrAgentAccountModelsRequired: '請至少配置一個模型'
+ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml
index 57e2dc4d4..8b816b6e7 100644
--- a/agent/i18n/lang/zh.yaml
+++ b/agent/i18n/lang/zh.yaml
@@ -44,7 +44,8 @@ ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}"
ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证"
-ErrAgentProviderMismatch: "账号提供商不匹配"
+ErrAgentAccountModelsRequired: "请至少配置一个模型"
+ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型"
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
ErrAgentBaseURLRequired: "Base URL 不能为空"
diff --git a/agent/init/migration/migrations/utils/agent_account_model_pool.go b/agent/init/migration/migrations/utils/agent_account_model_pool.go
index 1904bb584..a5f88f93f 100644
--- a/agent/init/migration/migrations/utils/agent_account_model_pool.go
+++ b/agent/init/migration/migrations/utils/agent_account_model_pool.go
@@ -11,9 +11,15 @@ import (
"gorm.io/gorm"
)
+type legacyAgentAccountModelPoolSource struct {
+ model.AgentAccount
+ LegacyModel string `gorm:"column:model"`
+ LegacyModels string `gorm:"column:models"`
+}
+
func MigrateAgentAccountModelPool(tx *gorm.DB) error {
- var accounts []model.AgentAccount
- if err := tx.Find(&accounts).Error; err != nil {
+ var accounts []legacyAgentAccountModelPoolSource
+ if err := tx.Table(model.AgentAccount{}.TableName()).Find(&accounts).Error; err != nil {
return err
}
for _, account := range accounts {
@@ -54,13 +60,14 @@ func MigrateAgentAccountModelPool(tx *gorm.DB) error {
return nil
}
-func buildMigratedAgentAccountModels(tx *gorm.DB, account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
+func buildMigratedAgentAccountModels(tx *gorm.DB, account *legacyAgentAccountModelPoolSource) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, nil
}
+ baseAccount := account.AgentAccount
requested := make([]dto.AgentAccountModel, 0)
- if strings.TrimSpace(account.Models) != "" {
- if err := json.Unmarshal([]byte(account.Models), &requested); err != nil {
+ if strings.TrimSpace(account.LegacyModels) != "" {
+ if err := json.Unmarshal([]byte(account.LegacyModels), &requested); err != nil {
return nil, err
}
}
@@ -83,7 +90,7 @@ func buildMigratedAgentAccountModels(tx *gorm.DB, account *model.AgentAccount) (
seen[target] = struct{}{}
requested = append(requested, dto.AgentAccountModel{ID: target})
}
- appendModel(account.Model)
+ appendModel(account.LegacyModel)
if account.ID > 0 {
var agents []model.Agent
if err := tx.Where("account_id = ?", account.ID).Find(&agents).Error; err != nil {
@@ -93,7 +100,7 @@ func buildMigratedAgentAccountModels(tx *gorm.DB, account *model.AgentAccount) (
appendModel(agent.Model)
}
}
- models, err := service.MergeCatalogAgentAccountModelsForMigration(account, requested)
+ models, err := service.MergeCatalogAgentAccountModelsForMigration(&baseAccount, requested)
if err != nil {
if strings.TrimSpace(err.Error()) == "model is required" {
return nil, nil
diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts
index 355338fa3..d2f0d606b 100644
--- a/frontend/src/api/interface/ai.ts
+++ b/frontend/src/api/interface/ai.ts
@@ -243,14 +243,8 @@ export namespace AI {
bridgePort?: number;
allowedOrigins?: string[];
agentType: 'openclaw' | 'copaw';
- provider?: string;
model?: string;
- apiType?: string;
- maxTokens?: number;
- contextWindow?: number;
accountId?: number;
- apiKey?: string;
- baseURL?: string;
token?: string;
taskID: string;
advanced: boolean;
@@ -361,8 +355,7 @@ export namespace AI {
rememberApiKey: boolean;
baseURL: string;
apiType: string;
- maxTokens: number;
- contextWindow: number;
+ models?: AgentAccountModel[];
remark: string;
}
@@ -373,8 +366,6 @@ export namespace AI {
rememberApiKey: boolean;
baseURL: string;
apiType: string;
- maxTokens: number;
- contextWindow: number;
remark: string;
syncAgents: boolean;
}
@@ -394,11 +385,8 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseUrl: string;
- model: string;
models: AgentAccountModel[];
apiType: string;
- maxTokens: number;
- contextWindow: number;
verified: boolean;
remark: string;
createdAt: string;
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 8e66db8cc..6d464197e 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -692,8 +692,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts
index fed373ff0..bb381a49e 100644
--- a/frontend/src/lang/modules/es-es.ts
+++ b/frontend/src/lang/modules/es-es.ts
@@ -700,8 +700,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index 372c2ff63..9b4ebec2a 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -693,8 +693,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index f4a399d4f..2f259c72c 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -685,8 +685,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index dc79e0235..fe1d73737 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -700,8 +700,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index d40361cb5..c548dbd6c 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -695,8 +695,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 3432d7902..1287edaca 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -692,8 +692,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts
index 7e70532ed..b27f75f0f 100644
--- a/frontend/src/lang/modules/tr.ts
+++ b/frontend/src/lang/modules/tr.ts
@@ -696,8 +696,6 @@ const message = {
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
- accountCreateHelper:
- 'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 9800b1eac..8838e7f24 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -660,7 +660,6 @@ const message = {
accountModelsHelper: '配置該帳號可提供給 OpenClaw 使用與切換的模型列表',
accountModelsRequired: '請至少配置一個模型',
accountModelsDuplicate: '模型池中存在重複模型,請檢查後重試',
- accountCreateHelper: '建立模型帳號後可繼續修改模型池;如果供應商 catalog 已提供 Models,將自動匯入模型池',
modelPool: '模型池',
modelPoolHelper: '在此維護該模型帳號的模型池,建立智能體與 OpenClaw 模型切換都會使用這些模型',
modelInputTypes: '輸入類型',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index b835dd327..4e8d771b1 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -659,7 +659,6 @@ const message = {
accountModelsHelper: '配置账号可提供给 OpenClaw 使用和切换的模型列表',
accountModelsRequired: '请至少配置一个模型',
accountModelsDuplicate: '模型池中存在重复模型,请检查后重试',
- accountCreateHelper: '创建模型账号后可继续修改模型池;如果供应商 catalog 已提供 Models,将自动导入模型池',
modelPool: '模型池',
modelPoolHelper: '在这里维护该模型账号的模型池,智能体创建和 OpenClaw 模型切换都会使用这些模型',
modelInputTypes: '输入类型',
diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue
index 3e5217457..41c53c7ce 100644
--- a/frontend/src/views/ai/agents/agent/add/index.vue
+++ b/frontend/src/views/ai/agents/agent/add/index.vue
@@ -146,10 +146,6 @@ const form = reactive({
provider: 'deepseek',
accountId: undefined as unknown as number,
model: '',
- apiType: 'openai-completions',
- maxTokens: 8192,
- contextWindow: 128000,
- apiKey: '',
baseURL: '',
token: '',
advanced: true,
@@ -300,7 +296,6 @@ const loadAccounts = async () => {
handleAccountChange();
} else {
form.accountId = undefined as unknown as number;
- form.apiKey = '';
form.baseURL = '';
}
};
@@ -310,7 +305,6 @@ const handleProviderChange = () => {
return;
}
form.model = '';
- form.apiKey = '';
form.baseURL = '';
form.accountId = undefined as unknown as number;
loadAccounts();
@@ -324,9 +318,7 @@ const handleAgentTypeChange = async () => {
form.model = '';
form.provider = 'deepseek';
form.accountId = undefined as unknown as number;
- form.apiKey = '';
form.baseURL = '';
- form.apiType = 'openai-completions';
if (form.agentType === 'openclaw') {
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
@@ -349,10 +341,6 @@ const handleAccountChange = () => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (selected) {
form.baseURL = selected.baseUrl || '';
- form.apiKey = selected.apiKey || '';
- form.apiType = selected.apiType || 'openai-completions';
- form.maxTokens = selected.maxTokens || 8192;
- form.contextWindow = selected.contextWindow || 128000;
if (!selected.models?.some((item) => item.id === form.model)) {
form.model = selected.models?.[0]?.id || '';
}
@@ -394,14 +382,8 @@ const submit = async () => {
webUIPort: form.webUIPort,
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,
- apiType: form.agentType === 'openclaw' ? form.apiType : undefined,
- maxTokens: form.agentType === 'openclaw' ? form.maxTokens : undefined,
- contextWindow: form.agentType === 'openclaw' ? form.contextWindow : undefined,
accountId: form.agentType === 'openclaw' ? form.accountId : undefined,
- apiKey: form.agentType === 'openclaw' ? form.apiKey : undefined,
- baseURL: form.agentType === 'openclaw' ? form.baseURL : undefined,
token: form.agentType === 'openclaw' ? form.token : undefined,
taskID: taskID,
advanced: form.advanced,
diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/agents/model/add/index.vue
index ddaeb88a8..9ab29380c 100644
--- a/frontend/src/views/ai/agents/model/add/index.vue
+++ b/frontend/src/views/ai/agents/model/add/index.vue
@@ -26,13 +26,6 @@
-
@@ -44,12 +37,30 @@
/>
-
-
-
-
-
-
+
+ {{ $t('aiTools.agents.accountModels') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Text
+ Image
+
+
+
+
+
+
@@ -72,6 +83,7 @@