mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 16:00:52 +00:00
feat: openclaw supports configuring multiple models (#12213)
This commit is contained in:
@@ -190,6 +190,87 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary List Agent account models
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountModelReq true "request"
|
||||
// @Success 200 {array} dto.AgentAccountModel
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/models [post]
|
||||
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
|
||||
var req dto.AgentAccountModelReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
list, err := agentService.GetAccountModels(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Create Agent account model
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountModelCreateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/models/create [post]
|
||||
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
|
||||
var req dto.AgentAccountModelCreateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.CreateAccountModel(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent account model
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountModelUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/models/update [post]
|
||||
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
|
||||
var req dto.AgentAccountModelUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateAccountModel(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent account model
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountModelDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/models/delete [post]
|
||||
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
|
||||
var req dto.AgentAccountModelDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteAccountModel(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Verify Agent account
|
||||
// @Accept json
|
||||
|
||||
+73
-37
@@ -75,31 +75,62 @@ type AgentModelConfigUpdateReq struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountModel struct {
|
||||
RecordID uint `json:"recordId"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input []string `json:"input"`
|
||||
}
|
||||
|
||||
type AgentAccountModelReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountModelCreateReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model AgentAccountModel `json:"model" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountModelUpdateReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model AgentAccountModel `json:"model" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountModelDeleteReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
RecordID uint `json:"recordId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountCreateReq struct {
|
||||
Provider string `json:"provider" 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"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
Remark string `json:"remark"`
|
||||
Provider string `json:"provider" 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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type AgentAccountVerifyReq struct {
|
||||
@@ -119,25 +150,30 @@ type AgentAccountSearch struct {
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"providerName"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Model string `json:"model"`
|
||||
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"`
|
||||
ID uint `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"providerName"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ProviderModelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input []string `json:"input"`
|
||||
}
|
||||
|
||||
type ProviderInfo struct {
|
||||
|
||||
@@ -7,6 +7,7 @@ type AgentAccount struct {
|
||||
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"`
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type AgentAccountModel struct {
|
||||
BaseModel
|
||||
AccountID uint `json:"accountId" gorm:"index"`
|
||||
Model string `json:"model" gorm:"index"`
|
||||
Name string `json:"name"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input string `json:"input" gorm:"type:text"`
|
||||
SortOrder int `json:"sortOrder" gorm:"index"`
|
||||
}
|
||||
|
||||
func (AgentAccountModel) TableName() string {
|
||||
return "agent_account_models"
|
||||
}
|
||||
@@ -5,8 +5,12 @@ import (
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID string
|
||||
Name string
|
||||
ID string
|
||||
Name string
|
||||
ContextWindow int
|
||||
MaxTokens int
|
||||
Reasoning bool
|
||||
Input []string
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
@@ -264,7 +268,60 @@ func cloneMeta(meta Meta) Meta {
|
||||
clone := meta
|
||||
if len(meta.Models) > 0 {
|
||||
clone.Models = make([]Model, len(meta.Models))
|
||||
copy(clone.Models, meta.Models)
|
||||
for i, item := range meta.Models {
|
||||
clone.Models[i] = normalizeModel(meta.Key, item)
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func normalizeModel(provider string, model Model) Model {
|
||||
clone := model
|
||||
clone.ID = strings.TrimSpace(clone.ID)
|
||||
clone.Name = strings.TrimSpace(clone.Name)
|
||||
if clone.Name == "" {
|
||||
clone.Name = clone.ID
|
||||
}
|
||||
if clone.MaxTokens <= 0 || clone.ContextWindow <= 0 {
|
||||
resolvedMaxTokens, resolvedContextWindow := catalogRuntimeDefaults(strings.ToLower(strings.TrimSpace(provider)))
|
||||
if clone.MaxTokens <= 0 {
|
||||
clone.MaxTokens = resolvedMaxTokens
|
||||
}
|
||||
if clone.ContextWindow <= 0 {
|
||||
clone.ContextWindow = resolvedContextWindow
|
||||
}
|
||||
}
|
||||
if len(clone.Input) == 0 {
|
||||
clone.Input = defaultModelInputs(provider)
|
||||
}
|
||||
if !clone.Reasoning {
|
||||
clone.Reasoning = isReasoningModel(clone.ID)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func defaultModelInputs(provider string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(provider)) {
|
||||
case "kimi-coding":
|
||||
return []string{"text", "image"}
|
||||
default:
|
||||
return []string{"text"}
|
||||
}
|
||||
}
|
||||
|
||||
func catalogRuntimeDefaults(provider string) (int, int) {
|
||||
switch provider {
|
||||
case "deepseek":
|
||||
return 8192, 128000
|
||||
case "zai":
|
||||
return 131072, 204800
|
||||
case "openrouter":
|
||||
return 8192, 128000
|
||||
case "minimax", "kimi-coding":
|
||||
return 8192, 200000
|
||||
case "custom", "vllm":
|
||||
return 8192, 128000
|
||||
default:
|
||||
return 8192, 256000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextW
|
||||
case "deepseek":
|
||||
return buildDeepseekPatch(modelName, baseURL, apiKey), nil
|
||||
case "gemini":
|
||||
return buildGeminiPatch(modelName), nil
|
||||
return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
|
||||
case "moonshot", "kimi":
|
||||
return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil
|
||||
case "bailian-coding-plan":
|
||||
@@ -47,10 +47,6 @@ func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextW
|
||||
}
|
||||
}
|
||||
|
||||
func buildGeminiPatch(modelName string) *OpenClawPatch {
|
||||
return &OpenClawPatch{PrimaryModel: modelName}
|
||||
}
|
||||
|
||||
func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch {
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: modelName,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package repo
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
|
||||
type AgentAccountModelRepo struct{}
|
||||
|
||||
type IAgentAccountModelRepo interface {
|
||||
List(opts ...DBOption) ([]model.AgentAccountModel, error)
|
||||
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
|
||||
Create(item *model.AgentAccountModel) error
|
||||
Save(item *model.AgentAccountModel) error
|
||||
DeleteByID(id uint) error
|
||||
Delete(opts ...DBOption) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountModelRepo() IAgentAccountModelRepo {
|
||||
return &AgentAccountModelRepo{}
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) List(opts ...DBOption) ([]model.AgentAccountModel, error) {
|
||||
var list []model.AgentAccountModel
|
||||
if err := getDb(opts...).Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) GetFirst(opts ...DBOption) (*model.AgentAccountModel, error) {
|
||||
var item model.AgentAccountModel
|
||||
if err := getDb(opts...).First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) Create(item *model.AgentAccountModel) error {
|
||||
return getDb().Create(item).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) error {
|
||||
return getDb().Save(item).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
|
||||
return getDb().Delete(&model.AgentAccountModel{}, id).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
|
||||
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
|
||||
}
|
||||
+1025
-85
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,12 @@ var (
|
||||
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
|
||||
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
|
||||
|
||||
aiRepo = repo.NewIAiRepo()
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
|
||||
agentRepo = repo.NewIAgentRepo()
|
||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||
aiRepo = repo.NewIAiRepo()
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
|
||||
agentRepo = repo.NewIAgentRepo()
|
||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
|
||||
@@ -45,6 +45,8 @@ ErrAgentProviderNotSupported: 'Unsupported agent provider'
|
||||
ErrAgentAccountRequired: 'Select an agent account first'
|
||||
ErrAgentAccountNotVerified: 'Agent account not verified'
|
||||
ErrAgentProviderMismatch: 'Agent provider mismatch'
|
||||
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
|
||||
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Base URL is required'
|
||||
ErrAgentApiKeyRequired: 'API key is required'
|
||||
ErrAgentComposeRequired: 'Compose content is required'
|
||||
|
||||
@@ -40,6 +40,8 @@ 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'
|
||||
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
|
||||
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'URL base requerida'
|
||||
ErrAgentApiKeyRequired: 'Clave API requerida'
|
||||
ErrAgentComposeRequired: 'Contenido compose requerido'
|
||||
|
||||
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
||||
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
||||
ErrAgentAccountNotVerified: 'アカウント未検証です'
|
||||
ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
|
||||
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
||||
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'ベースURLが必要です'
|
||||
ErrAgentApiKeyRequired: 'APIキーが必要です'
|
||||
ErrAgentComposeRequired: 'Composeが必要です'
|
||||
|
||||
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
||||
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
||||
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
||||
ErrAgentProviderMismatch: '에이전트 공급자 불일치'
|
||||
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
||||
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: '기본 URL 필요'
|
||||
ErrAgentApiKeyRequired: 'API 키 필요'
|
||||
ErrAgentComposeRequired: 'Compose 내용 필요'
|
||||
|
||||
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
|
||||
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
|
||||
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
|
||||
ErrAgentProviderMismatch: 'Penyedia ejen tidak sepadan'
|
||||
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
|
||||
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'URL asas diperlukan'
|
||||
ErrAgentApiKeyRequired: 'Kunci API diperlukan'
|
||||
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
|
||||
|
||||
@@ -40,6 +40,8 @@ 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'
|
||||
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'
|
||||
ErrAgentApiKeyRequired: 'Chave API obrigatória'
|
||||
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
|
||||
|
||||
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер
|
||||
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
||||
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
||||
ErrAgentProviderMismatch: 'Провайдер не совпадает'
|
||||
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
||||
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Нужен базовый URL'
|
||||
ErrAgentApiKeyRequired: 'Нужен API-ключ'
|
||||
ErrAgentComposeRequired: 'Нужен compose'
|
||||
|
||||
@@ -40,6 +40,8 @@ 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'
|
||||
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'
|
||||
ErrAgentApiKeyRequired: 'API anahtarı gerekli'
|
||||
ErrAgentComposeRequired: 'Compose içeriği gerekli'
|
||||
|
||||
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
|
||||
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
|
||||
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
|
||||
ErrAgentProviderMismatch: '帳號提供商不符,請重試'
|
||||
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
|
||||
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
|
||||
ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
|
||||
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
|
||||
|
||||
@@ -45,6 +45,8 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商"
|
||||
ErrAgentAccountRequired: "请选择智能体账号后重试"
|
||||
ErrAgentAccountNotVerified: "账号未通过验证"
|
||||
ErrAgentProviderMismatch: "账号提供商不匹配"
|
||||
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
|
||||
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
|
||||
ErrAgentBaseURLRequired: "Base URL 不能为空"
|
||||
ErrAgentApiKeyRequired: "API Key 不能为空"
|
||||
ErrAgentComposeRequired: "自定义编排内容不能为空"
|
||||
|
||||
@@ -74,6 +74,7 @@ func InitAgentDB() {
|
||||
migrations.NormalizeAgentAccountVerifiedStatus,
|
||||
migrations.NormalizeOllamaAccountAPIType,
|
||||
migrations.RewriteOpenclawBundledCaddyfile,
|
||||
migrations.InitAgentAccountModelPool,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -1102,3 +1102,13 @@ var RewriteOpenclawBundledCaddyfile = &gormigrate.Migration{
|
||||
return migrationutils.RewriteOpenclawBundledCaddyfile(tx)
|
||||
},
|
||||
}
|
||||
|
||||
var InitAgentAccountModelPool = &gormigrate.Migration{
|
||||
ID: "20260319-init-agent-account-model-pool",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.AgentAccountModel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrationutils.MigrateAgentAccountModelPool(tx)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MigrateAgentAccountModelPool(tx *gorm.DB) error {
|
||||
var accounts []model.AgentAccount
|
||||
if err := tx.Find(&accounts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, account := range accounts {
|
||||
count := int64(0)
|
||||
if err := tx.Model(&model.AgentAccountModel{}).Where("account_id = ?", account.ID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
models, err := buildMigratedAgentAccountModels(tx, &account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(models) == 0 {
|
||||
continue
|
||||
}
|
||||
for index, item := range models {
|
||||
inputPayload, err := json.Marshal(item.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record := &model.AgentAccountModel{
|
||||
AccountID: account.ID,
|
||||
Model: item.ID,
|
||||
Name: 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 buildMigratedAgentAccountModels(tx *gorm.DB, account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
|
||||
if account == nil {
|
||||
return nil, nil
|
||||
}
|
||||
requested := make([]dto.AgentAccountModel, 0)
|
||||
if strings.TrimSpace(account.Models) != "" {
|
||||
if err := json.Unmarshal([]byte(account.Models), &requested); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, item := range requested {
|
||||
target := strings.TrimSpace(item.ID)
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
seen[target] = struct{}{}
|
||||
}
|
||||
appendModel := func(modelID string) {
|
||||
target := strings.TrimSpace(modelID)
|
||||
if target == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[target]; ok {
|
||||
return
|
||||
}
|
||||
seen[target] = struct{}{}
|
||||
requested = append(requested, dto.AgentAccountModel{ID: target})
|
||||
}
|
||||
appendModel(account.Model)
|
||||
if account.ID > 0 {
|
||||
var agents []model.Agent
|
||||
if err := tx.Where("account_id = ?", account.ID).Find(&agents).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, agent := range agents {
|
||||
appendModel(agent.Model)
|
||||
}
|
||||
}
|
||||
models, err := service.MergeCatalogAgentAccountModelsForMigration(account, requested)
|
||||
if err != nil {
|
||||
if strings.TrimSpace(err.Error()) == "model is required" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
@@ -49,6 +49,10 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/search", baseApi.PageAgentAccounts)
|
||||
aiToolsRouter.POST("/agents/accounts/models", baseApi.GetAgentAccountModels)
|
||||
aiToolsRouter.POST("/agents/accounts/models/create", baseApi.CreateAgentAccountModel)
|
||||
aiToolsRouter.POST("/agents/accounts/models/update", baseApi.UpdateAgentAccountModel)
|
||||
aiToolsRouter.POST("/agents/accounts/models/delete", baseApi.DeleteAgentAccountModel)
|
||||
aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
|
||||
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
|
||||
|
||||
@@ -309,9 +309,42 @@ export namespace AI {
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface AgentAccountModel {
|
||||
recordId: number;
|
||||
id: string;
|
||||
name: string;
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
reasoning: boolean;
|
||||
input: string[];
|
||||
}
|
||||
|
||||
export interface AgentAccountModelReq {
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export interface AgentAccountModelCreateReq {
|
||||
accountId: number;
|
||||
model: AgentAccountModel;
|
||||
}
|
||||
|
||||
export interface AgentAccountModelUpdateReq {
|
||||
accountId: number;
|
||||
model: AgentAccountModel;
|
||||
}
|
||||
|
||||
export interface AgentAccountModelDeleteReq {
|
||||
accountId: number;
|
||||
recordId: number;
|
||||
}
|
||||
|
||||
export interface ProviderModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
reasoning: boolean;
|
||||
input: string[];
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
@@ -327,7 +360,6 @@ export namespace AI {
|
||||
apiKey: string;
|
||||
rememberApiKey: boolean;
|
||||
baseURL: string;
|
||||
model: string;
|
||||
apiType: string;
|
||||
maxTokens: number;
|
||||
contextWindow: number;
|
||||
@@ -340,7 +372,6 @@ export namespace AI {
|
||||
apiKey: string;
|
||||
rememberApiKey: boolean;
|
||||
baseURL: string;
|
||||
model: string;
|
||||
apiType: string;
|
||||
maxTokens: number;
|
||||
contextWindow: number;
|
||||
@@ -364,6 +395,7 @@ export namespace AI {
|
||||
rememberApiKey: boolean;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
models: AgentAccountModel[];
|
||||
apiType: string;
|
||||
maxTokens: number;
|
||||
contextWindow: number;
|
||||
|
||||
@@ -129,6 +129,22 @@ export const pageAgentAccounts = (req: AI.AgentAccountSearch) => {
|
||||
return http.post<ResPage<AI.AgentAccountItem>>(`/ai/agents/accounts/search`, req);
|
||||
};
|
||||
|
||||
export const getAgentAccountModels = (req: AI.AgentAccountModelReq) => {
|
||||
return http.post<AI.AgentAccountModel[]>(`/ai/agents/accounts/models`, req);
|
||||
};
|
||||
|
||||
export const createAgentAccountModel = (req: AI.AgentAccountModelCreateReq) => {
|
||||
return http.post(`/ai/agents/accounts/models/create`, req);
|
||||
};
|
||||
|
||||
export const updateAgentAccountModel = (req: AI.AgentAccountModelUpdateReq) => {
|
||||
return http.post(`/ai/agents/accounts/models/update`, req);
|
||||
};
|
||||
|
||||
export const deleteAgentAccountModel = (req: AI.AgentAccountModelDeleteReq) => {
|
||||
return http.post(`/ai/agents/accounts/models/delete`, req);
|
||||
};
|
||||
|
||||
export const verifyAgentAccount = (req: AI.AgentAccountVerifyReq) => {
|
||||
return http.post(`/ai/agents/accounts/verify`, req);
|
||||
};
|
||||
|
||||
@@ -688,6 +688,17 @@ const message = {
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Token',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
|
||||
@@ -696,6 +696,17 @@ const message = {
|
||||
provider: 'Proveedor de modelos',
|
||||
apiKey: 'Clave API',
|
||||
baseUrl: 'URL base',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Token',
|
||||
manualModel: 'Entrada manual de modelo',
|
||||
verified: 'Verificado',
|
||||
|
||||
@@ -689,6 +689,17 @@ const message = {
|
||||
provider: 'モデルプロバイダー',
|
||||
apiKey: 'API キー',
|
||||
baseUrl: 'ベースURL',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'トークン',
|
||||
manualModel: '手動入力',
|
||||
verified: '検証済み',
|
||||
|
||||
@@ -681,6 +681,17 @@ const message = {
|
||||
provider: '모델 제공자',
|
||||
apiKey: 'API 키',
|
||||
baseUrl: '기본 URL',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: '토큰',
|
||||
manualModel: '수동 입력',
|
||||
verified: '검증됨',
|
||||
|
||||
@@ -696,6 +696,17 @@ const message = {
|
||||
provider: 'Penyedia model',
|
||||
apiKey: 'Kunci API',
|
||||
baseUrl: 'URL asas',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Token',
|
||||
manualModel: 'Input manual',
|
||||
verified: 'Disahkan',
|
||||
|
||||
@@ -691,6 +691,17 @@ const message = {
|
||||
provider: 'Provedor de modelos',
|
||||
apiKey: 'Chave API',
|
||||
baseUrl: 'URL base',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Token',
|
||||
manualModel: 'Entrada manual',
|
||||
verified: 'Verificado',
|
||||
|
||||
@@ -688,6 +688,17 @@ const message = {
|
||||
provider: 'Поставщик моделей',
|
||||
apiKey: 'API ключ',
|
||||
baseUrl: 'Базовый URL',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Токен',
|
||||
manualModel: 'Ручной ввод',
|
||||
verified: 'Проверено',
|
||||
|
||||
@@ -692,6 +692,17 @@ const message = {
|
||||
provider: 'Model sağlayıcı',
|
||||
apiKey: 'API anahtarı',
|
||||
baseUrl: 'Temel URL',
|
||||
accountModels: 'Model Catalog',
|
||||
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.',
|
||||
modelInputTypes: 'Input Types',
|
||||
reasoning: 'Reasoning Model',
|
||||
token: 'Token',
|
||||
manualModel: 'Manuel giriş',
|
||||
verified: 'Doğrulandı',
|
||||
|
||||
@@ -656,6 +656,15 @@ const message = {
|
||||
provider: '模型供應商',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
accountModels: '模型池',
|
||||
accountModelsHelper: '配置該帳號可提供給 OpenClaw 使用與切換的模型列表',
|
||||
accountModelsRequired: '請至少配置一個模型',
|
||||
accountModelsDuplicate: '模型池中存在重複模型,請檢查後重試',
|
||||
accountCreateHelper: '建立模型帳號後可繼續修改模型池;如果供應商 catalog 已提供 Models,將自動匯入模型池',
|
||||
modelPool: '模型池',
|
||||
modelPoolHelper: '在此維護該模型帳號的模型池,建立智能體與 OpenClaw 模型切換都會使用這些模型',
|
||||
modelInputTypes: '輸入類型',
|
||||
reasoning: '推理模型',
|
||||
token: 'Token',
|
||||
manualModel: '手動輸入模型',
|
||||
verified: '驗證狀態',
|
||||
|
||||
@@ -655,6 +655,15 @@ const message = {
|
||||
provider: '模型供应商',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
accountModels: '模型池',
|
||||
accountModelsHelper: '配置账号可提供给 OpenClaw 使用和切换的模型列表',
|
||||
accountModelsRequired: '请至少配置一个模型',
|
||||
accountModelsDuplicate: '模型池中存在重复模型,请检查后重试',
|
||||
accountCreateHelper: '创建模型账号后可继续修改模型池;如果供应商 catalog 已提供 Models,将自动导入模型池',
|
||||
modelPool: '模型池',
|
||||
modelPoolHelper: '在这里维护该模型账号的模型池,智能体创建和 OpenClaw 模型切换都会使用这些模型',
|
||||
modelInputTypes: '输入类型',
|
||||
reasoning: '推理模型',
|
||||
token: 'Token',
|
||||
manualModel: '手动输入模型',
|
||||
verified: '验证状态',
|
||||
|
||||
@@ -70,15 +70,11 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="manualModel">{{ $t('aiTools.agents.manualModel') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.model.model')" prop="model">
|
||||
<el-input v-if="manualModel" v-model="form.model" />
|
||||
|
||||
<el-select v-else v-model="form.model" filterable @change="handleModelChange">
|
||||
<el-select v-model="form.model" filterable>
|
||||
<el-option v-for="item in filteredModels" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<span class="input-help">{{ $t('aiTools.agents.accountModelsHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.baseUrl')" v-if="form.accountId" prop="baseURL">
|
||||
<el-input v-model="form.baseURL" disabled />
|
||||
@@ -134,7 +130,6 @@ const accountOptions = ref<AI.AgentAccountItem[]>([]);
|
||||
const providerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
|
||||
const providerAccountCount = ref<Record<string, number>>({});
|
||||
const manualModel = ref(false);
|
||||
const appInfo = ref<App.AppDTO>();
|
||||
const accountAddRef = ref();
|
||||
const systemIP = ref('');
|
||||
@@ -199,7 +194,10 @@ const rules = reactive({
|
||||
specifyIP: [Rules.ipv4orV6],
|
||||
});
|
||||
|
||||
const filteredModels = computed(() => providerModels.value[form.provider] || []);
|
||||
const filteredModels = computed(() => {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
return selected?.models || [];
|
||||
});
|
||||
|
||||
const syncAllowedOriginsWithDefault = (force = false) => {
|
||||
if (form.agentType !== 'openclaw') {
|
||||
@@ -316,7 +314,6 @@ const handleProviderChange = () => {
|
||||
form.baseURL = '';
|
||||
form.accountId = undefined as unknown as number;
|
||||
loadAccounts();
|
||||
setDefaultModel();
|
||||
};
|
||||
|
||||
const handleAgentTypeChange = async () => {
|
||||
@@ -345,12 +342,6 @@ const handleAgentTypeChange = async () => {
|
||||
await loadVersions('copaw');
|
||||
};
|
||||
|
||||
const handleModelChange = () => {
|
||||
if (manualModel.value) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountChange = () => {
|
||||
if (form.agentType !== 'openclaw') {
|
||||
return;
|
||||
@@ -362,8 +353,8 @@ const handleAccountChange = () => {
|
||||
form.apiType = selected.apiType || 'openai-completions';
|
||||
form.maxTokens = selected.maxTokens || 8192;
|
||||
form.contextWindow = selected.contextWindow || 128000;
|
||||
if ((selected.provider === 'custom' || selected.provider === 'vllm') && selected.model && !manualModel.value) {
|
||||
form.model = selected.model;
|
||||
if (!selected.models?.some((item) => item.id === form.model)) {
|
||||
form.model = selected.models?.[0]?.id || '';
|
||||
}
|
||||
}
|
||||
setDefaultModel();
|
||||
@@ -373,19 +364,13 @@ const setDefaultModel = () => {
|
||||
if (form.agentType !== 'openclaw') {
|
||||
return;
|
||||
}
|
||||
if (manualModel.value) {
|
||||
return;
|
||||
}
|
||||
const models = filteredModels.value;
|
||||
if (models.length > 0 && !form.model) {
|
||||
form.model = models[0].id;
|
||||
return;
|
||||
}
|
||||
if (form.provider === 'custom' || form.provider === 'vllm') {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if (selected?.model && !form.model) {
|
||||
form.model = selected.model;
|
||||
}
|
||||
if (models.length === 0) {
|
||||
form.model = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -458,7 +443,6 @@ 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();
|
||||
if (form.agentType === 'copaw') {
|
||||
|
||||
@@ -5,19 +5,11 @@
|
||||
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.manualModel" @change="handleManualModelChange">
|
||||
{{ t('aiTools.agents.manualModel') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('aiTools.model.model')" prop="model">
|
||||
<el-input v-if="form.manualModel && isOllamaProvider" v-model="ollamaModelSuffix">
|
||||
<template #prepend>ollama/</template>
|
||||
</el-input>
|
||||
<el-input v-else-if="form.manualModel" v-model="form.model" />
|
||||
<el-select v-else v-model="form.model" filterable>
|
||||
<el-select v-model="form.model" filterable>
|
||||
<el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<span class="input-help">{{ t('aiTools.agents.accountModelsHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveModel">
|
||||
@@ -28,11 +20,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
|
||||
import { pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
@@ -47,59 +39,19 @@ const { isIntl } = useGlobalStore();
|
||||
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
|
||||
|
||||
const agentId = ref(0);
|
||||
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
|
||||
const accountOptions = ref<AI.AgentAccountItem[]>([]);
|
||||
const modelOptions = ref<AI.ProviderModelInfo[]>([]);
|
||||
const provdier = ref('');
|
||||
|
||||
const getModelPrefix = (provider: string) => {
|
||||
switch (provider) {
|
||||
case 'gemini':
|
||||
return 'google';
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
};
|
||||
const modelOptions = ref<AI.AgentAccountModel[]>([]);
|
||||
|
||||
const form = reactive({
|
||||
accountId: undefined as unknown as number,
|
||||
manualModel: false,
|
||||
model: '',
|
||||
});
|
||||
|
||||
const isOllamaProvider = computed(() => provdier.value === 'ollama');
|
||||
|
||||
const ollamaModelSuffix = computed({
|
||||
get: () => {
|
||||
const model = form.model || '';
|
||||
if (model.startsWith('ollama/')) {
|
||||
return model.slice('ollama/'.length);
|
||||
}
|
||||
return model;
|
||||
},
|
||||
set: (value: string) => {
|
||||
form.model = `ollama/${value || ''}`;
|
||||
},
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
accountId: [Rules.requiredSelect],
|
||||
model: [Rules.requiredInput],
|
||||
model: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
const loadProviders = async () => {
|
||||
if (Object.keys(providerModels.value).length > 0) {
|
||||
return;
|
||||
}
|
||||
const res = await getAgentProviders();
|
||||
const data = res.data || [];
|
||||
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
|
||||
providerModels.value = filteredData.reduce((acc, item) => {
|
||||
acc[item.provider] = item.models || [];
|
||||
return acc;
|
||||
}, {} as Record<string, AI.ProviderModelInfo[]>);
|
||||
};
|
||||
|
||||
const loadAccounts = async () => {
|
||||
const res = await pageAgentAccounts({
|
||||
page: 1,
|
||||
@@ -111,66 +63,22 @@ const loadAccounts = async () => {
|
||||
accountOptions.value = isIntl.value ? items.filter((item) => !blockedProviders.has(item.provider)) : items;
|
||||
};
|
||||
|
||||
const setModelsByProvider = (provider: string) => {
|
||||
modelOptions.value = providerModels.value[provider] || [];
|
||||
const setModelOptionsByAccount = (accountId: number) => {
|
||||
const selected = accountOptions.value.find((item) => item.id === accountId);
|
||||
modelOptions.value = selected?.models || [];
|
||||
if (!modelOptions.value.some((item) => item.id === form.model)) {
|
||||
form.model = modelOptions.value[0]?.id || '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountChange = () => {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if (!selected) {
|
||||
modelOptions.value = [];
|
||||
form.model = '';
|
||||
return;
|
||||
}
|
||||
provdier.value = selected.provider;
|
||||
if (selected.provider === 'custom' || selected.provider === 'vllm') {
|
||||
form.manualModel = true;
|
||||
form.model = selected.model || form.model;
|
||||
modelOptions.value = [];
|
||||
return;
|
||||
}
|
||||
if (selected.provider === 'ollama') {
|
||||
form.manualModel = true;
|
||||
if (!form.model.startsWith('ollama/')) {
|
||||
form.model = 'ollama/';
|
||||
}
|
||||
setModelsByProvider(selected.provider);
|
||||
return;
|
||||
}
|
||||
const modelPrefix = getModelPrefix(selected.provider);
|
||||
setModelsByProvider(selected.provider);
|
||||
if (!form.manualModel && (!form.model || !form.model.startsWith(`${modelPrefix}/`))) {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualModelChange = (val: unknown) => {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if ((selected?.provider === 'custom' || selected?.provider === 'vllm') && !Boolean(val)) {
|
||||
form.manualModel = true;
|
||||
return;
|
||||
}
|
||||
if (selected?.provider === 'ollama' && Boolean(val) && !form.model.startsWith('ollama/')) {
|
||||
form.model = 'ollama/';
|
||||
}
|
||||
if (Boolean(val)) {
|
||||
return;
|
||||
}
|
||||
if (!selected) {
|
||||
form.model = '';
|
||||
return;
|
||||
}
|
||||
const modelPrefix = getModelPrefix(selected.provider);
|
||||
if (!form.model || !form.model.startsWith(`${modelPrefix}/`)) {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
setModelOptionsByAccount(form.accountId);
|
||||
};
|
||||
|
||||
const load = async (agent: AI.AgentItem) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
agentId.value = agent.id;
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
if (accountOptions.value.length === 0) {
|
||||
form.accountId = undefined as unknown as number;
|
||||
@@ -181,30 +89,8 @@ const load = async (agent: AI.AgentItem) => {
|
||||
const currentAccount =
|
||||
accountOptions.value.find((item) => item.id === agent.accountId) || accountOptions.value[0];
|
||||
form.accountId = currentAccount.id;
|
||||
provdier.value = currentAccount.provider;
|
||||
setModelsByProvider(currentAccount.provider);
|
||||
const modelPrefix = getModelPrefix(currentAccount.provider);
|
||||
const inProviderModels = modelOptions.value.some((item) => item.id === agent.model);
|
||||
form.manualModel =
|
||||
currentAccount.provider === 'custom' ||
|
||||
currentAccount.provider === 'vllm' ||
|
||||
currentAccount.provider === 'ollama' ||
|
||||
!inProviderModels;
|
||||
if (agent.model && (form.manualModel || agent.model.startsWith(`${modelPrefix}/`))) {
|
||||
form.model = agent.model;
|
||||
} else {
|
||||
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
|
||||
}
|
||||
if (
|
||||
(currentAccount.provider === 'custom' || currentAccount.provider === 'vllm') &&
|
||||
currentAccount.model &&
|
||||
!form.model
|
||||
) {
|
||||
form.model = currentAccount.model;
|
||||
}
|
||||
if (currentAccount.provider === 'ollama' && !form.model.startsWith('ollama/')) {
|
||||
form.model = 'ollama/';
|
||||
}
|
||||
form.model = agent.model || currentAccount.models?.[0]?.id || '';
|
||||
setModelOptionsByAccount(currentAccount.id);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
|
||||
<el-select v-model="form.provider" @change="handleProviderChange" :disabled="form.id > 0">
|
||||
<el-select v-model="form.provider" :disabled="form.id > 0" @change="handleProviderChange">
|
||||
<el-option
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
@@ -26,18 +26,14 @@
|
||||
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
|
||||
<el-input v-model="form.baseURL" :disabled="!editableBaseURLProviders.includes(form.provider)" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('aiTools.model.model')"
|
||||
prop="model"
|
||||
v-if="form.provider === 'custom' || form.provider === 'vllm'"
|
||||
>
|
||||
<el-input v-model="form.model" placeholder="gpt-4o-mini" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="'API ' + $t('commons.table.type')"
|
||||
prop="apiType"
|
||||
v-if="form.provider === 'custom' || form.provider === 'vllm' || form.provider === 'ollama'"
|
||||
>
|
||||
<el-alert
|
||||
:title="$t('aiTools.agents.accountCreateHelper')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
/>
|
||||
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType">
|
||||
<el-select v-model="form.apiType">
|
||||
<el-option label="openai-completions" value="openai-completions" />
|
||||
<el-option label="openai-responses" value="openai-responses" />
|
||||
@@ -48,18 +44,10 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="Max Tokens"
|
||||
prop="maxTokens"
|
||||
v-if="form.provider === 'custom' || form.provider === 'vllm'"
|
||||
>
|
||||
<el-form-item label="Max Tokens" prop="maxTokens">
|
||||
<el-input-number v-model="form.maxTokens" :min="1" :max="2000000" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="Context Window"
|
||||
prop="contextWindow"
|
||||
v-if="form.provider === 'custom' || form.provider === 'vllm'"
|
||||
>
|
||||
<el-form-item label="Context Window" prop="contextWindow">
|
||||
<el-input-number v-model="form.contextWindow" :min="1" :max="2000000" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.remark')" prop="remark">
|
||||
@@ -88,6 +76,7 @@ import { Rules } from '@/global/form-rules';
|
||||
import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai';
|
||||
import i18n from '@/lang';
|
||||
import { getAgentProviderDisplayName } from '@/utils/agent';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
@@ -105,7 +94,6 @@ const form = reactive({
|
||||
provider: '',
|
||||
name: '',
|
||||
baseURL: '',
|
||||
model: '',
|
||||
apiType: 'openai-completions',
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
@@ -124,7 +112,6 @@ const rules = reactive({
|
||||
name: [Rules.requiredInput],
|
||||
apiKey: [Rules.requiredInput],
|
||||
baseURL: [Rules.requiredInput],
|
||||
model: [Rules.requiredInput],
|
||||
apiType: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
@@ -142,7 +129,6 @@ const submit = async () => {
|
||||
baseURL: form.baseURL,
|
||||
apiKey: form.apiKey,
|
||||
rememberApiKey: form.rememberApiKey,
|
||||
model: form.model,
|
||||
apiType: form.apiType,
|
||||
maxTokens: form.maxTokens,
|
||||
contextWindow: form.contextWindow,
|
||||
@@ -156,7 +142,6 @@ const submit = async () => {
|
||||
baseURL: form.baseURL,
|
||||
apiKey: form.apiKey,
|
||||
rememberApiKey: form.rememberApiKey,
|
||||
model: form.model,
|
||||
apiType: form.apiType,
|
||||
maxTokens: form.maxTokens,
|
||||
contextWindow: form.contextWindow,
|
||||
@@ -165,6 +150,8 @@ const submit = async () => {
|
||||
}
|
||||
emit('search');
|
||||
open.value = false;
|
||||
} catch (error: any) {
|
||||
MsgError(String(error?.message || i18n.global.t('commons.res.commonError')));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -174,11 +161,15 @@ const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
loading.value = false;
|
||||
form.id = 0;
|
||||
form.model = '';
|
||||
form.provider = '';
|
||||
form.name = '';
|
||||
form.baseURL = '';
|
||||
form.apiType = 'openai-completions';
|
||||
form.maxTokens = 8192;
|
||||
form.contextWindow = 128000;
|
||||
form.apiKey = '';
|
||||
form.rememberApiKey = false;
|
||||
form.remark = '';
|
||||
form.syncAgents = false;
|
||||
};
|
||||
|
||||
@@ -189,7 +180,6 @@ interface OpenParams {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
rememberApiKey?: boolean;
|
||||
model?: string;
|
||||
apiType?: string;
|
||||
maxTokens?: number;
|
||||
contextWindow?: number;
|
||||
@@ -199,6 +189,9 @@ interface OpenParams {
|
||||
const openDrawer = async (params?: OpenParams) => {
|
||||
open.value = true;
|
||||
loading.value = false;
|
||||
if (providerOptions.value.length === 0) {
|
||||
await loadProviders();
|
||||
}
|
||||
if (params?.id) {
|
||||
form.id = params.id;
|
||||
form.provider = params.provider || '';
|
||||
@@ -206,7 +199,6 @@ const openDrawer = async (params?: OpenParams) => {
|
||||
form.baseURL = params.baseURL || '';
|
||||
form.apiKey = params.apiKey || '';
|
||||
form.rememberApiKey = params.rememberApiKey || false;
|
||||
form.model = params.model || '';
|
||||
form.apiType = params.apiType || 'openai-completions';
|
||||
form.maxTokens = params.maxTokens || 8192;
|
||||
form.contextWindow = params.contextWindow || 128000;
|
||||
@@ -219,15 +211,11 @@ const openDrawer = async (params?: OpenParams) => {
|
||||
form.baseURL = '';
|
||||
form.apiKey = '';
|
||||
form.rememberApiKey = false;
|
||||
form.model = '';
|
||||
form.apiType = 'openai-completions';
|
||||
form.maxTokens = 8192;
|
||||
form.contextWindow = 128000;
|
||||
form.remark = '';
|
||||
form.syncAgents = false;
|
||||
if (providerOptions.value.length === 0) {
|
||||
await loadProviders();
|
||||
}
|
||||
if (params?.provider) {
|
||||
form.provider = params.provider;
|
||||
} else if (providerOptions.value.length > 0) {
|
||||
@@ -259,20 +247,14 @@ const handleProviderChange = () => {
|
||||
if (form.provider === 'custom' || form.provider === 'vllm') {
|
||||
form.baseURL = '';
|
||||
form.apiType = form.apiType || 'openai-completions';
|
||||
form.maxTokens = form.maxTokens || 8192;
|
||||
form.contextWindow = form.contextWindow || 128000;
|
||||
form.model = form.model || '';
|
||||
return;
|
||||
}
|
||||
if (form.provider === 'ollama') {
|
||||
} else if (form.provider === 'ollama') {
|
||||
form.baseURL = '';
|
||||
form.apiType = 'openai-completions';
|
||||
return;
|
||||
}
|
||||
if (form.provider !== 'ollama') {
|
||||
form.baseURL = providerBaseURL.value[form.provider] || '';
|
||||
form.apiType = 'openai-responses';
|
||||
} else {
|
||||
form.baseURL = '';
|
||||
form.baseURL = providerBaseURL.value[form.provider] || '';
|
||||
if (!form.apiType) {
|
||||
form.apiType = 'openai-completions';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,11 +36,17 @@
|
||||
width="180"
|
||||
:formatter="dateFormat"
|
||||
/>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
|
||||
<fu-table-operations
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
width="180"
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<AddDialog ref="addRef" @search="search" />
|
||||
<ModelPoolDialog ref="modelPoolRef" @updated="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,6 +56,7 @@ import { deleteAgentAccount, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import RouterMenu from '@/views/ai/agents/index.vue';
|
||||
import AddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
import ModelPoolDialog from '@/views/ai/agents/model/pool/index.vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
@@ -57,6 +64,7 @@ import { getAgentProviderDisplayName } from '@/utils/agent';
|
||||
|
||||
const items = ref<AI.AgentAccountItem[]>([]);
|
||||
const addRef = ref();
|
||||
const modelPoolRef = ref();
|
||||
const searchName = ref('');
|
||||
|
||||
const buttons = [
|
||||
@@ -64,6 +72,10 @@ const buttons = [
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: AI.AgentAccountItem) => onEdit(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.modelPool'),
|
||||
click: (row: AI.AgentAccountItem) => onManageModelPool(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentAccountItem) => onDelete(row),
|
||||
@@ -105,7 +117,6 @@ const onEdit = (row: AI.AgentAccountItem) => {
|
||||
baseURL: row.baseUrl,
|
||||
apiKey: row.apiKey,
|
||||
rememberApiKey: row.rememberApiKey,
|
||||
model: row.model,
|
||||
apiType: row.apiType,
|
||||
maxTokens: row.maxTokens,
|
||||
contextWindow: row.contextWindow,
|
||||
@@ -114,6 +125,12 @@ const onEdit = (row: AI.AgentAccountItem) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onManageModelPool = (row: AI.AgentAccountItem) => {
|
||||
if (modelPoolRef.value?.open) {
|
||||
modelPoolRef.value.open(row);
|
||||
}
|
||||
};
|
||||
|
||||
const isVerificationSkipped = (provider: string) => {
|
||||
const key = (provider || '').toLowerCase();
|
||||
return key === 'custom' || key === 'vllm' || key === 'ollama' || key === 'kimi-coding';
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="headerTitle" size="large" @close="handleClose">
|
||||
<template #content>
|
||||
<div v-loading="loading">
|
||||
<el-alert
|
||||
:title="$t('aiTools.agents.modelPoolHelper')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openCreate">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<ComplexTable :data="items">
|
||||
<el-table-column :label="$t('aiTools.model.model')" prop="id" min-width="220" />
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" />
|
||||
<el-table-column label="Context Window" prop="contextWindow" min-width="140" />
|
||||
<el-table-column label="Max Tokens" prop="maxTokens" min-width="120" />
|
||||
<el-table-column :label="$t('aiTools.agents.modelInputTypes')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ (row.input || []).join(', ') || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.reasoning')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.reasoning ? 'success' : 'info'">
|
||||
{{ row.reasoning ? $t('commons.true') : $t('commons.false') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
|
||||
</ComplexTable>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
|
||||
<DrawerPro v-model="editorOpen" :header="editorTitle" size="normal" @close="handleEditorClose">
|
||||
<el-form ref="formRef" :model="form" label-position="top" :rules="rules" v-loading="saving">
|
||||
<el-form-item :label="$t('aiTools.model.model')" prop="id">
|
||||
<el-select
|
||||
v-model="form.id"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:reserve-keyword="false"
|
||||
@change="handleModelIDChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="model in providerModelOptions"
|
||||
:key="model.id"
|
||||
:label="model.name"
|
||||
:value="model.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Context Window" prop="contextWindow">
|
||||
<el-input-number v-model="form.contextWindow" :min="1" :max="2000000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Max Tokens" prop="maxTokens">
|
||||
<el-input-number v-model="form.maxTokens" :min="1" :max="2000000" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.modelInputTypes')" prop="input">
|
||||
<el-checkbox-group v-model="form.input">
|
||||
<el-checkbox label="text">Text</el-checkbox>
|
||||
<el-checkbox label="image">Image</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.reasoning')" prop="reasoning">
|
||||
<el-switch v-model="form.reasoning" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="saving" @click="editorOpen = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button :disabled="saving" type="primary" @click="submit">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
createAgentAccountModel,
|
||||
deleteAgentAccountModel,
|
||||
getAgentAccountModels,
|
||||
getAgentProviders,
|
||||
updateAgentAccountModel,
|
||||
} from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import { getAgentProviderDisplayName } from '@/utils/agent';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
|
||||
const emit = defineEmits(['updated']);
|
||||
|
||||
const open = ref(false);
|
||||
const editorOpen = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
|
||||
const items = ref<AI.AgentAccountModel[]>([]);
|
||||
const { isIntl } = useGlobalStore();
|
||||
|
||||
const account = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
provider: '',
|
||||
});
|
||||
|
||||
const form = reactive<AI.AgentAccountModel>({
|
||||
recordId: 0,
|
||||
id: '',
|
||||
name: '',
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
});
|
||||
|
||||
const headerTitle = computed(() => {
|
||||
const accountName = account.name || getAgentProviderDisplayName(account.provider, account.provider);
|
||||
return `${accountName} · ${i18n.global.t('aiTools.agents.modelPool')}`;
|
||||
});
|
||||
|
||||
const editorTitle = computed(() =>
|
||||
form.recordId ? i18n.global.t('commons.button.edit') : i18n.global.t('commons.button.add'),
|
||||
);
|
||||
|
||||
const providerModelOptions = computed(() => providerModels.value[account.provider] || []);
|
||||
|
||||
const rules = reactive({
|
||||
id: [Rules.requiredInput],
|
||||
name: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: AI.AgentAccountModel) => openEdit(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentAccountModel) => onDelete(row),
|
||||
},
|
||||
];
|
||||
|
||||
const buildModelItem = (model?: Partial<AI.AgentAccountModel>): AI.AgentAccountModel => ({
|
||||
recordId: Number(model?.recordId || 0),
|
||||
id: String(model?.id || '').trim(),
|
||||
name: String(model?.name || '').trim(),
|
||||
contextWindow: Number(model?.contextWindow || 0) || 128000,
|
||||
maxTokens: Number(model?.maxTokens || 0) || 8192,
|
||||
reasoning: Boolean(model?.reasoning),
|
||||
input: Array.from(new Set((model?.input || []).filter((value) => value === 'text' || value === 'image'))),
|
||||
});
|
||||
|
||||
const loadProviders = async () => {
|
||||
if (Object.keys(providerModels.value).length > 0) {
|
||||
return;
|
||||
}
|
||||
const res = await getAgentProviders();
|
||||
const data = res.data || [];
|
||||
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
|
||||
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
|
||||
providerModels.value = filteredData.reduce((acc, item) => {
|
||||
acc[item.provider] = item.models || [];
|
||||
return acc;
|
||||
}, {} as Record<string, AI.ProviderModelInfo[]>);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
if (!account.id) {
|
||||
items.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAgentAccountModels({ accountId: account.id });
|
||||
items.value = (res.data || []).map((item) => buildModelItem(item));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.recordId = 0;
|
||||
form.id = '';
|
||||
form.name = '';
|
||||
form.contextWindow = 128000;
|
||||
form.maxTokens = 8192;
|
||||
form.reasoning = false;
|
||||
form.input = ['text'];
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
resetForm();
|
||||
const firstModel = providerModelOptions.value[0];
|
||||
if (firstModel) {
|
||||
form.id = firstModel.id;
|
||||
form.name = firstModel.name || firstModel.id;
|
||||
form.contextWindow = firstModel.contextWindow || 128000;
|
||||
form.maxTokens = firstModel.maxTokens || 8192;
|
||||
form.reasoning = firstModel.reasoning;
|
||||
form.input = firstModel.input?.length ? [...firstModel.input] : ['text'];
|
||||
}
|
||||
editorOpen.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (row: AI.AgentAccountModel) => {
|
||||
Object.assign(form, buildModelItem(row));
|
||||
editorOpen.value = true;
|
||||
};
|
||||
|
||||
const handleModelIDChange = (value: string) => {
|
||||
form.id = String(value || '').trim();
|
||||
const matched = providerModelOptions.value.find((item) => item.id === form.id);
|
||||
if (!matched) {
|
||||
return;
|
||||
}
|
||||
if (!form.name) {
|
||||
form.name = matched.name || matched.id;
|
||||
}
|
||||
form.contextWindow = matched.contextWindow || form.contextWindow || 128000;
|
||||
form.maxTokens = matched.maxTokens || form.maxTokens || 8192;
|
||||
form.reasoning = matched.reasoning;
|
||||
form.input = matched.input?.length ? [...matched.input] : ['text'];
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!account.id || !formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
const duplicate = items.value.some((item) => item.id === form.id && item.recordId !== form.recordId);
|
||||
if (duplicate) {
|
||||
MsgError(i18n.global.t('aiTools.agents.accountModelsDuplicate'));
|
||||
return;
|
||||
}
|
||||
if (!form.input?.length) {
|
||||
form.input = ['text'];
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
if (form.recordId) {
|
||||
await updateAgentAccountModel({
|
||||
accountId: account.id,
|
||||
model: buildModelItem(form),
|
||||
});
|
||||
} else {
|
||||
await createAgentAccountModel({
|
||||
accountId: account.id,
|
||||
model: buildModelItem(form),
|
||||
});
|
||||
}
|
||||
editorOpen.value = false;
|
||||
await search();
|
||||
emit('updated');
|
||||
} catch (error: any) {
|
||||
MsgError(String(error?.message || i18n.global.t('commons.res.commonError')));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (row: AI.AgentAccountModel) => {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('commons.msg.delete', [row.name || row.id]),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
);
|
||||
await deleteAgentAccountModel({
|
||||
accountId: account.id,
|
||||
recordId: row.recordId,
|
||||
});
|
||||
await search();
|
||||
emit('updated');
|
||||
};
|
||||
|
||||
const handleEditorClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
saving.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
loading.value = false;
|
||||
account.id = 0;
|
||||
account.name = '';
|
||||
account.provider = '';
|
||||
items.value = [];
|
||||
editorOpen.value = false;
|
||||
handleEditorClose();
|
||||
};
|
||||
|
||||
const openDrawer = async (row: AI.AgentAccountItem) => {
|
||||
account.id = row.id;
|
||||
account.name = row.name;
|
||||
account.provider = row.provider;
|
||||
open.value = true;
|
||||
await loadProviders();
|
||||
await search();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open: openDrawer,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user