diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index bafbaf14f..a5d472d49 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -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"` + AgentType string `json:"agentType"` + Provider string `json:"provider"` + Model string `json:"model"` APIType string `json:"apiType"` MaxTokens int `json:"maxTokens"` ContextWindow int `json:"contextWindow"` @@ -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"` diff --git a/agent/app/model/agent.go b/agent/app/model/agent.go index f8301857f..fa382e708 100644 --- a/agent/app/model/agent.go +++ b/agent/app/model/agent.go @@ -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"` diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index 6776febe6..df7867177 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -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, diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 16c1186e7..97a0759d3 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -63,50 +63,20 @@ 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") - } - 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 provider == "custom" { - primaryID := customPrimaryModelID(req.Model) - if primaryID == "" { - primaryID = normalizeCustomModel(req.Model) - } - runtimeModel = "custom/" + primaryID + agentType := normalizeAgentType(req.AgentType) + if !isSupportedAgentType(agentType) { + return nil, fmt.Errorf("agent type is invalid") } if err := checkPortExist(req.WebUIPort); err != nil { return nil, err } - if err := checkPortExist(req.BridgePort); err != nil { - return nil, err + if agentType == constant.AppOpenclaw { + if req.BridgePort <= 0 { + return nil, fmt.Errorf("bridge port is required") + } + if err := checkPortExist(req.BridgePort); err != nil { + return nil, err + } } if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 { return nil, buserr.New("ErrNameIsExist") @@ -114,7 +84,11 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { 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") } @@ -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) == "" { @@ -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 @@ -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)) @@ -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 @@ -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, @@ -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 == "" { diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index f9144bc8a..df8420c94 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -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) } diff --git a/agent/constant/app.go b/agent/constant/app.go index 74e766125..051d4a087 100644 --- a/agent/constant/app.go +++ b/agent/constant/app.go @@ -8,6 +8,7 @@ const ( AppOpenresty = "openresty" AppOpenclaw = "openclaw" + AppCopaw = "copaw" AppMysql = "mysql" AppMariaDB = "mariadb" AppPostgresql = "postgresql" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 91ab21e5a..6d9c6dba0 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -69,6 +69,7 @@ func InitAgentDB() { migrations.AddAppInstallSortOrder, migrations.AddAgentAccountRememberAPIKey, migrations.AddEditionSetting, + migrations.AddAgentTypeForAgents, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 25a5b8dc5..b2fc7f7f9 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -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 + }, +} diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index ca048068e..a91e70d65 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -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; @@ -267,6 +268,7 @@ export namespace AI { export interface AgentItem { id: number; name: string; + agentType: 'openclaw' | 'copaw'; provider: string; providerName: string; model: string; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index eab64a743..0d8b4b2df 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -1,6 +1,7 @@ import { AI } from '@/api/interface/ai'; import http from '@/api'; import { ResPage, SearchWithPage } from '../interface'; +import { TimeoutEnum } from '@/enums/http-enum'; export const createOllamaModel = (name: string, taskID: string) => { return http.post(`/ai/ollama/model`, { name: name, taskID: taskID }); @@ -93,7 +94,7 @@ export const operateTensorRTLLM = (req: AI.TensorRTLLMOperate) => { }; export const createAgent = (req: AI.AgentCreateReq) => { - return http.post(`/ai/agents`, req); + return http.post(`/ai/agents`, req, TimeoutEnum.T_60S); }; export const pageAgents = (req: SearchWithPage) => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index d4cff108e..ef65943a5 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -684,6 +684,8 @@ const message = { accountCount: '{0} model accounts', syncAgents: 'Sync related agents', syncAgentsHelper: 'Update openclaw.json for agents using this model account', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'App Version', webuiPort: 'WebUI Port', bridgePort: 'Bridge Port', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index cc4255f94..e92e065e0 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -680,6 +680,8 @@ const message = { accountCount: '{0} cuentas de modelo', syncAgents: 'Sincronizar agentes vinculados', syncAgentsHelper: 'Actualiza openclaw.json para los agentes que usan esta cuenta de modelo', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'Versión de la app', webuiPort: 'Puerto WebUI', bridgePort: 'Puerto Bridge', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 187edf912..59ec0c802 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -672,6 +672,8 @@ const message = { accountCount: 'モデルアカウント {0} 件', syncAgents: '関連エージェントを同期', syncAgentsHelper: 'このモデルアカウントを使用するエージェントの openclaw.json を更新', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'アプリバージョン', webuiPort: 'WebUI ポート', bridgePort: 'Bridge ポート', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index e821ee927..3988929df 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -667,6 +667,8 @@ const message = { accountCount: '모델 계정 {0}개', syncAgents: '관련 에이전트 동기화', syncAgentsHelper: '이 모델 계정을 사용하는 에이전트의 openclaw.json 업데이트', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: '앱 버전', webuiPort: 'WebUI 포트', bridgePort: 'Bridge 포트', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 37d36ca17..8b54334d7 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -683,6 +683,8 @@ const message = { accountCount: 'Akaun model {0}', syncAgents: 'Segerakkan agen berkaitan', syncAgentsHelper: 'Kemas kini openclaw.json untuk agen yang menggunakan akaun model ini', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'Versi aplikasi', webuiPort: 'Port WebUI', bridgePort: 'Port Bridge', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 12efb52eb..5ee02250a 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -680,6 +680,8 @@ const message = { accountCount: '{0} contas de modelo', syncAgents: 'Sincronizar agentes vinculados', syncAgentsHelper: 'Atualize o openclaw.json para agentes que usam esta conta de modelo', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'Versão do app', webuiPort: 'Porta WebUI', bridgePort: 'Porta Bridge', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e0959a799..10d05d5f4 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -675,6 +675,8 @@ const message = { accountCount: '{0} аккаунтов модели', syncAgents: 'Синхронизировать связанные агенты', syncAgentsHelper: 'Обновите openclaw.json для агентов, использующих этот аккаунт модели', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'Версия приложения', webuiPort: 'Порт WebUI', bridgePort: 'Порт Bridge', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 1b0d2ec3b..67a321c68 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -689,6 +689,8 @@ const message = { accountCount: '{0} model hesabı', syncAgents: 'İlişkili ajanları senkronize et', syncAgentsHelper: 'Bu model hesabını kullanan ajanlar için openclaw.json dosyasını güncelleyin', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: 'Uygulama sürümü', webuiPort: 'WebUI portu', bridgePort: 'Bridge portu', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 22c203065..8b52a9421 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -658,6 +658,8 @@ const message = { accountCount: '模型帳號 {0} 個', syncAgents: '同步關聯智能體', syncAgentsHelper: '更新使用該模型帳號的智能體 openclaw.json', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: '應用版本', webuiPort: 'WebUI 埠', bridgePort: 'Bridge 埠', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fd3058ce0..f24c7e285 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -642,6 +642,8 @@ const message = { accountCount: '模型账号 {0} 个', syncAgents: '同步关联智能体', syncAgentsHelper: '更新使用该模型账号的智能体 openclaw.json', + openclawType: 'OpenClaw', + copawType: 'CoPaw', appVersion: '应用版本', webuiPort: 'WebUI 端口', bridgePort: 'Bridge 端口', diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index 0488f80f7..4bf7f3ce3 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -16,6 +16,16 @@ export const jumpToInstall = (type: string, key: string) => { case 'openclaw': jumpToPath(router, '/ai/agents/agent'); return true; + case 'copaw': + router.push({ + path: '/ai/agents/agent', + query: { + uncached: 'true', + open: 'create', + agentType: 'copaw', + }, + }); + return true; case 'mysql-cluster': jumpToPath(router, '/xpack/cluster/mysql'); return true; diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index e7d7b9ca3..42a913cd5 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -5,6 +5,12 @@ + + + + + + @@ -13,11 +19,15 @@ - + - + { + return (_rule: any, value: any, callback: (error?: Error) => void) => { + if (form.agentType !== 'openclaw') { + callback(); + return; + } + if (field === 'bridgePort') { + if (!value || Number(value) <= 0) { + callback(new Error('bridge port is required')); + return; + } + callback(); + return; + } + if (value === undefined || value === null || value === '') { + callback(new Error(`${field} is required`)); + return; + } + callback(); + }; +}; + const rules = reactive({ name: [Rules.requiredInput], + agentType: [Rules.requiredSelect], appVersion: [Rules.requiredSelect], webUIPort: [Rules.requiredInput], - bridgePort: [Rules.requiredInput], - provider: [Rules.requiredSelect], - accountId: [Rules.requiredSelect], - model: [Rules.requiredSelect], + bridgePort: [{ validator: validateOpenclawOnly('bridgePort'), trigger: 'blur' }], + provider: [{ validator: validateOpenclawOnly('provider'), trigger: 'change' }], + accountId: [{ validator: validateOpenclawOnly('accountId'), trigger: 'change' }], + model: [{ validator: validateOpenclawOnly('model'), trigger: 'change' }], containerName: [Rules.containerName], restartPolicy: [Rules.requiredSelect], cpuQuota: [checkNumberRange(0, 99999)], @@ -156,8 +190,8 @@ const rules = reactive({ const filteredModels = computed(() => providerModels.value[form.provider] || []); -const loadVersions = async () => { - const res = await getAppByKey('openclaw'); +const loadVersions = async (appKey: 'openclaw' | 'copaw') => { + const res = await getAppByKey(appKey); appInfo.value = res.data; versions.value = res.data.versions || []; if (!form.appVersion && versions.value.length > 0) { @@ -174,6 +208,11 @@ const loadCompose = async () => { }; const loadProviders = async () => { + if (form.agentType !== 'openclaw') { + providerOptions.value = []; + providerModels.value = {}; + return; + } const res = await getAgentProviders(); const data = res.data || []; providerOptions.value = data.map((item) => ({ @@ -206,6 +245,10 @@ const loadProviderAccountCounts = async (providers: string[]) => { }; const loadAccounts = async () => { + if (form.agentType !== 'openclaw') { + accountOptions.value = []; + return; + } if (!form.provider) { accountOptions.value = []; return; @@ -230,6 +273,9 @@ const loadAccounts = async () => { }; const handleProviderChange = () => { + if (form.agentType !== 'openclaw') { + return; + } form.model = ''; form.apiKey = ''; form.baseURL = ''; @@ -238,6 +284,27 @@ const handleProviderChange = () => { setDefaultModel(); }; +const handleAgentTypeChange = async () => { + if (form.name === '' || form.name === 'OpenClaw' || form.name === 'CoPaw') { + form.name = form.agentType === 'copaw' ? 'CoPaw' : 'OpenClaw'; + } + form.appVersion = ''; + form.model = ''; + form.provider = 'deepseek'; + form.accountId = undefined as unknown as number; + form.apiKey = ''; + form.baseURL = ''; + form.apiType = 'openai-completions'; + if (form.agentType === 'openclaw') { + form.bridgePort = form.bridgePort || 18790; + await loadVersions('openclaw'); + await loadProviders(); + await loadAccounts(); + return; + } + await loadVersions('copaw'); +}; + const handleModelChange = () => { if (manualModel.value) { return; @@ -245,6 +312,9 @@ const handleModelChange = () => { }; const handleAccountChange = () => { + if (form.agentType !== 'openclaw') { + return; + } const selected = accountOptions.value.find((item) => item.id === form.accountId); if (selected) { form.baseURL = selected.baseUrl || ''; @@ -260,6 +330,9 @@ const handleAccountChange = () => { }; const setDefaultModel = () => { + if (form.agentType !== 'openclaw') { + return; + } if (manualModel.value) { return; } @@ -282,40 +355,50 @@ const submit = async () => { } await formRef.value.validate(); const taskID = newUUID(); - if (!form.token) { + if (form.agentType === 'openclaw' && !form.token) { form.token = getRandomStr(32).toLowerCase(); } - const res = await createAgent({ - name: form.name, - appVersion: form.appVersion, - webUIPort: form.webUIPort, - bridgePort: form.bridgePort, - provider: form.provider, - model: form.model, - apiType: form.apiType, - maxTokens: form.maxTokens, - contextWindow: form.contextWindow, - accountId: form.accountId, - apiKey: form.apiKey, - baseURL: form.baseURL, - token: form.token, - taskID: taskID, - advanced: form.advanced, - containerName: form.containerName, - allowPort: form.allowPort, - specifyIP: form.specifyIP, - restartPolicy: form.restartPolicy, - cpuQuota: form.cpuQuota, - memoryLimit: form.memoryLimit, - memoryUnit: form.memoryUnit, - pullImage: form.pullImage, - editCompose: form.editCompose, - dockerCompose: form.dockerCompose, - }); - form.token = res.data.token || form.token; - emit('search'); - emit('task', taskID); - open.value = false; + try { + const res = await createAgent({ + name: form.name, + appVersion: form.appVersion, + webUIPort: form.webUIPort, + bridgePort: form.agentType === 'openclaw' ? form.bridgePort : 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, + containerName: form.containerName, + allowPort: form.allowPort, + specifyIP: form.specifyIP, + restartPolicy: form.restartPolicy, + cpuQuota: form.cpuQuota, + memoryLimit: form.memoryLimit, + memoryUnit: form.memoryUnit, + pullImage: form.pullImage, + editCompose: form.editCompose, + dockerCompose: form.dockerCompose, + }); + form.token = res.data.token || form.token; + emit('search'); + emit('task', taskID); + open.value = false; + } catch (error: any) { + const message = String(error?.message || '').toLowerCase(); + const isTimeout = message.includes('timeout') || error?.code === 'ECONNABORTED'; + if (isTimeout) { + emit('task', taskID); + open.value = false; + } + } }; const handleClose = () => { @@ -324,17 +407,29 @@ const handleClose = () => { form.dockerCompose = ''; }; -const openDrawer = async () => { - form.name = 'openclaw'; +const openDrawer = async (agentType?: 'openclaw' | 'copaw') => { + const targetType = agentType === 'copaw' ? 'copaw' : 'openclaw'; + form.name = targetType === 'copaw' ? 'CoPaw' : 'OpenClaw'; open.value = true; manualModel.value = false; + form.agentType = targetType; form.token = getRandomStr(32).toLowerCase(); - await loadVersions(); + if (form.agentType === 'copaw') { + await loadVersions('copaw'); + providerOptions.value = []; + providerModels.value = {}; + accountOptions.value = []; + return; + } + await loadVersions('openclaw'); await loadProviders(); await loadAccounts(); }; const openAccountCreate = () => { + if (form.agentType !== 'openclaw') { + return; + } if (accountAddRef.value?.open) { accountAddRef.value.open({ provider: form.provider }); } diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index c3fbf5f73..c9a286d9a 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -18,6 +18,19 @@ prop="name" min-width="120" /> + + +