Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions agent/app/dto/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ type AgentCreateReq struct {
Name string `json:"name" validate:"required"`
AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required"`
BridgePort int `json:"bridgePort" validate:"required"`
Provider string `json:"provider" validate:"required"`
Model string `json:"model" validate:"required"`
BridgePort int `json:"bridgePort"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reinstate bridge port validation for OpenClaw create requests

Dropping validate:"required" from bridgePort allows API clients to create an OpenClaw agent without a bridge port (0), because Create now only calls checkPortExist(req.BridgePort) and checkPortExist(0) passes. In that case we still inject PANEL_APP_PORT_BRIDGE=0 into install params, which can leave the bridge endpoint unusable or mapped to an unexpected ephemeral port for non-UI callers that bypass the frontend validator.

Useful? React with 👍 / 👎.

AgentType string `json:"agentType"`
Provider string `json:"provider"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Expand All @@ -33,6 +34,7 @@ type AgentCreateReq struct {
type AgentItem struct {
ID uint `json:"id"`
Name string `json:"name"`
AgentType string `json:"agentType"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Model string `json:"model"`
Expand Down
1 change: 1 addition & 0 deletions agent/app/model/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package model
type Agent struct {
BaseModel
Name string `json:"name" gorm:"not null;unique"`
AgentType string `json:"agentType" gorm:"default:openclaw"`
Provider string `json:"provider"`
Model string `json:"model"`
APIType string `json:"apiType"`
Expand Down
2 changes: 1 addition & 1 deletion agent/app/provider/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ var catalog = map[string]Meta{
"bailian-coding-plan": {
Key: "bailian-coding-plan",
DisplayName: "阿里云百炼 Coding Plan",
Sort: 105,
Sort: 11,
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
EnvKey: "QWEN_API_KEY",
Enabled: true,
Expand Down
191 changes: 130 additions & 61 deletions agent/app/service/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,58 +63,32 @@ const (
)

func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
provider := strings.ToLower(strings.TrimSpace(req.Provider))
if !isSupportedAgentProvider(provider) {
return nil, buserr.New("ErrAgentProviderNotSupported")
agentType := normalizeAgentType(req.AgentType)
if !isSupportedAgentType(agentType) {
return nil, fmt.Errorf("agent type is invalid")
}
if req.AccountID == 0 {
return nil, buserr.New("ErrAgentAccountRequired")
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil {
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
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))
baseURL := strings.TrimSpace(account.BaseURL)
if baseURL == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
baseURL = defaultURL
if agentType == constant.AppOpenclaw {
if req.BridgePort <= 0 {
return nil, fmt.Errorf("bridge port is required")
}
}
if provider == "ollama" && baseURL == "" {
return nil, buserr.New("ErrAgentBaseURLRequired")
}
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
return nil, buserr.New("ErrAgentApiKeyRequired")
}
apiType, maxTokens, contextWindow := resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow)
runtimeModel := strings.TrimSpace(req.Model)
if provider == "custom" {
primaryID := customPrimaryModelID(req.Model)
if primaryID == "" {
primaryID = normalizeCustomModel(req.Model)
if err := checkPortExist(req.BridgePort); err != nil {
return nil, err
}
runtimeModel = "custom/" + primaryID
}
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
if err := checkPortExist(req.BridgePort); err != nil {
return nil, err
}
if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 {
return nil, buserr.New("ErrNameIsExist")
}
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist")
}
app, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenclaw))
appKey := constant.AppOpenclaw
if agentType == constant.AppCopaw {
appKey = constant.AppCopaw
}
app, err := appRepo.GetFirst(appRepo.WithKey(appKey))
if err != nil || app.ID == 0 {
return nil, buserr.New("ErrRecordNotFound")
}
Expand All @@ -123,24 +97,86 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
return nil, buserr.New("ErrRecordNotFound")
}

token := strings.TrimSpace(req.Token)
if token == "" {
token = generateToken()
provider := ""
baseURL := ""
apiType := ""
maxTokens := 0
contextWindow := 0
apiKey := ""
runtimeModel := ""
accountID := uint(0)
token := ""
configPath := ""
storedModel := ""

if agentType == constant.AppOpenclaw {
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))
if err != nil {
return nil, err
}
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))
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")
}
apiType, maxTokens, contextWindow = resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow)
runtimeModel = strings.TrimSpace(req.Model)
if runtimeModel == "" {
return nil, buserr.New("ErrAgentProviderMismatch")
}
if provider == "custom" {
primaryID := customPrimaryModelID(req.Model)
if primaryID == "" {
primaryID = normalizeCustomModel(req.Model)
}
runtimeModel = "custom/" + primaryID
}
storedModel = req.Model
apiKey = account.APIKey
accountID = account.ID
token = strings.TrimSpace(req.Token)
if token == "" {
token = generateToken()
}
}

params := map[string]interface{}{
"PROVIDER": provider,
"MODEL": runtimeModel,
"API_TYPE": apiType,
"MAX_TOKENS": maxTokens,
"CONTEXT_WINDOW": contextWindow,
"BASE_URL": baseURL,
"API_KEY": account.APIKey,
"OPENCLAW_GATEWAY_TOKEN": token,
"PANEL_APP_PORT_HTTP": req.WebUIPort,
"PANEL_APP_PORT_BRIDGE": req.BridgePort,
constant.CPUS: "0",
constant.MemoryLimit: "0",
constant.HostIP: "",
"PANEL_APP_PORT_HTTP": req.WebUIPort,
constant.CPUS: "0",
constant.MemoryLimit: "0",
constant.HostIP: "",
}
if agentType == constant.AppOpenclaw {
params["PROVIDER"] = provider
params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType
params["MAX_TOKENS"] = maxTokens
params["CONTEXT_WINDOW"] = contextWindow
params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token
params["PANEL_APP_PORT_BRIDGE"] = req.BridgePort
}

if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
Expand Down Expand Up @@ -169,27 +205,32 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if err != nil {
return nil, err
}
configPath := path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
if agentType == constant.AppOpenclaw {
configPath = path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
}
agent := &model.Agent{
Name: req.Name,
AgentType: agentType,
Provider: provider,
Model: req.Model,
Model: storedModel,
APIType: apiType,
MaxTokens: maxTokens,
ContextWindow: contextWindow,
BaseURL: baseURL,
APIKey: account.APIKey,
APIKey: apiKey,
Token: token,
Status: appInstall.Status,
Message: appInstall.Message,
AppInstallID: appInstall.ID,
AccountID: account.ID,
AccountID: accountID,
ConfigPath: configPath,
}
if err := agentRepo.Create(agent); err != nil {
return nil, err
}
go a.writeConfigWithRetry(appInstall, provider, req.Model, apiType, maxTokens, contextWindow, baseURL, account.APIKey, token, agent.ID)
if agentType == constant.AppOpenclaw {
go a.writeConfigWithRetry(appInstall, provider, req.Model, apiType, maxTokens, contextWindow, baseURL, apiKey, token, agent.ID)
}

item := buildAgentItem(agent, appInstall, nil)
return &item, nil
Expand Down Expand Up @@ -244,6 +285,9 @@ func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
if err != nil {
return err
}
if normalizeAgentType(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))
Expand Down Expand Up @@ -284,6 +328,9 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if err != nil {
return err
}
if normalizeAgentType(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
Expand Down Expand Up @@ -1119,9 +1166,14 @@ func verifyMinimax(baseURL, apiKey string) error {
}

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,
Expand Down Expand Up @@ -1807,6 +1859,23 @@ func customPrimaryModelID(modelName string) string {
return ""
}

func normalizeAgentType(agentType string) string {
trim := strings.ToLower(strings.TrimSpace(agentType))
if trim == "" {
return constant.AppOpenclaw
}
return trim
}

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 == "" {
Expand Down
2 changes: 1 addition & 1 deletion agent/app/service/app_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return err
}
appKey := install.App.Key
if appKey == constant.AppOpenclaw {
if appKey == constant.AppOpenclaw || appKey == constant.AppCopaw {
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
}

Expand Down
1 change: 1 addition & 0 deletions agent/constant/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const (

AppOpenresty = "openresty"
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
Expand Down
1 change: 1 addition & 0 deletions agent/init/migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func InitAgentDB() {
migrations.AddAppInstallSortOrder,
migrations.AddAgentAccountRememberAPIKey,
migrations.AddEditionSetting,
migrations.AddAgentTypeForAgents,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
Expand Down
17 changes: 17 additions & 0 deletions agent/init/migration/migrations/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -993,3 +993,20 @@ var AddEditionSetting = &gormigrate.Migration{
return nil
},
}

var AddAgentTypeForAgents = &gormigrate.Migration{
ID: "20260302-add-agent-type-for-agents",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Agent{}); err != nil {
return err
}
if err := tx.Model(&model.Agent{}).Where("agent_type = '' OR agent_type IS NULL").Update("agent_type", constant.AppOpenclaw).Error; err != nil {
return err
}
return tx.Exec(
"UPDATE agents SET agent_type = ? WHERE app_install_id IN (SELECT ai.id FROM app_installs ai JOIN apps a ON ai.app_id = a.id WHERE a.key = ?)",
constant.AppCopaw,
constant.AppCopaw,
).Error
},
}
22 changes: 12 additions & 10 deletions frontend/src/api/interface/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,17 @@ export namespace AI {
name: string;
appVersion: string;
webUIPort: number;
bridgePort: number;
provider: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
accountId: number;
apiKey: string;
baseURL: string;
token: string;
bridgePort?: number;
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;
containerName: string;
Expand All @@ -267,6 +268,7 @@ export namespace AI {
export interface AgentItem {
id: number;
name: string;
agentType: 'openclaw' | 'copaw';
provider: string;
providerName: string;
model: string;
Expand Down
Loading
Loading