mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: Add an Agent Management Page (#11801)
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Create Agent
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentCreateReq true "request"
|
||||
// @Success 200 {object} dto.AgentItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents [post]
|
||||
func (b *BaseApi) CreateAgent(c *gin.Context) {
|
||||
var req dto.AgentCreateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := agentService.Create(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Agents
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchWithPage true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/search [post]
|
||||
func (b *BaseApi) PageAgents(c *gin.Context) {
|
||||
var req dto.SearchWithPage
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, list, err := agentService.Page(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/delete [post]
|
||||
func (b *BaseApi) DeleteAgent(c *gin.Context) {
|
||||
var req dto.AgentDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.Delete(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Providers
|
||||
// @Success 200 {object} []dto.ProviderInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/providers [get]
|
||||
func (b *BaseApi) GetAgentProviders(c *gin.Context) {
|
||||
list, err := agentService.GetProviders()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Create Agent account
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountCreateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts [post]
|
||||
func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
|
||||
var req dto.AgentAccountCreateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.CreateAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent account
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/update [post]
|
||||
func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
|
||||
var req dto.AgentAccountUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Agent accounts
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/search [post]
|
||||
func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
|
||||
var req dto.AgentAccountSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, list, err := agentService.PageAccounts(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Verify Agent account
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountVerifyReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/verify [post]
|
||||
func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
|
||||
var req dto.AgentAccountVerifyReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.VerifyAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent account
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentAccountDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/accounts/delete [post]
|
||||
func (b *BaseApi) DeleteAgentAccount(c *gin.Context) {
|
||||
var req dto.AgentAccountDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
install, err := appService.Install(req)
|
||||
install, err := appService.Install(req, true)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -20,6 +20,7 @@ var (
|
||||
aiToolService = service.NewIAIToolService()
|
||||
mcpServerService = service.NewIMcpServerService()
|
||||
tensorrtLLMService = service.NewITensorRTLLMService()
|
||||
agentService = service.NewIAgentService()
|
||||
|
||||
containerService = service.NewIContainerService()
|
||||
composeTemplateService = service.NewIComposeTemplateService()
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
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"`
|
||||
AccountID uint `json:"accountId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Token string `json:"token"`
|
||||
TaskID string `json:"taskID"`
|
||||
Advanced bool `json:"advanced"`
|
||||
ContainerName string `json:"containerName"`
|
||||
AllowPort bool `json:"allowPort"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
CpuQuota float64 `json:"cpuQuota"`
|
||||
MemoryLimit float64 `json:"memoryLimit"`
|
||||
MemoryUnit string `json:"memoryUnit"`
|
||||
PullImage bool `json:"pullImage"`
|
||||
EditCompose bool `json:"editCompose"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
Container string `json:"containerName"`
|
||||
WebUIPort int `json:"webUIPort"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
Path string `json:"path"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type AgentDeleteReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
TaskID string `json:"taskID"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
}
|
||||
|
||||
type AgentAccountCreateReq struct {
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
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"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
}
|
||||
|
||||
type AgentAccountVerifyReq struct {
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
}
|
||||
|
||||
type AgentAccountDeleteReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentAccountSearch struct {
|
||||
PageInfo
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Verified bool `json:"verified"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type ProviderModelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ProviderInfo struct {
|
||||
Provider string `json:"provider"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Models []ProviderModelInfo `json:"models"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
type Agent struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;unique"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
AccountID uint `json:"accountId"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type AgentAccount struct {
|
||||
BaseModel
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Verified bool `json:"verified"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func (AgentAccount) TableName() string {
|
||||
return "agent_provider_accounts"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
)
|
||||
|
||||
type AgentRepo struct{}
|
||||
|
||||
type IAgentRepo interface {
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.Agent, error)
|
||||
GetFirst(opts ...DBOption) (*model.Agent, error)
|
||||
Create(agent *model.Agent) error
|
||||
Save(agent *model.Agent) error
|
||||
DeleteByID(id uint) error
|
||||
DeleteByAppInstallID(appInstallID uint) error
|
||||
DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error
|
||||
List(opts ...DBOption) ([]model.Agent, error)
|
||||
}
|
||||
|
||||
func NewIAgentRepo() IAgentRepo {
|
||||
return &AgentRepo{}
|
||||
}
|
||||
|
||||
func (a AgentRepo) Page(page, size int, opts ...DBOption) (int64, []model.Agent, error) {
|
||||
var agents []model.Agent
|
||||
db := getDb(opts...).Model(&model.Agent{})
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&agents).Error
|
||||
return count, agents, err
|
||||
}
|
||||
|
||||
func (a AgentRepo) GetFirst(opts ...DBOption) (*model.Agent, error) {
|
||||
var agent model.Agent
|
||||
if err := getDb(opts...).First(&agent).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
func (a AgentRepo) Create(agent *model.Agent) error {
|
||||
return getDb().Create(agent).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) Save(agent *model.Agent) error {
|
||||
return getDb().Save(agent).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) DeleteByID(id uint) error {
|
||||
return getDb().Delete(&model.Agent{}, id).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) DeleteByAppInstallID(appInstallID uint) error {
|
||||
if appInstallID == 0 {
|
||||
return nil
|
||||
}
|
||||
return getDb().Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error {
|
||||
if appInstallID == 0 {
|
||||
return nil
|
||||
}
|
||||
return getTx(ctx).Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) List(opts ...DBOption) ([]model.Agent, error) {
|
||||
var agents []model.Agent
|
||||
if err := getDb(opts...).Find(&agents).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agents, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package repo
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
|
||||
type AgentAccountRepo struct{}
|
||||
|
||||
type IAgentAccountRepo interface {
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error)
|
||||
GetFirst(opts ...DBOption) (*model.AgentAccount, error)
|
||||
Create(account *model.AgentAccount) error
|
||||
Save(account *model.AgentAccount) error
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
return &AgentAccountRepo{}
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error) {
|
||||
var accounts []model.AgentAccount
|
||||
db := getDb(opts...).Model(&model.AgentAccount{})
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error
|
||||
return count, accounts, err
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) GetFirst(opts ...DBOption) (*model.AgentAccount, error) {
|
||||
var account model.AgentAccount
|
||||
if err := getDb(opts...).First(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) Create(account *model.AgentAccount) error {
|
||||
return getDb().Create(account).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) Save(account *model.AgentAccount) error {
|
||||
return getDb().Save(account).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) DeleteByID(id uint) error {
|
||||
return getDb().Delete(&model.AgentAccount{}, id).Error
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
||||
var accounts []model.AgentAccount
|
||||
if err := getDb(opts...).Find(&accounts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
@@ -72,6 +72,33 @@ func WithByDetailName(detailName string) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithByProvider(provider string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(provider) == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("provider = ?", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func WithByModel(model string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(model) == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("model = ?", model)
|
||||
}
|
||||
}
|
||||
|
||||
func WithByAccountID(accountID uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if accountID == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("account_id = ?", accountID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithByType(tp string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("`type` = ?", tp)
|
||||
|
||||
@@ -0,0 +1,820 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
type AgentService struct{}
|
||||
|
||||
type IAgentService interface {
|
||||
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
||||
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
|
||||
Delete(req dto.AgentDeleteReq) error
|
||||
GetProviders() ([]dto.ProviderInfo, error)
|
||||
CreateAccount(req dto.AgentAccountCreateReq) error
|
||||
UpdateAccount(req dto.AgentAccountUpdateReq) error
|
||||
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
||||
VerifyAccount(req dto.AgentAccountVerifyReq) error
|
||||
DeleteAccount(req dto.AgentAccountDeleteReq) error
|
||||
}
|
||||
|
||||
func NewIAgentService() IAgentService {
|
||||
return &AgentService{}
|
||||
}
|
||||
|
||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
provider := strings.ToLower(strings.TrimSpace(req.Provider))
|
||||
if !isSupportedAgentProvider(provider) {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "provider", nil)
|
||||
}
|
||||
if req.AccountID == 0 {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "accountId", nil)
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !account.Verified {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "account", nil)
|
||||
}
|
||||
if account.Provider != "" && provider != "" && account.Provider != provider {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "provider", nil)
|
||||
}
|
||||
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.WithDetail("ErrInvalidParams", "baseURL", nil)
|
||||
}
|
||||
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "apiKey", nil)
|
||||
}
|
||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPortExist(req.BridgePort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 {
|
||||
return nil, buserr.New("ErrNameIsExist")
|
||||
}
|
||||
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
|
||||
return nil, buserr.New("ErrNameIsExist")
|
||||
}
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenclaw))
|
||||
if err != nil || app.ID == 0 {
|
||||
return nil, buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(req.AppVersion))
|
||||
if err != nil || detail.ID == 0 {
|
||||
return nil, buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(req.Token)
|
||||
if token == "" {
|
||||
token = randomToken()
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"PROVIDER": provider,
|
||||
"MODEL": req.Model,
|
||||
"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: "",
|
||||
}
|
||||
|
||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "dockerCompose", nil)
|
||||
}
|
||||
installReq := request.AppInstallCreate{
|
||||
AppDetailId: detail.ID,
|
||||
Name: req.Name,
|
||||
Params: params,
|
||||
TaskID: req.TaskID,
|
||||
AppContainerConfig: request.AppContainerConfig{
|
||||
Advanced: req.Advanced,
|
||||
ContainerName: req.ContainerName,
|
||||
AllowPort: req.AllowPort,
|
||||
SpecifyIP: req.SpecifyIP,
|
||||
RestartPolicy: req.RestartPolicy,
|
||||
CpuQuota: req.CpuQuota,
|
||||
MemoryLimit: req.MemoryLimit,
|
||||
MemoryUnit: req.MemoryUnit,
|
||||
PullImage: req.PullImage,
|
||||
EditCompose: req.EditCompose,
|
||||
DockerCompose: req.DockerCompose,
|
||||
},
|
||||
}
|
||||
appInstall, err := NewIAppService().Install(installReq, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configPath := path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
|
||||
agent := &model.Agent{
|
||||
Name: req.Name,
|
||||
Provider: provider,
|
||||
Model: req.Model,
|
||||
BaseURL: baseURL,
|
||||
APIKey: account.APIKey,
|
||||
Token: token,
|
||||
Status: appInstall.Status,
|
||||
Message: appInstall.Message,
|
||||
AppInstallID: appInstall.ID,
|
||||
AccountID: account.ID,
|
||||
ConfigPath: configPath,
|
||||
}
|
||||
if err := agentRepo.Create(agent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go a.writeConfigWithRetry(appInstall, provider, req.Model, baseURL, req.APIKey, token, agent.ID)
|
||||
|
||||
item := buildAgentItem(agent, appInstall, nil)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) {
|
||||
var opts []repo.DBOption
|
||||
if strings.TrimSpace(req.Info) != "" {
|
||||
opts = append(opts, repo.WithByLikeName(req.Info))
|
||||
}
|
||||
count, list, err := agentRepo.Page(req.Page, req.PageSize, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
items := make([]dto.AgentItem, 0, len(list))
|
||||
for _, item := range list {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
|
||||
envMap := readInstallEnv(appInstall.Env)
|
||||
items = append(items, buildAgentItem(&item, &appInstall, envMap))
|
||||
}
|
||||
return count, items, nil
|
||||
}
|
||||
|
||||
func (a AgentService) Delete(req dto.AgentDeleteReq) error {
|
||||
if req.ID == 0 {
|
||||
return buserr.WithDetail("ErrInvalidParams", "id", nil)
|
||||
}
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AppInstallID == 0 {
|
||||
return agentRepo.DeleteByID(agent.ID)
|
||||
}
|
||||
operate := request.AppInstalledOperate{
|
||||
InstallId: agent.AppInstallID,
|
||||
Operate: constant.Delete,
|
||||
TaskID: req.TaskID,
|
||||
ForceDelete: req.ForceDelete,
|
||||
}
|
||||
if err := NewIAppInstalledService().Operate(operate); err != nil {
|
||||
return err
|
||||
}
|
||||
go a.waitAndDeleteAgent(agent.ID, agent.AppInstallID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
||||
definitions := providerDefinitions()
|
||||
providers := make([]dto.ProviderInfo, 0, len(definitions))
|
||||
for key, def := range definitions {
|
||||
providers = append(providers, dto.ProviderInfo{
|
||||
Provider: key,
|
||||
BaseURL: def.BaseURL,
|
||||
Models: def.Models,
|
||||
})
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
provider := strings.ToLower(strings.TrimSpace(req.Provider))
|
||||
if !isSupportedAgentProvider(provider) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "provider", nil)
|
||||
}
|
||||
apiKey := strings.TrimSpace(req.APIKey)
|
||||
if apiKey == "" {
|
||||
return buserr.WithDetail("ErrInvalidParams", "apiKey", nil)
|
||||
}
|
||||
baseURL := strings.TrimSpace(req.BaseURL)
|
||||
if provider != "ollama" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
if provider == "ollama" && baseURL == "" {
|
||||
return buserr.WithDetail("ErrInvalidParams", "baseURL", nil)
|
||||
}
|
||||
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
account := &model.AgentAccount{
|
||||
Provider: provider,
|
||||
Name: req.Name,
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
Verified: true,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
return agentAccountRepo.Create(account)
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := strings.ToLower(strings.TrimSpace(account.Provider))
|
||||
baseURL := strings.TrimSpace(req.BaseURL)
|
||||
if provider != "ollama" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
if provider == "ollama" && baseURL == "" {
|
||||
return buserr.WithDetail("ErrInvalidParams", "baseURL", nil)
|
||||
}
|
||||
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
account.Name = req.Name
|
||||
account.APIKey = req.APIKey
|
||||
account.BaseURL = baseURL
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.SyncAgents {
|
||||
if err := a.syncAgentsByAccount(account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) {
|
||||
var opts []repo.DBOption
|
||||
if strings.TrimSpace(req.Provider) != "" {
|
||||
opts = append(opts, repo.WithByProvider(req.Provider))
|
||||
}
|
||||
if strings.TrimSpace(req.Name) != "" {
|
||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||
}
|
||||
count, list, err := agentAccountRepo.Page(req.Page, req.PageSize, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
items = append(items, dto.AgentAccountInfo{
|
||||
ID: item.ID,
|
||||
Provider: item.Provider,
|
||||
Name: item.Name,
|
||||
APIKey: item.APIKey,
|
||||
BaseURL: item.BaseURL,
|
||||
Verified: item.Verified,
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
return count, items, nil
|
||||
}
|
||||
|
||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||
provider := strings.ToLower(strings.TrimSpace(req.Provider))
|
||||
if !isSupportedAgentProvider(provider) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "provider", nil)
|
||||
}
|
||||
apiKey := strings.TrimSpace(req.APIKey)
|
||||
if apiKey == "" {
|
||||
return buserr.WithDetail("ErrInvalidParams", "apiKey", nil)
|
||||
}
|
||||
baseURL := strings.TrimSpace(req.BaseURL)
|
||||
if baseURL == "" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
if provider == "ollama" && baseURL == "" {
|
||||
return buserr.WithDetail("ErrInvalidParams", "baseURL", nil)
|
||||
}
|
||||
if provider == "ollama" {
|
||||
return nil
|
||||
}
|
||||
return verifyProvider(provider, baseURL, apiKey)
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
if req.ID == 0 {
|
||||
return buserr.WithDetail("ErrInvalidParams", "id", nil)
|
||||
}
|
||||
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
return agentAccountRepo.DeleteByID(req.ID)
|
||||
}
|
||||
|
||||
func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
agents, err := agentRepo.List(repo.WithByAccountID(account.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
baseURL := account.BaseURL
|
||||
if account.Provider != "ollama" {
|
||||
if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
for _, agent := range agents {
|
||||
confDir := ""
|
||||
if agent.ConfigPath != "" {
|
||||
confDir = path.Dir(agent.ConfigPath)
|
||||
} else if agent.AppInstallID > 0 {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
|
||||
if err == nil {
|
||||
confDir = path.Join(install.GetPath(), "data", "conf")
|
||||
}
|
||||
}
|
||||
if confDir == "" {
|
||||
continue
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account.Provider, agent.Model, baseURL, account.APIKey, agent.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.BaseURL = baseURL
|
||||
agent.APIKey = account.APIKey
|
||||
agent.Provider = account.Provider
|
||||
_ = agentRepo.Save(&agent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyProvider(provider, baseURL, apiKey string) error {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
reqURL, headers := buildVerifyRequest(provider, baseURL, apiKey)
|
||||
request, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range headers {
|
||||
request.Header.Set(key, value)
|
||||
}
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("verify failed: %d", resp.StatusCode), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
||||
item := dto.AgentItem{
|
||||
ID: agent.ID,
|
||||
Name: agent.Name,
|
||||
Provider: agent.Provider,
|
||||
Model: agent.Model,
|
||||
BaseURL: agent.BaseURL,
|
||||
APIKey: maskKey(agent.APIKey),
|
||||
Token: agent.Token,
|
||||
Status: agent.Status,
|
||||
Message: agent.Message,
|
||||
AppInstallID: agent.AppInstallID,
|
||||
ConfigPath: agent.ConfigPath,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
}
|
||||
if appInstall != nil && appInstall.ID > 0 {
|
||||
item.Container = appInstall.ContainerName
|
||||
item.AppVersion = appInstall.Version
|
||||
item.WebUIPort = appInstall.HttpPort
|
||||
item.Path = appInstall.GetPath()
|
||||
item.Status = appInstall.Status
|
||||
item.Message = appInstall.Message
|
||||
if envMap != nil {
|
||||
if bridge, ok := envMap["PANEL_APP_PORT_BRIDGE"]; ok {
|
||||
item.BridgePort = toInt(bridge)
|
||||
}
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) {
|
||||
if appInstallID == 0 {
|
||||
_ = agentRepo.DeleteByID(agentID)
|
||||
return
|
||||
}
|
||||
for i := 0; i < 180; i++ {
|
||||
_, err := appInstallRepo.GetFirst(repo.WithByID(appInstallID))
|
||||
if err != nil {
|
||||
_ = agentRepo.DeleteByID(agentID)
|
||||
return
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, baseURL, apiKey, token string, agentID uint) {
|
||||
if appInstall == nil {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
composePath := appInstall.GetComposePath()
|
||||
for i := 0; i < 60; i++ {
|
||||
if fileOp.Stat(composePath) {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token); err != nil {
|
||||
global.LOG.Errorf("write openclaw config failed: %v", err)
|
||||
agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID))
|
||||
if errGet == nil && agent != nil {
|
||||
agent.Message = err.Error()
|
||||
agent.Status = constant.StatusError
|
||||
_ = agentRepo.Save(agent)
|
||||
}
|
||||
return
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
for i := 0; i < 60; i++ {
|
||||
if fileOp.Stat(dataDir) {
|
||||
if err := fileOp.ChownR(dataDir, "1000", "1000", true); err != nil {
|
||||
global.LOG.Errorf("chown data dir failed: %v", err)
|
||||
agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID))
|
||||
if errGet == nil && agent != nil {
|
||||
agent.Message = err.Error()
|
||||
agent.Status = constant.StatusError
|
||||
_ = agentRepo.Save(agent)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
type openclawConfig struct {
|
||||
Gateway gatewayConfig `json:"gateway"`
|
||||
Agents agentsConfig `json:"agents"`
|
||||
Models *modelsConfig `json:"models,omitempty"`
|
||||
}
|
||||
|
||||
type gatewayConfig struct {
|
||||
Mode string `json:"mode"`
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
Auth gatewayAuth `json:"auth"`
|
||||
ControlUi gatewayControlUi `json:"controlUi"`
|
||||
}
|
||||
|
||||
type gatewayControlUi struct {
|
||||
AllowInsecureAuth bool `json:"allowInsecureAuth"`
|
||||
}
|
||||
|
||||
type gatewayAuth struct {
|
||||
Mode string `json:"mode"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type agentsConfig struct {
|
||||
Defaults agentDefaults `json:"defaults"`
|
||||
}
|
||||
|
||||
type agentDefaults struct {
|
||||
Model modelRef `json:"model"`
|
||||
}
|
||||
|
||||
type modelRef struct {
|
||||
Primary string `json:"primary"`
|
||||
}
|
||||
|
||||
type modelsConfig struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Providers map[string]modelProvider `json:"providers,omitempty"`
|
||||
}
|
||||
|
||||
type modelProvider struct {
|
||||
ApiKey string `json:"apiKey,omitempty"`
|
||||
BaseUrl string `json:"baseUrl,omitempty"`
|
||||
Api string `json:"api,omitempty"`
|
||||
Models []modelEntry `json:"models,omitempty"`
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
Input []string `json:"input"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token string) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
if strings.TrimSpace(modelName) == "" {
|
||||
return fmt.Errorf("model is required")
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return fmt.Errorf("gateway token is required")
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(confDir) {
|
||||
if err := fileOp.CreateDir(confDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cfg := openclawConfig{
|
||||
Gateway: gatewayConfig{
|
||||
Mode: "local",
|
||||
Bind: "lan",
|
||||
Port: 18789,
|
||||
Auth: gatewayAuth{
|
||||
Mode: "token",
|
||||
Token: token,
|
||||
},
|
||||
ControlUi: gatewayControlUi{
|
||||
AllowInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
Agents: agentsConfig{
|
||||
Defaults: agentDefaults{
|
||||
Model: modelRef{Primary: modelName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
if provider == "deepseek" {
|
||||
base := baseURL
|
||||
if base == "" {
|
||||
base = "https://api.deepseek.com/v1"
|
||||
}
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
"deepseek": {
|
||||
ApiKey: "${DEEPSEEK_API_KEY}",
|
||||
BaseUrl: base,
|
||||
Api: "openai-completions",
|
||||
Models: []modelEntry{
|
||||
{
|
||||
ID: "deepseek-chat",
|
||||
Name: "DeepSeek Chat",
|
||||
Reasoning: false,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 128000,
|
||||
MaxTokens: 8192,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if provider == "ollama" {
|
||||
modelID := modelName
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
cfg.Models = &modelsConfig{
|
||||
Mode: "merge",
|
||||
Providers: map[string]modelProvider{
|
||||
"ollama": {
|
||||
ApiKey: "ollama",
|
||||
BaseUrl: baseURL,
|
||||
Api: "openai-responses",
|
||||
Models: []modelEntry{
|
||||
{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: true,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: 160000,
|
||||
MaxTokens: 8192,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configPath := path.Join(confDir, "openclaw.json")
|
||||
if err := fileOp.SaveFile(configPath, string(payload), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envPath := path.Join(confDir, ".env")
|
||||
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
|
||||
if envKey := providerEnvKey(provider); envKey != "" && strings.TrimSpace(apiKey) != "" {
|
||||
lines = append(lines, fmt.Sprintf("%s=%s", envKey, apiKey))
|
||||
}
|
||||
content := strings.Join(lines, "\n") + "\n"
|
||||
return fileOp.SaveFile(envPath, content, 0600)
|
||||
}
|
||||
|
||||
func providerEnvKey(provider string) string {
|
||||
switch provider {
|
||||
case "openai":
|
||||
return "OPENAI_API_KEY"
|
||||
case "anthropic":
|
||||
return "ANTHROPIC_API_KEY"
|
||||
case "gemini":
|
||||
return "GEMINI_API_KEY"
|
||||
case "minimax":
|
||||
return "MINIMAX_API_KEY"
|
||||
case "deepseek":
|
||||
return "DEEPSEEK_API_KEY"
|
||||
case "qwen":
|
||||
return "QWEN_API_KEY"
|
||||
case "ollama":
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type providerDefinition struct {
|
||||
BaseURL string
|
||||
Models []dto.ProviderModelInfo
|
||||
}
|
||||
|
||||
func providerDefinitions() map[string]providerDefinition {
|
||||
return map[string]providerDefinition{
|
||||
"openai": {
|
||||
BaseURL: "https://api.openai.com/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "openai/codex-mini-latest", Name: "Codex Mini"},
|
||||
{ID: "openai/gpt-4.1", Name: "GPT-4.1"},
|
||||
{ID: "openai/gpt-4o", Name: "GPT-4o"},
|
||||
{ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"},
|
||||
{ID: "openai/gpt-5", Name: "GPT-5"},
|
||||
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"},
|
||||
},
|
||||
},
|
||||
"anthropic": {
|
||||
BaseURL: "https://api.anthropic.com",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
|
||||
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"},
|
||||
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"},
|
||||
},
|
||||
},
|
||||
"gemini": {
|
||||
BaseURL: "https://generativelanguage.googleapis.com",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "google/gemini-1.5-flash", Name: "Gemini 1.5 Flash"},
|
||||
{ID: "google/gemini-1.5-pro", Name: "Gemini 1.5 Pro"},
|
||||
{ID: "google/gemini-2.0-flash", Name: "Gemini 2.0 Flash"},
|
||||
{ID: "google/gemini-2.5-flash", Name: "Gemini 2.5 Flash"},
|
||||
{ID: "google/gemini-2.5-pro", Name: "Gemini 2.5 Pro"},
|
||||
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"},
|
||||
},
|
||||
},
|
||||
"minimax": {
|
||||
BaseURL: "https://api.minimax.chat/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "minimax/Minimax-M2.1", Name: "Minimax M2.1"},
|
||||
},
|
||||
},
|
||||
"deepseek": {
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Models: []dto.ProviderModelInfo{
|
||||
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
|
||||
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"},
|
||||
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"},
|
||||
},
|
||||
},
|
||||
"ollama": {
|
||||
BaseURL: "",
|
||||
Models: []dto.ProviderModelInfo{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func providerDefaultBaseURL(provider string) (string, bool) {
|
||||
defs := providerDefinitions()
|
||||
if def, ok := defs[provider]; ok {
|
||||
if def.BaseURL == "" {
|
||||
return "", false
|
||||
}
|
||||
return def.BaseURL, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isSupportedAgentProvider(provider string) bool {
|
||||
_, ok := providerDefinitions()[provider]
|
||||
return ok
|
||||
}
|
||||
|
||||
func buildVerifyRequest(provider, baseURL, apiKey string) (string, map[string]string) {
|
||||
headers := map[string]string{}
|
||||
base := strings.TrimRight(baseURL, "/")
|
||||
switch provider {
|
||||
case "anthropic":
|
||||
headers["x-api-key"] = apiKey
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
if strings.Contains(base, "/v1") {
|
||||
return base + "/models", headers
|
||||
}
|
||||
return base + "/v1/models", headers
|
||||
case "gemini":
|
||||
if strings.Contains(base, "/v1beta") {
|
||||
return fmt.Sprintf("%s/models?key=%s", base, apiKey), headers
|
||||
}
|
||||
return fmt.Sprintf("%s/v1beta/models?key=%s", base, apiKey), headers
|
||||
default:
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
if strings.Contains(base, "/v1") {
|
||||
return base + "/models", headers
|
||||
}
|
||||
return base + "/v1/models", headers
|
||||
}
|
||||
}
|
||||
|
||||
func readInstallEnv(envStr string) map[string]interface{} {
|
||||
if strings.TrimSpace(envStr) == "" {
|
||||
return nil
|
||||
}
|
||||
data := map[string]interface{}{}
|
||||
if err := json.Unmarshal([]byte(envStr), &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func maskKey(value string) string {
|
||||
trim := strings.TrimSpace(value)
|
||||
if len(trim) <= 6 {
|
||||
return trim
|
||||
}
|
||||
return fmt.Sprintf("%s****%s", trim[:3], trim[len(trim)-3:])
|
||||
}
|
||||
|
||||
func toInt(value interface{}) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
if v == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, _ := strconv.Atoi(v)
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func randomToken() string {
|
||||
bytes := make([]byte, 24)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ type IAppService interface {
|
||||
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
|
||||
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
|
||||
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
|
||||
Install(req request.AppInstallCreate) (*model.AppInstall, error)
|
||||
Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error)
|
||||
SyncAppListFromRemote(taskID string) error
|
||||
GetAppUpdate() (*response.AppUpdateRes, error)
|
||||
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
|
||||
@@ -339,7 +339,7 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.AppInstall, err error) {
|
||||
func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (appInstall *model.AppInstall, err error) {
|
||||
if err = docker.CreateDefaultDockerNetwork(); err != nil {
|
||||
err = buserr.WithDetail("Err1PanelNetworkFailed", err.Error(), nil)
|
||||
return
|
||||
@@ -533,8 +533,10 @@ func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.App
|
||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runScript(t, appInstall, "init"); err != nil {
|
||||
return err
|
||||
if executeScript {
|
||||
if err = runScript(t, appInstall, "init"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if app.Key == "openresty" {
|
||||
if err = handleSiteDir(app, appDetail, req, t); err != nil {
|
||||
|
||||
@@ -392,6 +392,10 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
||||
return err
|
||||
}
|
||||
appKey := install.App.Key
|
||||
if appKey == constant.AppOpenclaw {
|
||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||
}
|
||||
|
||||
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
|
||||
if len(resources) > 0 {
|
||||
|
||||
@@ -12,9 +12,11 @@ var (
|
||||
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
|
||||
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
|
||||
|
||||
aiRepo = repo.NewIAiRepo()
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
|
||||
aiRepo = repo.NewIAiRepo()
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
|
||||
agentRepo = repo.NewIAgentRepo()
|
||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
|
||||
@@ -383,7 +383,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
req.AppDetailId = create.AppInstall.AppDetailId
|
||||
req.Params = create.AppInstall.Params
|
||||
req.AppContainerConfig = create.AppInstall.AppContainerConfig
|
||||
install, err = NewIAppService().Install(req)
|
||||
install, err = NewIAppService().Install(req, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
AppTakeDown = "TakeDown"
|
||||
|
||||
AppOpenresty = "openresty"
|
||||
AppOpenclaw = "openclaw"
|
||||
AppMysql = "mysql"
|
||||
AppMariaDB = "mariadb"
|
||||
AppPostgresql = "postgresql"
|
||||
|
||||
@@ -63,6 +63,8 @@ func InitAgentDB() {
|
||||
migrations.UpdateApp,
|
||||
migrations.AddCronjobArgs,
|
||||
migrations.AddWebsiteAcmeAccountColumn,
|
||||
migrations.AddAgentTables,
|
||||
migrations.MigrateOpenclawAgents,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
@@ -821,3 +822,135 @@ var AddWebsiteAcmeAccountColumn = &gormigrate.Migration{
|
||||
return tx.AutoMigrate(&model.WebsiteAcmeAccount{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddAgentTables = &gormigrate.Migration{
|
||||
ID: "20260205-add-agent-tables",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.Agent{},
|
||||
&model.AgentAccount{},
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
var MigrateOpenclawAgents = &gormigrate.Migration{
|
||||
ID: "20260207-migrate-openclaw-agents",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var installs []model.AppInstall
|
||||
if err := tx.Preload("App").Find(&installs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, install := range installs {
|
||||
appKey := install.App.Key
|
||||
if appKey == "" || install.App.Resource == "" {
|
||||
var app model.App
|
||||
if err := tx.First(&app, install.AppId).Error; err == nil {
|
||||
install.App = app
|
||||
appKey = app.Key
|
||||
}
|
||||
}
|
||||
if appKey != constant.AppOpenclaw {
|
||||
continue
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&model.Agent{}).Where("app_install_id = ?", install.ID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
envMap := map[string]interface{}{}
|
||||
if strings.TrimSpace(install.Env) != "" {
|
||||
_ = json.Unmarshal([]byte(install.Env), &envMap)
|
||||
}
|
||||
provider := strings.ToLower(getEnvStr(envMap, "PROVIDER"))
|
||||
if provider == "" {
|
||||
continue
|
||||
}
|
||||
modelName := getEnvStr(envMap, "MODEL")
|
||||
baseURL := getEnvStr(envMap, "BASE_URL")
|
||||
apiKey := getEnvStr(envMap, "API_KEY")
|
||||
token := getEnvStr(envMap, "OPENCLAW_GATEWAY_TOKEN")
|
||||
if provider != "ollama" {
|
||||
if baseURL == "" {
|
||||
if defaultURL, ok := defaultBaseURL(provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
}
|
||||
if provider == "ollama" && baseURL == "" {
|
||||
continue
|
||||
}
|
||||
var account model.AgentAccount
|
||||
err := tx.Where("provider = ? AND api_key = ? AND base_url = ?", provider, apiKey, baseURL).First(&account).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
account = model.AgentAccount{
|
||||
Provider: provider,
|
||||
Name: install.Name,
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
Verified: apiKey != "" || provider == "ollama",
|
||||
}
|
||||
if err := tx.Create(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json")
|
||||
agent := model.Agent{
|
||||
Name: install.Name,
|
||||
Provider: provider,
|
||||
Model: modelName,
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
Token: token,
|
||||
Status: install.Status,
|
||||
Message: install.Message,
|
||||
AppInstallID: install.ID,
|
||||
AccountID: account.ID,
|
||||
ConfigPath: configPath,
|
||||
}
|
||||
if err := tx.Create(&agent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func getEnvStr(envMap map[string]interface{}, key string) string {
|
||||
if envMap == nil {
|
||||
return ""
|
||||
}
|
||||
if value, ok := envMap[key]; ok {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func defaultBaseURL(provider string) (string, bool) {
|
||||
switch provider {
|
||||
case "openai":
|
||||
return "https://api.openai.com/v1", true
|
||||
case "anthropic":
|
||||
return "https://api.anthropic.com", true
|
||||
case "gemini":
|
||||
return "https://generativelanguage.googleapis.com", true
|
||||
case "minimax":
|
||||
return "https://api.minimax.chat/v1", true
|
||||
case "deepseek":
|
||||
return "https://api.deepseek.com/v1", true
|
||||
case "qwen":
|
||||
return "https://dashscope.aliyuncs.com/compatible-mode/v1", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +39,15 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/tensorrt/update", baseApi.UpdateTensorRTLLM)
|
||||
aiToolsRouter.POST("/tensorrt/delete", baseApi.DeleteTensorRTLLM)
|
||||
aiToolsRouter.POST("/tensorrt/operate", baseApi.OperateTensorRTLLM)
|
||||
|
||||
aiToolsRouter.POST("/agents", baseApi.CreateAgent)
|
||||
aiToolsRouter.POST("/agents/search", baseApi.PageAgents)
|
||||
aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent)
|
||||
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
|
||||
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/search", baseApi.PageAgentAccounts)
|
||||
aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount)
|
||||
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func LoadMenus() string {
|
||||
}},
|
||||
{ID: "4", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model", Sort: 400,
|
||||
Children: []dto.ShowMenu{
|
||||
{ID: "44", Disabled: false, Title: "aiTools.agents.agents", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "OllamaModel", Path: "/ai/model", Sort: 100},
|
||||
{ID: "42", Disabled: false, Title: "menu.mcp", IsShow: true, Label: "MCPServer", Path: "/ai/mcp", Sort: 200},
|
||||
{ID: "43", Disabled: false, Title: "aiTools.gpu.gpu", IsShow: true, Label: "GPU", Path: "/ai/gpu", Sort: 300},
|
||||
@@ -68,6 +69,7 @@ func MenuSort() []dto.MenuLabelSort {
|
||||
{Label: "SSL", Sort: 200},
|
||||
{Label: "PHP", Sort: 300},
|
||||
{Label: "AI-Menu", Sort: 400},
|
||||
{Label: "Agents", Sort: 50},
|
||||
{Label: "OllamaModel", Sort: 100},
|
||||
{Label: "MCPServer", Sort: 200},
|
||||
{Label: "GPU", Sort: 300},
|
||||
|
||||
@@ -23,6 +23,7 @@ func Init() {
|
||||
migrations.DeleteXpackHideMenu,
|
||||
migrations.AddCronjobGroup,
|
||||
migrations.AddDiskMenu,
|
||||
migrations.AddAgentsMenu,
|
||||
migrations.AddSimpleNodeGroup,
|
||||
migrations.AddUpgradeBackupCopies,
|
||||
migrations.AddScriptSync,
|
||||
|
||||
@@ -556,6 +556,20 @@ var AddDiskMenu = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var AddAgentsMenu = &gormigrate.Migration{
|
||||
ID: "20260204-add-agents-menu",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return helper.AddMenu(dto.ShowMenu{
|
||||
ID: "44",
|
||||
Disabled: false,
|
||||
Title: "aiTools.agents.agents",
|
||||
IsShow: true,
|
||||
Label: "Agents",
|
||||
Path: "/ai/agents/agent",
|
||||
}, "4", tx)
|
||||
},
|
||||
}
|
||||
|
||||
var AddSimpleNodeGroup = &gormigrate.Migration{
|
||||
ID: "20250916-add-simple-node-group",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
@@ -235,4 +235,112 @@ export namespace AI {
|
||||
id: number;
|
||||
operate: string;
|
||||
}
|
||||
|
||||
export interface AgentCreateReq {
|
||||
name: string;
|
||||
appVersion: string;
|
||||
webUIPort: number;
|
||||
bridgePort: number;
|
||||
provider: string;
|
||||
model: string;
|
||||
accountId: number;
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
token: string;
|
||||
taskID: string;
|
||||
advanced: boolean;
|
||||
containerName: string;
|
||||
allowPort: boolean;
|
||||
specifyIP: string;
|
||||
restartPolicy: string;
|
||||
cpuQuota: number;
|
||||
memoryLimit: number;
|
||||
memoryUnit: string;
|
||||
pullImage: boolean;
|
||||
editCompose: boolean;
|
||||
dockerCompose: string;
|
||||
}
|
||||
|
||||
export interface AgentItem {
|
||||
id: number;
|
||||
name: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
token: string;
|
||||
status: string;
|
||||
message: string;
|
||||
appInstallId: number;
|
||||
appVersion: string;
|
||||
containerName: string;
|
||||
webUIPort: number;
|
||||
bridgePort: number;
|
||||
path: string;
|
||||
configPath: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
export interface AgentDeleteReq {
|
||||
id: number;
|
||||
taskID: string;
|
||||
forceDelete: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
provider: string;
|
||||
baseUrl: string;
|
||||
models: ProviderModelInfo[];
|
||||
}
|
||||
|
||||
export interface AgentAccountCreateReq {
|
||||
provider: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AgentAccountUpdateReq {
|
||||
id: number;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
remark: string;
|
||||
syncAgents: boolean;
|
||||
}
|
||||
|
||||
export interface AgentAccountSearch {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
provider: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AgentAccountItem {
|
||||
id: number;
|
||||
provider: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
verified: boolean;
|
||||
remark: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AgentAccountVerifyReq {
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
}
|
||||
|
||||
export interface AgentAccountDeleteReq {
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import http from '@/api';
|
||||
import { ResPage } from '../interface';
|
||||
import { ResPage, SearchWithPage } from '../interface';
|
||||
|
||||
export const createOllamaModel = (name: string, taskID: string) => {
|
||||
return http.post(`/ai/ollama/model`, { name: name, taskID: taskID });
|
||||
@@ -91,3 +91,39 @@ export const deleteTensorRTLLM = (req: AI.TensorRTLLMDelete) => {
|
||||
export const operateTensorRTLLM = (req: AI.TensorRTLLMOperate) => {
|
||||
return http.post(`/ai/tensorrt/operate`, req);
|
||||
};
|
||||
|
||||
export const createAgent = (req: AI.AgentCreateReq) => {
|
||||
return http.post<AI.AgentItem>(`/ai/agents`, req);
|
||||
};
|
||||
|
||||
export const pageAgents = (req: SearchWithPage) => {
|
||||
return http.post<ResPage<AI.AgentItem>>(`/ai/agents/search`, req);
|
||||
};
|
||||
|
||||
export const deleteAgent = (req: AI.AgentDeleteReq) => {
|
||||
return http.post(`/ai/agents/delete`, req);
|
||||
};
|
||||
|
||||
export const getAgentProviders = () => {
|
||||
return http.get<AI.ProviderInfo[]>(`/ai/agents/providers`);
|
||||
};
|
||||
|
||||
export const createAgentAccount = (req: AI.AgentAccountCreateReq) => {
|
||||
return http.post(`/ai/agents/accounts`, req);
|
||||
};
|
||||
|
||||
export const updateAgentAccount = (req: AI.AgentAccountUpdateReq) => {
|
||||
return http.post(`/ai/agents/accounts/update`, req);
|
||||
};
|
||||
|
||||
export const pageAgentAccounts = (req: AI.AgentAccountSearch) => {
|
||||
return http.post<ResPage<AI.AgentAccountItem>>(`/ai/agents/accounts/search`, req);
|
||||
};
|
||||
|
||||
export const verifyAgentAccount = (req: AI.AgentAccountVerifyReq) => {
|
||||
return http.post(`/ai/agents/accounts/verify`, req);
|
||||
};
|
||||
|
||||
export const deleteAgentAccount = (req: AI.AgentAccountDeleteReq) => {
|
||||
return http.post(`/ai/agents/accounts/delete`, req);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<el-form-item prop="advanced">
|
||||
<el-checkbox v-model="form.advanced" :label="$t('app.advanced')" size="large" />
|
||||
</el-form-item>
|
||||
<div v-if="form.advanced">
|
||||
<el-form-item :label="$t('app.containerName')" prop="containerName">
|
||||
<el-input v-model.trim="form.containerName" :placeholder="$t('app.containerNameHelper')" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showAllowPort" prop="allowPort">
|
||||
<el-checkbox v-model="form.allowPort" :label="$t('app.allowPort')" size="large" />
|
||||
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showSpecifyIP && form.allowPort" :label="$t('app.specifyIP')" prop="specifyIP">
|
||||
<el-input v-model="form.specifyIP" />
|
||||
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showRestartPolicy" :label="$t('container.restartPolicy')" prop="restartPolicy">
|
||||
<el-select v-model="form.restartPolicy" class="p-w-300">
|
||||
<el-option :label="$t('container.no')" value="no" />
|
||||
<el-option :label="$t('container.always')" value="always" />
|
||||
<el-option :label="$t('container.onFailure')" value="on-failure" />
|
||||
<el-option :label="$t('container.unlessStopped')" value="unless-stopped" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.cpuQuota')" prop="cpuQuota" :rules="checkNumberRange(0, limits.cpu)">
|
||||
<el-input type="number" class="!w-2/5" v-model.number="form.cpuQuota" maxlength="5">
|
||||
<template #append>{{ $t('app.cpuCore') }}</template>
|
||||
</el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('container.memoryLimit')"
|
||||
prop="memoryLimit"
|
||||
:rules="checkNumberRange(0, limits.memory)"
|
||||
>
|
||||
<el-input class="!w-2/5" v-model.number="form.memoryLimit" maxlength="10">
|
||||
<template #append>
|
||||
<el-select v-model="form.memoryUnit" class="p-w-100" @change="changeUnit">
|
||||
<el-option label="MB" value="M" />
|
||||
<el-option label="GB" value="G" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">{{ $t('container.limitHelper', [limits.memory]) }}{{ form.memoryUnit }}B</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showPullImage" prop="pullImage">
|
||||
<el-checkbox v-model="form.pullImage" :label="$t('app.pullImage')" />
|
||||
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showCompose" prop="editCompose">
|
||||
<el-checkbox v-model="form.editCompose" :label="$t('app.editCompose')" />
|
||||
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
|
||||
</el-form-item>
|
||||
<div v-if="showCompose && form.editCompose">
|
||||
<CodemirrorPro v-model="form.dockerCompose" mode="yaml" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, toRef } from 'vue';
|
||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||
import { checkNumberRange } from '@/global/form-rules';
|
||||
import { loadResourceLimit } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
form: {
|
||||
advanced: boolean;
|
||||
containerName: string;
|
||||
allowPort: boolean;
|
||||
specifyIP: string;
|
||||
restartPolicy: string;
|
||||
cpuQuota: number;
|
||||
memoryLimit: number;
|
||||
memoryUnit: string;
|
||||
pullImage: boolean;
|
||||
editCompose: boolean;
|
||||
dockerCompose: string;
|
||||
};
|
||||
showAllowPort?: boolean;
|
||||
showSpecifyIP?: boolean;
|
||||
showRestartPolicy?: boolean;
|
||||
showPullImage?: boolean;
|
||||
showCompose?: boolean;
|
||||
autoLoadLimit?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showAllowPort: true,
|
||||
showSpecifyIP: true,
|
||||
showRestartPolicy: true,
|
||||
showPullImage: true,
|
||||
showCompose: true,
|
||||
autoLoadLimit: true,
|
||||
},
|
||||
);
|
||||
|
||||
const form = toRef(props, 'form');
|
||||
const limits = ref<Container.ResourceLimit>({
|
||||
cpu: null as number,
|
||||
memory: null as number,
|
||||
});
|
||||
const oldMemory = ref(0);
|
||||
|
||||
const changeUnit = () => {
|
||||
if (form.value.memoryUnit === 'M') {
|
||||
limits.value.memory = oldMemory.value;
|
||||
} else {
|
||||
limits.value.memory = Number((oldMemory.value / 1024).toFixed(2));
|
||||
}
|
||||
};
|
||||
|
||||
const loadLimit = async () => {
|
||||
const res = await loadResourceLimit();
|
||||
limits.value = res.data;
|
||||
limits.value.memory = Number((limits.value.memory / 1024 / 1024).toFixed(2));
|
||||
oldMemory.value = limits.value.memory;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoLoadLimit) {
|
||||
loadLimit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -31,6 +31,8 @@ interface DialogProps {
|
||||
port: any;
|
||||
ip: string;
|
||||
protocol: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
@@ -48,18 +50,28 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const buildUrl = (host: string) => {
|
||||
let url = `${protocol}://${host}:${params.port}`;
|
||||
if (params.path) {
|
||||
url += params.path.startsWith('/') ? params.path : `/${params.path}`;
|
||||
}
|
||||
if (params.query) {
|
||||
url += params.query.startsWith('?') ? params.query : `?${params.query}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
if (res.data.systemIP.indexOf(':') === -1) {
|
||||
if (params.ip && params.ip === 'ipv6') {
|
||||
MsgWarning(i18n.global.t('setting.systemIPWarning1', ['IPv4']));
|
||||
return;
|
||||
}
|
||||
window.open(`${protocol}://${res.data.systemIP}:${params.port}`, '_blank');
|
||||
window.open(buildUrl(res.data.systemIP), '_blank');
|
||||
} else {
|
||||
if (params.ip && params.ip === 'ipv4') {
|
||||
MsgWarning(i18n.global.t('setting.systemIPWarning1', ['IPv6']));
|
||||
return;
|
||||
}
|
||||
window.open(`${protocol}://[${res.data.systemIP}]:${params.port}`, '_blank');
|
||||
window.open(buildUrl(`[${res.data.systemIP}]`), '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -676,6 +676,30 @@ const message = {
|
||||
localIP: 'Local IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Model',
|
||||
create: 'Add Model',
|
||||
|
||||
@@ -672,6 +672,30 @@ const message = {
|
||||
localIP: 'IP local',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Modelo',
|
||||
create: 'Agregar modelo',
|
||||
|
||||
@@ -661,6 +661,30 @@ const message = {
|
||||
localIP: 'ローカルIP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'モデル',
|
||||
create: 'モデルを追加',
|
||||
|
||||
@@ -658,6 +658,30 @@ const message = {
|
||||
localIP: '로컬 IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: '모델',
|
||||
create: '모델 추가',
|
||||
|
||||
@@ -673,6 +673,30 @@ const message = {
|
||||
localIP: 'IP Tempatan',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Model',
|
||||
create: 'Tambah Model',
|
||||
|
||||
@@ -670,6 +670,30 @@ const message = {
|
||||
localIP: 'IP local',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Modelo',
|
||||
create: 'Adicionar Modelo',
|
||||
|
||||
@@ -666,6 +666,30 @@ const message = {
|
||||
localIP: 'Локальный IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Модель',
|
||||
create: 'Добавить модель',
|
||||
|
||||
@@ -680,6 +680,30 @@ const message = {
|
||||
localIP: 'Yerel IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: 'Model',
|
||||
create: 'Model Ekle',
|
||||
|
||||
@@ -648,6 +648,30 @@ const message = {
|
||||
localIP: '本機 IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: 'Agents',
|
||||
agent: 'Agent',
|
||||
createAgent: 'Create Agent',
|
||||
createModel: 'Create Model',
|
||||
agentList: 'Agent List',
|
||||
modelList: 'Model List',
|
||||
account: 'Account',
|
||||
accountName: 'Account Name',
|
||||
accountList: 'Account List',
|
||||
createAccount: 'Create Account',
|
||||
syncAgents: 'Sync related agents',
|
||||
syncAgentsHelper: 'Update openclaw.json for agents using this account',
|
||||
appVersion: 'App Version',
|
||||
webuiPort: 'WebUI Port',
|
||||
bridgePort: 'Bridge Port',
|
||||
provider: 'Provider',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: 'Model Name',
|
||||
manualModel: 'Manual input',
|
||||
verified: 'Verified',
|
||||
},
|
||||
model: {
|
||||
model: '模型',
|
||||
create: '新增模型',
|
||||
|
||||
@@ -650,6 +650,30 @@ const message = {
|
||||
localIP: '本机 IP',
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
agents: '智能体',
|
||||
agent: '智能体',
|
||||
createAgent: '创建智能体',
|
||||
createModel: '创建模型',
|
||||
agentList: '智能体列表',
|
||||
modelList: '模型列表',
|
||||
account: '账号',
|
||||
accountName: '账号名称',
|
||||
accountList: '账号列表',
|
||||
createAccount: '创建账号',
|
||||
syncAgents: '同步关联智能体',
|
||||
syncAgentsHelper: '更新使用该账号的智能体 openclaw.json',
|
||||
appVersion: '应用版本',
|
||||
webuiPort: 'WebUI 端口',
|
||||
bridgePort: 'Bridge 端口',
|
||||
provider: '模型供应商',
|
||||
apiKey: 'API Key',
|
||||
baseUrl: 'Base URL',
|
||||
token: 'Token',
|
||||
modelName: '模型名称',
|
||||
manualModel: '手动输入模型',
|
||||
verified: '验证状态',
|
||||
},
|
||||
model: {
|
||||
model: '模型',
|
||||
create: '添加模型',
|
||||
|
||||
@@ -11,6 +11,26 @@ const databaseRouter = {
|
||||
title: 'menu.aiTools',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/ai/agents/agent',
|
||||
name: 'Agents',
|
||||
component: () => import('@/views/ai/agents/agent/index.vue'),
|
||||
meta: {
|
||||
icon: 'p-jiqiren2',
|
||||
title: 'aiTools.agents.agents',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/agents/model',
|
||||
name: 'AgentsModel',
|
||||
component: () => import('@/views/ai/agents/model/index.vue'),
|
||||
meta: {
|
||||
title: 'aiTools.agents.account',
|
||||
activeMenu: '/ai/agents/agent',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/model/ollama',
|
||||
name: 'OllamaModel',
|
||||
|
||||
@@ -13,6 +13,9 @@ export const jumpToInstall = (type: string, key: string) => {
|
||||
return true;
|
||||
}
|
||||
switch (key) {
|
||||
case 'openclaw':
|
||||
jumpToPath(router, '/ai/agents/agent');
|
||||
return true;
|
||||
case 'mysql-cluster':
|
||||
jumpToPath(router, '/xpack/cluster/mysql');
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('aiTools.agents.createAgent')" size="large" @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<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.appVersion')" prop="appVersion">
|
||||
<el-select v-model="form.appVersion" filterable>
|
||||
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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-input-number v-model="form.bridgePort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
|
||||
<el-select v-model="form.provider" @change="handleProviderChange">
|
||||
<el-option v-for="item in providerOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="accountOptions.length > 0" :label="$t('aiTools.agents.account')" prop="accountId">
|
||||
<el-select v-model="form.accountId" @change="handleAccountChange">
|
||||
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-else :label="$t('aiTools.agents.account')">
|
||||
<el-button type="text" @click="openAccountCreate">
|
||||
{{ $t('aiTools.agents.createAccount') }}
|
||||
</el-button>
|
||||
</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-option v-for="item in filteredModels" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.apiKey')" prop="apiKey">
|
||||
<el-input v-model="form.apiKey" type="password" show-password readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
|
||||
<el-input v-model="form.baseURL" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.token')">
|
||||
<el-input v-model="form.token" readonly>
|
||||
<template #append>
|
||||
<CopyButton :content="form.token" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<AdvancedSetting :form="form" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit">{{ $t('commons.button.confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<AccountAddDialog ref="accountAddRef" @search="handleAccountCreated" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { checkNumberRange, Rules } from '@/global/form-rules';
|
||||
import { createAgent, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAppByKey, getAppDetail } from '@/api/modules/app';
|
||||
import { getRandomStr, newUUID } from '@/utils/util';
|
||||
import { App } from '@/api/interface/app';
|
||||
import AdvancedSetting from '@/components/advanced-setting/index.vue';
|
||||
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
|
||||
const emit = defineEmits(['search', 'task']);
|
||||
|
||||
const open = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const versions = ref<string[]>([]);
|
||||
const accountOptions = ref<AI.AgentAccountItem[]>([]);
|
||||
const providerOptions = ref<string[]>([]);
|
||||
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
|
||||
const manualModel = ref(false);
|
||||
const appInfo = ref<App.AppDTO>();
|
||||
const accountAddRef = ref();
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
appVersion: '',
|
||||
webUIPort: 18789,
|
||||
bridgePort: 18790,
|
||||
provider: 'deepseek',
|
||||
accountId: 0,
|
||||
model: '',
|
||||
apiKey: '',
|
||||
baseURL: '',
|
||||
token: '',
|
||||
advanced: true,
|
||||
containerName: '',
|
||||
allowPort: true,
|
||||
specifyIP: '',
|
||||
restartPolicy: 'unless-stopped',
|
||||
cpuQuota: 0,
|
||||
memoryLimit: 0,
|
||||
memoryUnit: 'M',
|
||||
pullImage: true,
|
||||
editCompose: false,
|
||||
dockerCompose: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput],
|
||||
appVersion: [Rules.requiredSelect],
|
||||
webUIPort: [Rules.requiredInput],
|
||||
bridgePort: [Rules.requiredInput],
|
||||
provider: [Rules.requiredSelect],
|
||||
accountId: [Rules.requiredSelect],
|
||||
model: [Rules.requiredSelect],
|
||||
containerName: [Rules.containerName],
|
||||
restartPolicy: [Rules.requiredSelect],
|
||||
cpuQuota: [checkNumberRange(0, 99999)],
|
||||
memoryLimit: [checkNumberRange(0, 9999999999)],
|
||||
specifyIP: [Rules.ipv4orV6],
|
||||
});
|
||||
|
||||
const filteredModels = computed(() => providerModels.value[form.provider] || []);
|
||||
|
||||
const loadVersions = async () => {
|
||||
const res = await getAppByKey('openclaw');
|
||||
appInfo.value = res.data;
|
||||
versions.value = res.data.versions || [];
|
||||
if (!form.appVersion && versions.value.length > 0) {
|
||||
form.appVersion = versions.value[0];
|
||||
}
|
||||
};
|
||||
|
||||
const loadCompose = async () => {
|
||||
if (!appInfo.value || !form.appVersion) {
|
||||
return;
|
||||
}
|
||||
const res = await getAppDetail(appInfo.value.id, form.appVersion, appInfo.value.type);
|
||||
form.dockerCompose = res.data.dockerCompose || '';
|
||||
};
|
||||
|
||||
const loadProviders = async () => {
|
||||
const res = await getAgentProviders();
|
||||
const data = res.data || [];
|
||||
providerOptions.value = data.map((item) => item.provider);
|
||||
providerModels.value = data.reduce((acc, item) => {
|
||||
acc[item.provider] = item.models || [];
|
||||
return acc;
|
||||
}, {} as Record<string, AI.ProviderModelInfo[]>);
|
||||
if (!providerOptions.value.includes(form.provider) && providerOptions.value.length > 0) {
|
||||
form.provider = providerOptions.value[0];
|
||||
}
|
||||
setDefaultModel();
|
||||
};
|
||||
|
||||
const loadAccounts = async () => {
|
||||
if (!form.provider) {
|
||||
accountOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const req: AI.AgentAccountSearch = {
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
provider: form.provider,
|
||||
name: '',
|
||||
};
|
||||
const res = await pageAgentAccounts(req);
|
||||
accountOptions.value = res.data.items || [];
|
||||
if (accountOptions.value.length > 0) {
|
||||
form.accountId = accountOptions.value[0].id;
|
||||
handleAccountChange();
|
||||
} else {
|
||||
form.accountId = 0;
|
||||
form.apiKey = '';
|
||||
form.baseURL = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleProviderChange = () => {
|
||||
form.model = '';
|
||||
form.apiKey = '';
|
||||
form.baseURL = '';
|
||||
form.accountId = 0;
|
||||
loadAccounts();
|
||||
setDefaultModel();
|
||||
};
|
||||
|
||||
const handleModelChange = () => {
|
||||
if (manualModel.value) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountChange = () => {
|
||||
const selected = accountOptions.value.find((item) => item.id === form.accountId);
|
||||
if (selected) {
|
||||
form.baseURL = selected.baseUrl || '';
|
||||
form.apiKey = selected.apiKey || '';
|
||||
}
|
||||
setDefaultModel();
|
||||
};
|
||||
|
||||
const setDefaultModel = () => {
|
||||
if (manualModel.value) {
|
||||
return;
|
||||
}
|
||||
const models = filteredModels.value;
|
||||
if (models.length > 0 && !form.model) {
|
||||
form.model = models[0].id;
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
const taskID = newUUID();
|
||||
if (!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,
|
||||
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;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
form.token = '';
|
||||
form.dockerCompose = '';
|
||||
};
|
||||
|
||||
const openDrawer = async () => {
|
||||
open.value = true;
|
||||
manualModel.value = false;
|
||||
form.token = getRandomStr(32).toLowerCase();
|
||||
await loadVersions();
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
};
|
||||
|
||||
const openAccountCreate = () => {
|
||||
if (accountAddRef.value?.open) {
|
||||
accountAddRef.value.open({ provider: form.provider });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountCreated = async () => {
|
||||
await loadAccounts();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => form.editCompose,
|
||||
async (value) => {
|
||||
if (value && !form.dockerCompose) {
|
||||
await loadCompose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.appVersion,
|
||||
async (value, oldValue) => {
|
||||
if (!value || value === oldValue) {
|
||||
return;
|
||||
}
|
||||
if (form.editCompose) {
|
||||
await loadCompose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
open: openDrawer,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<DialogPro
|
||||
v-model="open"
|
||||
:title="$t('commons.button.delete') + ' - ' + agentName"
|
||||
size="small"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="deleteForm" label-position="left" v-loading="loading">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('website.forceDelete')" />
|
||||
<span class="input-help">
|
||||
{{ $t('website.forceDeleteHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit" :loading="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
<TaskLog ref="taskLogRef" @close="handleClose" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
import { deleteAgent } from '@/api/modules/ai';
|
||||
import { newUUID } from '@/utils/util';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const taskLogRef = ref();
|
||||
const agentName = ref('');
|
||||
const deleteForm = ref<FormInstance>();
|
||||
const deleteReq = ref({
|
||||
id: 0,
|
||||
taskID: '',
|
||||
forceDelete: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const acceptParams = (id: number, name: string) => {
|
||||
deleteReq.value = {
|
||||
id: id,
|
||||
taskID: newUUID(),
|
||||
forceDelete: false,
|
||||
};
|
||||
agentName.value = name;
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await deleteAgent(deleteReq.value);
|
||||
handleClose();
|
||||
taskLogRef.value?.openWithTaskID(deleteReq.value.taskID);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterMenu />
|
||||
<LayoutContent :title="$t('aiTools.agents.agentList')">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="openCreate">{{ $t('aiTools.agents.createAgent') }}</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch v-model:searchName="searchName" @search="search" />
|
||||
<TableRefresh @search="search" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :data="items" :pagination-config="paginationConfig" @search="search">
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="160" />
|
||||
<el-table-column :label="$t('aiTools.agents.appVersion')" prop="appVersion" width="120" />
|
||||
<el-table-column :label="$t('aiTools.agents.webuiPort')" prop="webUIPort" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="Position" plain size="small" @click="jumpWebUI(row)">
|
||||
{{ row.webUIPort }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.bridgePort')" prop="bridgePort" width="120" />
|
||||
<el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="120" />
|
||||
<el-table-column :label="$t('aiTools.model.model')" prop="model" min-width="180" />
|
||||
<el-table-column :label="$t('aiTools.agents.token')" width="120">
|
||||
<template #default="{ row }">
|
||||
<CopyButton :content="row.token" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" prop="status" width="120">
|
||||
<template #default="{ row }">
|
||||
<Status :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.date')"
|
||||
prop="createdAt"
|
||||
width="180"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
:buttons="buttons"
|
||||
min-width="300"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
:ellipsis="5"
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<AddDialog ref="addRef" @search="search" @task="openTaskLog" />
|
||||
<TaskLog ref="taskLogRef" @close="search" />
|
||||
<DeleteDialog ref="deleteRef" @close="search" />
|
||||
<ComposeLogs ref="composeLogRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { pageAgents } from '@/api/modules/ai';
|
||||
import { installedOp } from '@/api/modules/app';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { SearchWithPage } from '@/api/interface';
|
||||
import { dateFormat, newUUID } from '@/utils/util';
|
||||
|
||||
import RouterMenu from '@/views/ai/agents/index.vue';
|
||||
import AddDialog from '@/views/ai/agents/agent/add/index.vue';
|
||||
import DeleteDialog from '@/views/ai/agents/agent/delete/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import ComposeLogs from '@/components/log/compose/index.vue';
|
||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
|
||||
const items = ref<AI.AgentItem[]>([]);
|
||||
const addRef = ref();
|
||||
const taskLogRef = ref();
|
||||
const deleteRef = ref();
|
||||
const composeLogRef = ref();
|
||||
const dialogTerminalRef = ref();
|
||||
const dialogPortJumpRef = ref();
|
||||
const searchName = ref('');
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.operate.start'),
|
||||
click: (row: AI.AgentItem) => onOperate(row, 'start'),
|
||||
disabled: (row: AI.AgentItem) => row.status === 'Running',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.operate.stop'),
|
||||
click: (row: AI.AgentItem) => onOperate(row, 'stop'),
|
||||
disabled: (row: AI.AgentItem) => row.status !== 'Running',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.operate.restart'),
|
||||
click: (row: AI.AgentItem) => onOperate(row, 'restart'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.log'),
|
||||
click: (row: AI.AgentItem) => openLog(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('menu.terminal'),
|
||||
click: (row: AI.AgentItem) => openTerminal(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentItem) => onDelete(row),
|
||||
},
|
||||
];
|
||||
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
const req: SearchWithPage = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
info: searchName.value || '',
|
||||
};
|
||||
const res = await pageAgents(req);
|
||||
items.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total || 0;
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
if (addRef.value?.open) {
|
||||
addRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
const openTaskLog = (taskID: string) => {
|
||||
if (taskLogRef.value?.openWithTaskID) {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
}
|
||||
};
|
||||
|
||||
const onOperate = async (row: AI.AgentItem, operate: string) => {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('app.operatorHelper', [i18n.global.t('commons.operate.' + operate)]),
|
||||
i18n.global.t('commons.operate.' + operate),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
const taskID = newUUID();
|
||||
await installedOp({ installId: row.appInstallId, operate, taskID });
|
||||
await search();
|
||||
};
|
||||
|
||||
const openLog = (row: AI.AgentItem) => {
|
||||
if (row.status === 'Installing') {
|
||||
taskLogRef.value?.openWithResourceID('App', 'TaskInstall', row.appInstallId);
|
||||
return;
|
||||
}
|
||||
composeLogRef.value?.acceptParams({
|
||||
compose: `${row.path}/docker-compose.yml`,
|
||||
resource: row.name,
|
||||
container: row.containerName,
|
||||
});
|
||||
};
|
||||
|
||||
const openTerminal = (row: AI.AgentItem) => {
|
||||
const title = i18n.global.t('aiTools.agents.agent') + ' ' + row.name;
|
||||
dialogTerminalRef.value?.acceptParams({ containerID: row.containerName, title });
|
||||
};
|
||||
|
||||
const jumpWebUI = (row: AI.AgentItem) => {
|
||||
if (dialogPortJumpRef.value?.acceptParams) {
|
||||
dialogPortJumpRef.value.acceptParams({
|
||||
port: row.webUIPort,
|
||||
protocol: 'http',
|
||||
query: `token=${row.token}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = (row: AI.AgentItem) => {
|
||||
deleteRef.value?.acceptParams(row.id, row.name);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await search();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton :buttons="buttons" />
|
||||
<LayoutContent>
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import i18n from '@/lang';
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.agent'),
|
||||
path: '/ai/agents/agent',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.account'),
|
||||
path: '/ai/agents/model',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="headerTitle" size="large" @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
|
||||
<el-select v-model="form.provider" @change="handleProviderChange" :disabled="form.id > 0">
|
||||
<el-option v-for="item in providerOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.accountName')" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
|
||||
<el-input v-model="form.baseURL" :disabled="form.provider !== 'ollama'" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.apiKey')" prop="apiKey">
|
||||
<el-input v-model="form.apiKey" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.remark')" prop="remark">
|
||||
<el-input v-model="form.remark" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.id" prop="syncAgents">
|
||||
<el-checkbox v-model="form.syncAgents" :label="$t('aiTools.agents.syncAgents')" />
|
||||
<span class="input-help">{{ $t('aiTools.agents.syncAgentsHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="submit">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
|
||||
const open = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const providerOptions = ref<string[]>([]);
|
||||
const providerBaseURL = ref<Record<string, string>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
provider: '',
|
||||
name: '',
|
||||
baseURL: '',
|
||||
apiKey: '',
|
||||
remark: '',
|
||||
syncAgents: false,
|
||||
});
|
||||
|
||||
const headerTitle = computed(() =>
|
||||
form.id ? i18n.global.t('commons.button.edit') : i18n.global.t('aiTools.agents.createAccount'),
|
||||
);
|
||||
|
||||
const rules = reactive({
|
||||
provider: [Rules.requiredSelect],
|
||||
name: [Rules.requiredInput],
|
||||
apiKey: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
if (form.id) {
|
||||
await updateAgentAccount({
|
||||
id: form.id,
|
||||
name: form.name,
|
||||
baseURL: form.baseURL,
|
||||
apiKey: form.apiKey,
|
||||
remark: form.remark,
|
||||
syncAgents: form.syncAgents,
|
||||
});
|
||||
} else {
|
||||
await createAgentAccount({
|
||||
provider: form.provider,
|
||||
name: form.name,
|
||||
baseURL: form.baseURL,
|
||||
apiKey: form.apiKey,
|
||||
remark: form.remark,
|
||||
});
|
||||
}
|
||||
MsgSuccess();
|
||||
emit('search');
|
||||
open.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
loading.value = false;
|
||||
form.id = 0;
|
||||
form.syncAgents = false;
|
||||
};
|
||||
|
||||
interface OpenParams {
|
||||
id?: number;
|
||||
provider?: string;
|
||||
name?: string;
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
const openDrawer = (params?: OpenParams) => {
|
||||
open.value = true;
|
||||
loading.value = false;
|
||||
if (params?.id) {
|
||||
form.id = params.id;
|
||||
form.provider = params.provider || '';
|
||||
form.name = params.name || '';
|
||||
form.baseURL = params.baseURL || '';
|
||||
form.apiKey = params.apiKey || '';
|
||||
form.remark = params.remark || '';
|
||||
form.syncAgents = false;
|
||||
return;
|
||||
}
|
||||
form.id = 0;
|
||||
form.name = '';
|
||||
form.baseURL = '';
|
||||
form.apiKey = '';
|
||||
form.remark = '';
|
||||
form.syncAgents = false;
|
||||
if (params?.provider) {
|
||||
form.provider = params.provider;
|
||||
handleProviderChange();
|
||||
}
|
||||
};
|
||||
|
||||
const loadProviders = async () => {
|
||||
const res = await getAgentProviders();
|
||||
const data = res.data || [];
|
||||
providerOptions.value = data.map((item) => item.provider);
|
||||
providerBaseURL.value = data.reduce((acc, item) => {
|
||||
acc[item.provider] = item.baseUrl || '';
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
if (!form.provider && providerOptions.value.length > 0) {
|
||||
form.provider = providerOptions.value[0];
|
||||
handleProviderChange();
|
||||
}
|
||||
};
|
||||
|
||||
const handleProviderChange = () => {
|
||||
if (form.provider !== 'ollama') {
|
||||
form.baseURL = providerBaseURL.value[form.provider] || '';
|
||||
} else {
|
||||
form.baseURL = '';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProviders();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open: openDrawer,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterMenu />
|
||||
<LayoutContent :title="$t('aiTools.agents.accountList')">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="openCreate">{{ $t('aiTools.agents.createAccount') }}</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch v-model:searchName="searchName" @search="search" />
|
||||
<TableRefresh @search="search" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :data="items" :pagination-config="paginationConfig" @search="search">
|
||||
<el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="120" />
|
||||
<el-table-column :label="$t('aiTools.agents.accountName')" prop="name" min-width="200" />
|
||||
<el-table-column :label="$t('aiTools.agents.baseUrl')" prop="baseUrl" min-width="200" />
|
||||
<el-table-column :label="$t('aiTools.agents.apiKey')" prop="apiKey" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ maskKey(row.apiKey) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.verified')" prop="verified" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.verified ? 'success' : 'info'">
|
||||
{{ row.verified ? 'OK' : 'N/A' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.date')"
|
||||
prop="createdAt"
|
||||
width="180"
|
||||
:formatter="dateFormat"
|
||||
/>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<AddDialog ref="addRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
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 { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
|
||||
const items = ref<AI.AgentAccountItem[]>([]);
|
||||
const addRef = ref();
|
||||
const searchName = ref('');
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: AI.AgentAccountItem) => onEdit(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentAccountItem) => onDelete(row),
|
||||
},
|
||||
];
|
||||
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
const req: AI.AgentAccountSearch = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
provider: '',
|
||||
name: searchName.value || '',
|
||||
};
|
||||
const res = await pageAgentAccounts(req);
|
||||
items.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total || 0;
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
if (addRef.value?.open) {
|
||||
addRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
const onEdit = (row: AI.AgentAccountItem) => {
|
||||
if (addRef.value?.open) {
|
||||
addRef.value.open({
|
||||
id: row.id,
|
||||
provider: row.provider,
|
||||
name: row.name,
|
||||
baseURL: row.baseUrl,
|
||||
apiKey: row.apiKey,
|
||||
remark: row.remark,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const maskKey = (value: string) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (value.length <= 6) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 3)}****${value.slice(-3)}`;
|
||||
};
|
||||
|
||||
const onDelete = async (row: AI.AgentAccountItem) => {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('commons.msg.confirmDelete', [row.name]),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
);
|
||||
await deleteAgentAccount({ id: row.id });
|
||||
await search();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await search();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user