feat(ai-agents): Add copaw type for agent (#12017)

This commit is contained in:
CityFun
2026-03-02 06:53:58 +00:00
committed by GitHub
parent 9ddb996a44
commit d155217ce3
23 changed files with 389 additions and 130 deletions
+5 -3
View File
@@ -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"`
+1
View File
@@ -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"`
+1 -1
View File
@@ -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,
+131 -62
View File
@@ -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 == "" {
+1 -1
View File
@@ -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)
}
+1
View File
@@ -8,6 +8,7 @@ const (
AppOpenresty = "openresty"
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
+1
View File
@@ -69,6 +69,7 @@ func InitAgentDB() {
migrations.AddAppInstallSortOrder,
migrations.AddAgentAccountRememberAPIKey,
migrations.AddEditionSetting,
migrations.AddAgentTypeForAgents,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+17
View File
@@ -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
},
}
+12 -10
View File
@@ -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;
+2 -1
View File
@@ -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.AgentItem>(`/ai/agents`, req);
return http.post<AI.AgentItem>(`/ai/agents`, req, TimeoutEnum.T_60S);
};
export const pageAgents = (req: SearchWithPage) => {
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -672,6 +672,8 @@ const message = {
accountCount: 'モデルアカウント {0} ',
syncAgents: '関連エージェントを同期',
syncAgentsHelper: 'このモデルアカウントを使用するエージェントの openclaw.json を更新',
openclawType: 'OpenClaw',
copawType: 'CoPaw',
appVersion: 'アプリバージョン',
webuiPort: 'WebUI ポート',
bridgePort: 'Bridge ポート',
+2
View File
@@ -667,6 +667,8 @@ const message = {
accountCount: '모델 계정 {0}개',
syncAgents: '관련 에이전트 동기화',
syncAgentsHelper: '이 모델 계정을 사용하는 에이전트의 openclaw.json 업데이트',
openclawType: 'OpenClaw',
copawType: 'CoPaw',
appVersion: '앱 버전',
webuiPort: 'WebUI 포트',
bridgePort: 'Bridge 포트',
+2
View File
@@ -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',
+2
View File
@@ -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',
+2
View File
@@ -675,6 +675,8 @@ const message = {
accountCount: '{0} аккаунтов модели',
syncAgents: 'Синхронизировать связанные агенты',
syncAgentsHelper: 'Обновите openclaw.json для агентов, использующих этот аккаунт модели',
openclawType: 'OpenClaw',
copawType: 'CoPaw',
appVersion: 'Версия приложения',
webuiPort: 'Порт WebUI',
bridgePort: 'Порт Bridge',
+2
View File
@@ -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',
+2
View File
@@ -658,6 +658,8 @@ const message = {
accountCount: '模型帳號 {0} ',
syncAgents: '同步關聯智能體',
syncAgentsHelper: '更新使用該模型帳號的智能體 openclaw.json',
openclawType: 'OpenClaw',
copawType: 'CoPaw',
appVersion: '應用版本',
webuiPort: 'WebUI ',
bridgePort: 'Bridge ',
+2
View File
@@ -642,6 +642,8 @@ const message = {
accountCount: '模型账号 {0} ',
syncAgents: '同步关联智能体',
syncAgentsHelper: '更新使用该模型账号的智能体 openclaw.json',
openclawType: 'OpenClaw',
copawType: 'CoPaw',
appVersion: '应用版本',
webuiPort: 'WebUI 端口',
bridgePort: 'Bridge 端口',
+10
View File
@@ -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;
+138 -43
View File
@@ -5,6 +5,12 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="`${$t('aiTools.agents.agents')}${$t('commons.table.type')}`" prop="agentType">
<el-select v-model="form.agentType" @change="handleAgentTypeChange">
<el-option :label="$t('aiTools.agents.openclawType')" value="openclaw" />
<el-option :label="$t('aiTools.agents.copawType')" value="copaw" />
</el-select>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.appVersion')" prop="appVersion">
<el-select v-model="form.appVersion" filterable>
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
@@ -13,11 +19,15 @@
<el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort">
<el-input-number v-model="form.webUIPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.bridgePort')" prop="bridgePort">
<el-form-item
v-if="form.agentType === 'openclaw'"
:label="$t('aiTools.agents.bridgePort')"
prop="bridgePort"
>
<el-input-number v-model="form.bridgePort" :min="1" :max="65535" />
</el-form-item>
</el-card>
<el-card class="form-card">
<el-card class="form-card" v-if="form.agentType === 'openclaw'">
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
<el-select v-model="form.provider" @change="handleProviderChange">
<el-option
@@ -114,6 +124,7 @@ const accountAddRef = ref();
const form = reactive({
name: '',
agentType: 'openclaw' as 'openclaw' | 'copaw',
appVersion: '',
webUIPort: 18789,
bridgePort: 18790,
@@ -139,14 +150,37 @@ const form = reactive({
dockerCompose: '',
});
const validateOpenclawOnly = (field: 'provider' | 'accountId' | 'model' | 'bridgePort') => {
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 });
}
+49 -9
View File
@@ -18,6 +18,19 @@
prop="name"
min-width="120"
/>
<el-table-column
:label="`${$t('aiTools.agents.agents')}${$t('commons.table.type')}`"
prop="agentType"
min-width="120"
>
<template #default="{ row }">
{{
row.agentType === 'copaw'
? $t('aiTools.agents.copawType')
: $t('aiTools.agents.openclawType')
}}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status" width="120">
<template #default="{ row }">
<Status :status="row.status" />
@@ -38,10 +51,13 @@
min-width="120"
>
<template #default="{ row }">
<span>{{ getAgentProviderDisplayName(row.provider, row.providerName) }}</span>
<div>
<span>{{ row.model }}</span>
</div>
<template v-if="row.agentType !== 'copaw'">
<span>{{ getAgentProviderDisplayName(row.provider, row.providerName) }}</span>
<div>
<span>{{ row.model }}</span>
</div>
</template>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.port')" prop="webUIPort" min-width="150">
@@ -49,7 +65,7 @@
<el-button icon="Position" plain size="small" @click="jumpWebUI(row)">
{{ $t('aiTools.agents.webuiPort') }}: {{ row.webUIPort }}
</el-button>
<div class="mt-0.5">
<div v-if="row.agentType !== 'copaw'" class="mt-0.5">
<el-button plain size="small">
{{ $t('aiTools.agents.bridgePort') }} {{ row.bridgePort }}
</el-button>
@@ -58,12 +74,13 @@
</el-table-column>
<el-table-column :label="$t('aiTools.agents.token')" min-width="80">
<template #default="{ row }">
<el-space>
<el-space v-if="row.agentType !== 'copaw'">
<CopyButton :content="row.token" />
<el-button link type="primary" @click="onResetToken(row)">
{{ $t('commons.button.reset') }}
</el-button>
</el-space>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
@@ -96,6 +113,7 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { pageAgents, resetAgentToken } from '@/api/modules/ai';
import { installedOp, searchAppInstalled } from '@/api/modules/app';
import { AI } from '@/api/interface/ai';
@@ -127,6 +145,8 @@ const upgradeRef = ref();
const composeLogRef = ref();
const dialogTerminalRef = ref();
const dialogPortJumpRef = ref();
const route = useRoute();
const router = useRouter();
const isActive = ref(false);
const isExist = ref(false);
const searchName = ref('');
@@ -135,6 +155,7 @@ const buttons = [
{
label: i18n.global.t('menu.config'),
click: (row: AI.AgentItem) => openConfig(row),
show: (row: AI.AgentItem) => row.agentType !== 'copaw',
},
{
label: i18n.global.t('menu.terminal'),
@@ -191,12 +212,26 @@ const search = async () => {
}
};
const openCreate = () => {
const openCreate = (agentType?: 'openclaw' | 'copaw') => {
const targetType = agentType === 'copaw' ? 'copaw' : 'openclaw';
if (addRef.value?.open) {
addRef.value.open();
addRef.value.open(targetType);
}
};
const openCreateFromQuery = async () => {
const shouldOpen = route.query.open === 'create';
if (!shouldOpen) {
return;
}
const agentType = route.query.agentType === 'copaw' ? 'copaw' : 'openclaw';
openCreate(agentType);
const nextQuery = { ...route.query };
delete nextQuery.open;
delete nextQuery.agentType;
await router.replace({ path: route.path, query: nextQuery });
};
const openTaskLog = (taskID: string) => {
if (taskLogRef.value?.openWithTaskID) {
taskLogRef.value.openWithTaskID(taskID);
@@ -236,11 +271,12 @@ const openTerminal = (row: AI.AgentItem) => {
};
const jumpWebUI = (row: AI.AgentItem) => {
const query = row.agentType === 'copaw' ? '' : `token=${row.token}`;
if (dialogPortJumpRef.value?.acceptParams) {
dialogPortJumpRef.value.acceptParams({
port: row.webUIPort,
protocol: 'http',
query: `token=${row.token}`,
query,
});
}
};
@@ -265,6 +301,9 @@ const onResetToken = async (row: AI.AgentItem) => {
};
const openConfig = (row: AI.AgentItem) => {
if (row.agentType === 'copaw') {
return;
}
configRef.value?.open(row);
};
@@ -279,5 +318,6 @@ const openUpgrade = async (row: AI.AgentItem) => {
onMounted(async () => {
await search();
await openCreateFromQuery();
});
</script>