mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-26 00:01:11 +00:00
feat(ai): unify model services and inference engine management (#14175)
This commit is contained in:
@@ -386,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ID uint `json:"id"`
|
||||
MasterAccountID uint `json:"masterAccountId"`
|
||||
Provider string `json:"provider"`
|
||||
|
||||
@@ -171,7 +171,8 @@ type ContainerItemStats struct {
|
||||
BuildCacheReclaimable int64 `json:"buildCacheReclaimable"`
|
||||
}
|
||||
type ContainerListStats struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
|
||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||
SystemUsage uint64 `json:"systemUsage"`
|
||||
|
||||
@@ -23,6 +23,9 @@ type AppInstallCreate struct {
|
||||
Services map[string]string `json:"services"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
|
||||
AppContainerConfig
|
||||
NodePushConfig
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
type AgentAccount struct {
|
||||
BaseModel
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,7 @@ type IAgentAccountRepo interface {
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -97,6 +99,25 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range models {
|
||||
models[i].AccountID = account.ID
|
||||
models[i].SortOrder = i + 1
|
||||
}
|
||||
return tx.Create(&models).Error
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeProviders(providers []string) []string {
|
||||
seen := make(map[string]struct{}, len(providers))
|
||||
result := make([]string, 0, len(providers))
|
||||
|
||||
@@ -1055,7 +1055,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
return err
|
||||
}
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1108,12 +1108,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
}
|
||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
verifyModel := ""
|
||||
if account.AppInstallID == 0 || len(models) > 0 {
|
||||
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1127,7 +1130,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
|
||||
if err := global.DB.Save(account).Error; err != nil {
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
@@ -1158,13 +1161,34 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
installIDs := make([]uint, 0, len(list))
|
||||
for _, account := range list {
|
||||
if account.AppInstallID != 0 {
|
||||
installIDs = append(installIDs, account.AppInstallID)
|
||||
}
|
||||
}
|
||||
localInstalls := make(map[uint]bool, len(installIDs))
|
||||
if len(installIDs) > 0 {
|
||||
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, install := range installs {
|
||||
localInstalls[install.ID] = true
|
||||
}
|
||||
}
|
||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
apiKey := ""
|
||||
if item.RememberAPIKey {
|
||||
apiKey = item.APIKey
|
||||
}
|
||||
appInstallID := uint(0)
|
||||
if localInstalls[item.AppInstallID] {
|
||||
appInstallID = item.AppInstallID
|
||||
}
|
||||
items = append(items, dto.AgentAccountInfo{
|
||||
AppInstallID: appInstallID,
|
||||
ID: item.ID,
|
||||
MasterAccountID: item.MasterAccountID,
|
||||
Provider: item.Provider,
|
||||
@@ -1362,13 +1386,27 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
||||
}
|
||||
|
||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.AppInstallID != 0 && account.Provider != "ollama" {
|
||||
_, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||
if err == nil {
|
||||
return buserr.WithDetail("ErrInvalidParams", "Delete the local inference engine instead", nil)
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var agentCount int64
|
||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
||||
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||
return nil, fmt.Errorf("invalid local model installation")
|
||||
}
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||
return nil, fmt.Errorf("model account belongs to another installation")
|
||||
}
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if !createIfMissing {
|
||||
return nil, nil
|
||||
}
|
||||
name := install.Name
|
||||
for suffix := 1; ; suffix++ {
|
||||
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
break
|
||||
}
|
||||
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||
}
|
||||
apiType, apiKey := "openai-completions", "vllm-local"
|
||||
if install.App.Key == "ollama" {
|
||||
apiType, apiKey = "openai-responses", "ollama-local"
|
||||
}
|
||||
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
||||
}
|
||||
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||
account.AppInstallID = install.ID
|
||||
if updateBaseURL {
|
||||
account.BaseURL = baseURL
|
||||
}
|
||||
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||
verifyModelExists := account.VerifyModel == ""
|
||||
for _, item := range models {
|
||||
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||
if item.ID == account.VerifyModel {
|
||||
verifyModelExists = true
|
||||
}
|
||||
}
|
||||
if !verifyModelExists {
|
||||
account.VerifyModel = ""
|
||||
accountChanged = true
|
||||
}
|
||||
var previous []model.AgentAccountModel
|
||||
if account.ID != 0 {
|
||||
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
modelsChanged := len(previous) != len(rows)
|
||||
if !modelsChanged {
|
||||
for i := range rows {
|
||||
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||
modelsChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if modelsChanged {
|
||||
err = agentAccountRepo.SaveLocal(account, rows)
|
||||
} else if accountChanged {
|
||||
err = agentAccountRepo.Save(account)
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||
for _, item := range models {
|
||||
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||
}
|
||||
_, err = SyncLocalModelAccount(install, 0, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), items, false, createIfMissing)
|
||||
return err
|
||||
}
|
||||
|
||||
func CleanupLocalModelAccount(installID uint) {
|
||||
if installID == 0 {
|
||||
return
|
||||
}
|
||||
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load local model accounts: %v", err)
|
||||
return
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||
switch addressType {
|
||||
case "custom":
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
||||
}
|
||||
return baseURL, nil
|
||||
case "localhost":
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||
case "systemIP":
|
||||
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
systemIP = strings.TrimSpace(systemIP)
|
||||
if systemIP == "" {
|
||||
systemIP = "127.0.0.1"
|
||||
}
|
||||
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||
default:
|
||||
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||
resolvedAPIType := strings.TrimSpace(apiType)
|
||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
@@ -113,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
||||
return resolvedAgentAccountInput{}, err
|
||||
}
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
if modelID == "" && !allowEmptyModel {
|
||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||
|
||||
@@ -125,6 +125,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -179,6 +182,9 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -202,7 +208,7 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||
_ = os.Remove(logItem)
|
||||
}
|
||||
return nil
|
||||
return SyncOllamaModelAccount(false)
|
||||
}
|
||||
|
||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
||||
@@ -250,7 +256,7 @@ func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
||||
_ = aiRepo.Create(&item)
|
||||
}
|
||||
|
||||
return dropList, nil
|
||||
return dropList, SyncOllamaModelAccount(false)
|
||||
}
|
||||
|
||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||
|
||||
@@ -499,6 +499,14 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
req.Params[constant.ContainerName] = containerName
|
||||
appInstall.ContainerName = containerName
|
||||
|
||||
modelAccountBaseURL := ""
|
||||
if app.Key == "ollama" {
|
||||
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
index := 0
|
||||
serviceName := ""
|
||||
for k := range servicesMap {
|
||||
@@ -609,6 +617,10 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
updateToolApp(appInstall)
|
||||
if app.Key == "ollama" {
|
||||
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -469,7 +469,12 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if err == nil && len(entries) == 0 {
|
||||
_ = op.DeleteDir(parentDir)
|
||||
}
|
||||
tx.Commit()
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||
CleanupLocalModelAccount(install.ID)
|
||||
}
|
||||
|
||||
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
||||
if len(existApps) == 0 {
|
||||
|
||||
@@ -324,10 +324,12 @@ func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string)
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
datas[index].ContainerID = list[index].ID
|
||||
if ctx.Err() != nil || list[index].State != "running" {
|
||||
continue
|
||||
if ctx.Err() == nil && list[index].State == "running" {
|
||||
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
|
||||
}
|
||||
if len(list[index].Names) > 0 {
|
||||
datas[index].ContainerName = strings.TrimPrefix(list[index].Names[0], "/")
|
||||
}
|
||||
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddVLLMSampleInterval,
|
||||
migrations.AddNativeAgentInstalls,
|
||||
migrations.AddQuickDirectory,
|
||||
migrations.AddLocalModelAccountLink,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1895,3 +1895,13 @@ var AddQuickDirectory = &gormigrate.Migration{
|
||||
return tx.AutoMigrate(&model.QuickDirectory{})
|
||||
},
|
||||
}
|
||||
|
||||
var AddLocalModelAccountLink = &gormigrate.Migration{
|
||||
ID: "20261023-add-local-model-account-link",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.AgentAccount{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.SyncOllamaModelAccount(true)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ var WebUrlMap = map[string]struct{}{
|
||||
"/ai/benchmark": {},
|
||||
"/ai/model/account": {},
|
||||
"/ai/model/local": {},
|
||||
"/ai/model/files": {},
|
||||
"/ai/gpu": {},
|
||||
"/ai/gpu/current": {},
|
||||
"/ai/gpu/history": {},
|
||||
|
||||
@@ -98,8 +98,8 @@ func LoadMenus() string {
|
||||
{ID: "2", Disabled: true, Title: "menu.apps", IsShow: true, Label: "App-Menu", Path: "/apps/all", Sort: 200},
|
||||
{ID: "3", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model/account", Sort: 300,
|
||||
Children: []dto.ShowMenu{
|
||||
{ID: "44", Disabled: false, Title: "aiTools.agents.agent", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", Sort: 50},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "AIModel", Path: "/ai/model/account", Sort: 100},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "AIModel", Path: "/ai/model/account", Sort: 50},
|
||||
{ID: "44", Disabled: false, Title: "aiTools.agents.agent", IsShow: true, Label: "Agents", Path: "/ai/agents/agent", 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},
|
||||
}},
|
||||
@@ -149,7 +149,7 @@ func LoadMenus() string {
|
||||
IsShow: true,
|
||||
Label: "AIProxyManagement",
|
||||
Path: "/ai/ai-proxy/model-pool",
|
||||
Sort: 150,
|
||||
Sort: 75,
|
||||
}, "AIModel")
|
||||
item[i].Children = UpsertMenuByLabel(item[i].Children, dto.ShowMenu{
|
||||
ID: "47",
|
||||
@@ -158,8 +158,8 @@ func LoadMenus() string {
|
||||
IsShow: true,
|
||||
Label: "SkillsHub",
|
||||
Path: "/ai/skills-hub",
|
||||
Sort: 155,
|
||||
}, "AIProxyManagement")
|
||||
Sort: 225,
|
||||
}, "MCPServer")
|
||||
item[i].Children = UpsertMenuByLabel(item[i].Children, dto.ShowMenu{
|
||||
ID: "45",
|
||||
Disabled: false,
|
||||
@@ -167,7 +167,7 @@ func LoadMenus() string {
|
||||
IsShow: true,
|
||||
Label: "AIBenchmark",
|
||||
Path: "/ai/benchmark",
|
||||
Sort: 160,
|
||||
Sort: 250,
|
||||
}, "SkillsHub")
|
||||
continue
|
||||
}
|
||||
@@ -204,12 +204,12 @@ func MenuSort() []dto.MenuLabelSort {
|
||||
{Label: "Home-Menu", Sort: 100},
|
||||
{Label: "App-Menu", Sort: 200},
|
||||
{Label: "AI-Menu", Sort: 300},
|
||||
{Label: "Agents", Sort: 50},
|
||||
{Label: "AIModel", Sort: 100},
|
||||
{Label: "AIProxyManagement", Sort: 150},
|
||||
{Label: "SkillsHub", Sort: 155},
|
||||
{Label: "AIBenchmark", Sort: 160},
|
||||
{Label: "AIModel", Sort: 50},
|
||||
{Label: "AIProxyManagement", Sort: 75},
|
||||
{Label: "Agents", Sort: 100},
|
||||
{Label: "MCPServer", Sort: 200},
|
||||
{Label: "SkillsHub", Sort: 225},
|
||||
{Label: "AIBenchmark", Sort: 250},
|
||||
{Label: "GPU", Sort: 300},
|
||||
{Label: "Website-Menu", Sort: 400},
|
||||
{Label: "Website", Sort: 100},
|
||||
|
||||
@@ -69,5 +69,6 @@ func coreMigrations() []*gormigrate.Migration {
|
||||
migrations.MoveVirtualMachineMenuToXpack,
|
||||
migrations.AddAPIKeys,
|
||||
migrations.AddOperationLogAPIKey,
|
||||
migrations.UpdateAiMenuOrder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,6 +1376,21 @@ var RemoveUpageHideMenu = &gormigrate.Migration{
|
||||
Migrate: reconcileHideMenuSetting,
|
||||
}
|
||||
|
||||
var UpdateAiMenuOrder = &gormigrate.Migration{
|
||||
ID: "20261023-update-ai-menu-order",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return helper.UpdateChildMenuSortByLabel(tx, "AI-Menu", []dto.MenuLabelSort{
|
||||
{Label: "AIModel", Sort: 50},
|
||||
{Label: "AIProxyManagement", Sort: 75},
|
||||
{Label: "Agents", Sort: 100},
|
||||
{Label: "MCPServer", Sort: 200},
|
||||
{Label: "SkillsHub", Sort: 225},
|
||||
{Label: "AIBenchmark", Sort: 250},
|
||||
{Label: "GPU", Sort: 300},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateFirewallMenuPath = &gormigrate.Migration{
|
||||
ID: "20260819-update-firewall-menu-path",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
@@ -18,7 +18,7 @@ export namespace AI {
|
||||
name: string;
|
||||
}
|
||||
export interface OllamaModelSearch extends ReqPage {
|
||||
info: string;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
@@ -738,6 +738,7 @@ export namespace AI {
|
||||
}
|
||||
|
||||
export interface AgentAccountItem {
|
||||
appInstallId?: number;
|
||||
id: number;
|
||||
provider: string;
|
||||
providerName: string;
|
||||
|
||||
@@ -136,6 +136,8 @@ export namespace App {
|
||||
}
|
||||
|
||||
export interface AppInstall {
|
||||
modelAccountBaseURLType?: string;
|
||||
modelAccountBaseURL?: string;
|
||||
appDetailId: number;
|
||||
params: any;
|
||||
taskID: string;
|
||||
|
||||
@@ -177,6 +177,7 @@ export namespace Container {
|
||||
}
|
||||
export interface ContainerListStats {
|
||||
containerID: string;
|
||||
containerName: string;
|
||||
cpuTotalUsage: number;
|
||||
systemUsage: number;
|
||||
cpuPercent: number;
|
||||
|
||||
@@ -22,8 +22,13 @@ export const loadOllamaModel = (name: string) => {
|
||||
export const syncOllamaModel = () => {
|
||||
return http.post<Array<AI.OllamaModelDropInfo>>(`/ai/ollama/model/sync`);
|
||||
};
|
||||
export const closeOllamaModel = (name: string) => {
|
||||
return http.post(`/ai/ollama/close`, { name: name });
|
||||
export const closeOllamaModel = (name: string, currentNode?: string) => {
|
||||
return http.post(
|
||||
`/ai/ollama/close`,
|
||||
{ name: name },
|
||||
undefined,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
export const bindDomain = (req: AI.BindDomain) => {
|
||||
|
||||
@@ -90,10 +90,14 @@ export const containerItemStats = (containerID: string, currentNode?: string) =>
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => {
|
||||
export const containerListStats = (
|
||||
currentNode?: string,
|
||||
ids?: string[],
|
||||
config?: AxiosRequestConfig & { skipErrorMessage?: boolean },
|
||||
) => {
|
||||
return http.get<Array<Container.ContainerListStats>>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, {
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
...config,
|
||||
headers: { ...config?.headers, ...(currentNode ? { CurrentNode: currentNode } : {}) },
|
||||
});
|
||||
};
|
||||
export const containerStats = (id: string) => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="source-font">CPU: {{ stats.cpuPercent.toFixed(2) }}%</div>
|
||||
<div class="float-left source-font">{{ $t('monitor.memory') }}: {{ stats.memoryPercent.toFixed(2) }}%</div>
|
||||
<el-popover placement="right" width="500px" class="float-right">
|
||||
<template #reference>
|
||||
<SvgIcon iconName="p-xiangqing" className="resource-detail-icon" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions direction="vertical" border :column="3" size="small">
|
||||
<el-descriptions-item :label="$t('container.cpuUsage')">
|
||||
{{ computeCPU(stats.cpuTotalUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.cpuTotal')">
|
||||
{{ computeCPU(stats.systemUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.core')">{{ stats.percpuUsage }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.memUsage')">
|
||||
{{ computeSizeForDocker(stats.memoryUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.memCache')">
|
||||
{{ computeSizeForDocker(stats.memoryCache) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.memTotal')">
|
||||
{{ computeSizeForDocker(stats.memoryLimit) }}
|
||||
</el-descriptions-item>
|
||||
<slot />
|
||||
</el-descriptions>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Container } from '@/api/interface/container';
|
||||
import { computeCPU, computeSizeForDocker } from '@/utils/size';
|
||||
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
|
||||
defineProps<{ stats: Container.ContainerListStats }>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.source-font {
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.resource-detail-icon) {
|
||||
margin-top: -3px;
|
||||
font-size: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1017,6 +1017,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'Generates one minimal image before saving and may incur charges',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Model category',
|
||||
modelName: 'Model name',
|
||||
modelArchitecture: 'Model type',
|
||||
backboneParams: 'MoE backbone parameters',
|
||||
activeParams: 'Active parameters',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Weight size',
|
||||
undisclosed: 'Undisclosed',
|
||||
featuredModels: 'Featured models',
|
||||
deploy: 'Deploy',
|
||||
approxWeightSize: 'Approx. {0} GB',
|
||||
services: 'Model services',
|
||||
addService: 'Add model service',
|
||||
addEngine: 'Add inference engine',
|
||||
onlineModel: 'Online models',
|
||||
instanceDeployment: 'Deploy by instance',
|
||||
nvidiaDeployment: 'NVIDIA-optimized deployment',
|
||||
engines: 'Inference engines',
|
||||
modelManagement: 'Model management',
|
||||
serviceDetails: 'Service details',
|
||||
deployments: 'Local deployments',
|
||||
files: 'Model files',
|
||||
connectExternal: 'Connect external service',
|
||||
deploymentTemplates: 'Startup command templates',
|
||||
engineProvider: 'Engine / Provider',
|
||||
endpoint: 'Endpoint',
|
||||
cloudAccount: 'Cloud account',
|
||||
connectionStatus: 'Connection status',
|
||||
unverified: 'Unverified',
|
||||
manage: 'Manage',
|
||||
loadFailed: 'Failed to load. Refresh to retry.',
|
||||
model: 'Models',
|
||||
localModel: 'Local Models',
|
||||
create_helper: 'Pull "{0}"',
|
||||
@@ -1032,7 +1063,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'The startup command must use port {0}; otherwise, the service will be inaccessible.',
|
||||
ascendVisibleDevices: 'Ascend visible devices (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Sync to model account',
|
||||
modelAccountAddressHelper:
|
||||
'Container address is for agent containers; 127.0.0.1, system IP, or custom address is for AI Gateway or external services.',
|
||||
containerAddress: 'Container address',
|
||||
@@ -1055,7 +1085,6 @@ const message = {
|
||||
sortCreated: 'Newest',
|
||||
downloadsCount: 'Downloads',
|
||||
likes: 'Likes',
|
||||
taskQueue: 'Download Queue',
|
||||
progress: 'Progress',
|
||||
localDownloaded: 'Downloaded',
|
||||
modelInfo: 'Model Details',
|
||||
|
||||
@@ -1019,6 +1019,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'Genera una imagen mínima antes de guardar y puede generar costes',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Categoría del modelo',
|
||||
modelName: 'Nombre del modelo',
|
||||
modelArchitecture: 'Tipo de modelo',
|
||||
backboneParams: 'Parámetros base de MoE',
|
||||
activeParams: 'Parámetros activos',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Tamaño de los pesos',
|
||||
undisclosed: 'No divulgado',
|
||||
featuredModels: 'Modelos destacados',
|
||||
deploy: 'Desplegar',
|
||||
approxWeightSize: 'Aprox. {0} GB',
|
||||
services: 'Servicios de modelos',
|
||||
addService: 'Añadir servicio de modelo',
|
||||
addEngine: 'Añadir motor de inferencia',
|
||||
onlineModel: 'Modelos en línea',
|
||||
instanceDeployment: 'Despliegue por instancia',
|
||||
nvidiaDeployment: 'Despliegue optimizado para NVIDIA',
|
||||
engines: 'Motores de inferencia',
|
||||
modelManagement: 'Gestión de modelos',
|
||||
serviceDetails: 'Detalles del servicio',
|
||||
deployments: 'Despliegues locales',
|
||||
files: 'Archivos de modelos',
|
||||
connectExternal: 'Conectar servicio externo',
|
||||
deploymentTemplates: 'Plantillas de comandos de inicio',
|
||||
engineProvider: 'Motor / Proveedor',
|
||||
endpoint: 'Punto de conexión',
|
||||
cloudAccount: 'Cuenta en la nube',
|
||||
connectionStatus: 'Estado de conexión',
|
||||
unverified: 'Sin verificar',
|
||||
manage: 'Gestionar',
|
||||
loadFailed: 'No se pudo cargar. Actualice la página e inténtelo de nuevo.',
|
||||
model: 'Modelo',
|
||||
localModel: 'Modelos locales',
|
||||
create_helper: 'Descargar "{0}"',
|
||||
@@ -1035,7 +1066,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'El comando de inicio debe usar el puerto {0}; de lo contrario, no se podrá acceder al servicio.',
|
||||
ascendVisibleDevices: 'Dispositivos Ascend visibles (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Sincronizar con cuenta de modelo',
|
||||
modelAccountAddressHelper:
|
||||
'La dirección del contenedor es para contenedores de agentes; 127.0.0.1, IP del sistema o dirección personalizada es para AI Gateway o servicios externos.',
|
||||
containerAddress: 'Dirección del contenedor',
|
||||
@@ -1058,7 +1088,6 @@ const message = {
|
||||
sortCreated: 'Más recientes',
|
||||
downloadsCount: 'Descargas',
|
||||
likes: 'Me gusta',
|
||||
taskQueue: 'Cola de descargas',
|
||||
progress: 'Progreso',
|
||||
localDownloaded: 'Descargados',
|
||||
modelInfo: 'Detalles del modelo',
|
||||
|
||||
@@ -1002,6 +1002,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'پیش از ذخیره یک تصویر حداقلی تولید میشود و ممکن است هزینه داشته باشد',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'دستهبندی مدل',
|
||||
modelName: 'نام مدل',
|
||||
modelArchitecture: 'نوع مدل',
|
||||
backboneParams: 'پارامترهای پایه MoE',
|
||||
activeParams: 'پارامترهای فعال',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'حجم وزنها',
|
||||
undisclosed: 'اعلام نشده',
|
||||
featuredModels: 'مدلهای برگزیده',
|
||||
deploy: 'استقرار',
|
||||
approxWeightSize: 'حدود {0} گیگابایت',
|
||||
services: 'سرویسهای مدل',
|
||||
addService: 'افزودن سرویس مدل',
|
||||
addEngine: 'افزودن موتور استنتاج',
|
||||
onlineModel: 'مدلهای آنلاین',
|
||||
instanceDeployment: 'استقرار بر اساس نمونه',
|
||||
nvidiaDeployment: 'استقرار بهینهشده برای NVIDIA',
|
||||
engines: 'موتورهای استنتاج',
|
||||
modelManagement: 'مدیریت مدلها',
|
||||
serviceDetails: 'جزئیات سرویس',
|
||||
deployments: 'استقرارهای محلی',
|
||||
files: 'فایلهای مدل',
|
||||
connectExternal: 'اتصال به سرویس خارجی',
|
||||
deploymentTemplates: 'الگوهای فرمان راهاندازی',
|
||||
engineProvider: 'موتور / ارائهدهنده',
|
||||
endpoint: 'نقطه پایانی',
|
||||
cloudAccount: 'حساب ابری',
|
||||
connectionStatus: 'وضعیت اتصال',
|
||||
unverified: 'تأیید نشده',
|
||||
manage: 'مدیریت',
|
||||
loadFailed: 'بارگذاری ناموفق بود. صفحه را تازهسازی کرده و دوباره تلاش کنید.',
|
||||
model: 'مدلها',
|
||||
localModel: 'مدلهای محلی',
|
||||
create_helper: 'دریافت "{0}"',
|
||||
@@ -1017,7 +1048,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'فرمان راهاندازی باید از پورت {0} استفاده کند؛ در غیر این صورت سرویس قابل دسترسی نخواهد بود.',
|
||||
ascendVisibleDevices: 'دستگاههای قابل مشاهده Ascend (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'همگامسازی با حساب مدل',
|
||||
modelAccountAddressHelper:
|
||||
'آدرس کانتینر برای کانتینرهای عامل است؛ 127.0.0.1، IP سیستم یا آدرس سفارشی برای دروازه AI یا خدمات خارجی است.',
|
||||
containerAddress: 'آدرس کانتینر',
|
||||
@@ -1040,7 +1070,6 @@ const message = {
|
||||
sortCreated: 'جدیدترین',
|
||||
downloadsCount: 'دانلودها',
|
||||
likes: 'لایکها',
|
||||
taskQueue: 'صف دانلود',
|
||||
progress: 'پیشرفت',
|
||||
localDownloaded: 'دانلود شده',
|
||||
modelInfo: 'جزئیات مدل',
|
||||
|
||||
@@ -1006,6 +1006,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: '保存前に最小画像を1枚生成するため、料金が発生する場合があります',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'モデルカテゴリ',
|
||||
modelName: 'モデル名',
|
||||
modelArchitecture: 'モデルの種類',
|
||||
backboneParams: 'MoE バックボーンパラメータ数',
|
||||
activeParams: 'アクティブパラメータ数',
|
||||
engram: 'Engram',
|
||||
weightFootprint: '重みのサイズ',
|
||||
undisclosed: '非公開',
|
||||
featuredModels: '注目のモデル',
|
||||
deploy: 'デプロイ',
|
||||
approxWeightSize: '約 {0} GB',
|
||||
services: 'モデルサービス',
|
||||
addService: 'モデルサービスを追加',
|
||||
addEngine: '推論エンジンを追加',
|
||||
onlineModel: 'オンラインモデル',
|
||||
instanceDeployment: 'インスタンス単位でデプロイ',
|
||||
nvidiaDeployment: 'NVIDIA 向け最適化デプロイ',
|
||||
engines: '推論エンジン',
|
||||
modelManagement: 'モデル管理',
|
||||
serviceDetails: 'サービス詳細',
|
||||
deployments: 'ローカルデプロイ',
|
||||
files: 'モデルファイル',
|
||||
connectExternal: '外部サービスに接続',
|
||||
deploymentTemplates: '起動コマンドテンプレート',
|
||||
engineProvider: 'エンジン / プロバイダー',
|
||||
endpoint: 'エンドポイント',
|
||||
cloudAccount: 'クラウドアカウント',
|
||||
connectionStatus: '接続状態',
|
||||
unverified: '未検証',
|
||||
manage: '管理',
|
||||
loadFailed: '読み込みに失敗しました。更新して再試行してください。',
|
||||
model: 'モデル',
|
||||
localModel: 'ローカルモデル',
|
||||
create_helper: 'を取得 "{0}"',
|
||||
@@ -1021,7 +1052,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'起動コマンドではポート {0} を使用する必要があります。使用しない場合、サービスにアクセスできません。',
|
||||
ascendVisibleDevices: 'Ascend 可視デバイス(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'モデルアカウントに同期',
|
||||
modelAccountAddressHelper:
|
||||
'コンテナアドレスはエージェントコンテナ向けです。127.0.0.1、システム IP、カスタムアドレスは AI Gateway または外部サービス向けです。',
|
||||
containerAddress: 'コンテナアドレス',
|
||||
@@ -1044,7 +1074,6 @@ const message = {
|
||||
sortCreated: '新規作成',
|
||||
downloadsCount: 'ダウンロード数',
|
||||
likes: 'いいね',
|
||||
taskQueue: 'ダウンロードキュー',
|
||||
progress: '進捗',
|
||||
localDownloaded: 'ダウンロード済み',
|
||||
modelInfo: 'モデル詳細',
|
||||
|
||||
@@ -996,6 +996,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: '저장 전에 최소 이미지 한 장을 생성하며 비용이 발생할 수 있습니다',
|
||||
},
|
||||
model: {
|
||||
modelCategory: '모델 카테고리',
|
||||
modelName: '모델 이름',
|
||||
modelArchitecture: '모델 유형',
|
||||
backboneParams: 'MoE 백본 매개변수 수',
|
||||
activeParams: '활성 매개변수 수',
|
||||
engram: 'Engram',
|
||||
weightFootprint: '가중치 크기',
|
||||
undisclosed: '비공개',
|
||||
featuredModels: '추천 모델',
|
||||
deploy: '배포',
|
||||
approxWeightSize: '약 {0} GB',
|
||||
services: '모델 서비스',
|
||||
addService: '모델 서비스 추가',
|
||||
addEngine: '추론 엔진 추가',
|
||||
onlineModel: '온라인 모델',
|
||||
instanceDeployment: '인스턴스별 배포',
|
||||
nvidiaDeployment: 'NVIDIA 최적화 배포',
|
||||
engines: '추론 엔진',
|
||||
modelManagement: '모델 관리',
|
||||
serviceDetails: '서비스 상세 정보',
|
||||
deployments: '로컬 배포',
|
||||
files: '모델 파일',
|
||||
connectExternal: '외부 서비스 연결',
|
||||
deploymentTemplates: '시작 명령 템플릿',
|
||||
engineProvider: '엔진 / 공급자',
|
||||
endpoint: '엔드포인트',
|
||||
cloudAccount: '클라우드 계정',
|
||||
connectionStatus: '연결 상태',
|
||||
unverified: '미검증',
|
||||
manage: '관리',
|
||||
loadFailed: '불러오지 못했습니다. 새로 고침 후 다시 시도하세요.',
|
||||
model: '모델',
|
||||
localModel: '로컬 모델',
|
||||
create_helper: '가져오기 "{0}"',
|
||||
@@ -1010,7 +1041,6 @@ const message = {
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '시작 명령은 {0} 포트를 사용해야 하며, 그렇지 않으면 서비스에 접근할 수 없습니다.',
|
||||
ascendVisibleDevices: 'Ascend 표시 장치 (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '모델 계정에 동기화',
|
||||
modelAccountAddressHelper:
|
||||
'컨테이너 주소는 에이전트 컨테이너 호출에 적합합니다. 127.0.0.1, 시스템 IP 또는 사용자 지정 주소는 AI Gateway 또는 외부 서비스 호출에 적합합니다.',
|
||||
containerAddress: '컨테이너 주소',
|
||||
@@ -1033,7 +1063,6 @@ const message = {
|
||||
sortCreated: '최신 생성',
|
||||
downloadsCount: '다운로드 수',
|
||||
likes: '좋아요',
|
||||
taskQueue: '다운로드 대기열',
|
||||
progress: '진행률',
|
||||
localDownloaded: '다운로드됨',
|
||||
modelInfo: '모델 상세 정보',
|
||||
|
||||
@@ -993,6 +993,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'ສ້າງຮູບຂະໜາດນ້ອຍໜຶ່ງຮູບກ່ອນບັນທຶກ ແລະ ອາດມີຄ່າໃຊ້ຈ່າຍ',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'ໝວດໝູ່ໂມເດວ',
|
||||
modelName: 'ຊື່ໂມເດວ',
|
||||
modelArchitecture: 'ປະເພດໂມເດວ',
|
||||
backboneParams: 'ພາຣາມິເຕີຫຼັກຂອງ MoE',
|
||||
activeParams: 'ພາຣາມິເຕີທີ່ໃຊ້ງານ',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'ຂະໜາດນ້ຳໜັກ',
|
||||
undisclosed: 'ບໍ່ເປີດເຜີຍ',
|
||||
featuredModels: 'ໂມເດວແນະນຳ',
|
||||
deploy: 'ນຳໃຊ້',
|
||||
approxWeightSize: 'ປະມານ {0} GB',
|
||||
services: 'ບໍລິການໂມເດວ',
|
||||
addService: 'ເພີ່ມບໍລິການໂມເດວ',
|
||||
addEngine: 'ເພີ່ມເຄື່ອງຈັກອະນຸມານ',
|
||||
onlineModel: 'ໂມເດວອອນລາຍ',
|
||||
instanceDeployment: 'ນຳໃຊ້ຕາມອິນສະແຕນ',
|
||||
nvidiaDeployment: 'ການນຳໃຊ້ທີ່ປັບໃຫ້ເໝາະກັບ NVIDIA',
|
||||
engines: 'ເຄື່ອງຈັກອະນຸມານ',
|
||||
modelManagement: 'ຈັດການໂມເດວ',
|
||||
serviceDetails: 'ລາຍລະອຽດບໍລິການ',
|
||||
deployments: 'ການນຳໃຊ້ພາຍໃນເຄື່ອງ',
|
||||
files: 'ໄຟລ໌ໂມເດວ',
|
||||
connectExternal: 'ເຊື່ອມຕໍ່ບໍລິການພາຍນອກ',
|
||||
deploymentTemplates: 'ແມ່ແບບຄຳສັ່ງເລີ່ມຕົ້ນ',
|
||||
engineProvider: 'ເຄື່ອງຈັກ / ຜູ້ໃຫ້ບໍລິການ',
|
||||
endpoint: 'ຈຸດເຊື່ອມຕໍ່',
|
||||
cloudAccount: 'ບັນຊີຄລາວ',
|
||||
connectionStatus: 'ສະຖານະການເຊື່ອມຕໍ່',
|
||||
unverified: 'ຍັງບໍ່ໄດ້ຢືນຢັນ',
|
||||
manage: 'ຈັດການ',
|
||||
loadFailed: 'ໂຫຼດບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໜ້າຄືນແລ້ວລອງອີກຄັ້ງ.',
|
||||
model: 'ໂມເດວ',
|
||||
localModel: 'ໂມເດວໃນເຄື່ອງ',
|
||||
create_helper: 'ດຶງ (Pull) "{0}"',
|
||||
@@ -1007,7 +1038,6 @@ const message = {
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: 'ຄຳສັ່ງເລີ່ມຕົ້ນຕ້ອງໃຊ້ພອດ {0}; ບໍ່ດັ່ງນັ້ນຈະບໍ່ສາມາດເຂົ້າເຖິງບໍລິການໄດ້.',
|
||||
ascendVisibleDevices: 'ອຸປະກອນ Ascend ທີ່ເຫັນໄດ້ (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'ຊິ້ງຄ໌ໄປຍັງບັນຊີໂມເດວ',
|
||||
modelAccountAddressHelper:
|
||||
'ທີ່ຢູ່ຄອນເທນເນີແມ່ນສຳລັບຄອນເທນເນີຕົວແທນ; 127.0.0.1, IP ລະບົບ, ຫຼື ທີ່ຢູ່ກຳນົດເອງແມ່ນສຳລັບ AI Gateway ຫຼື ບໍລິການພາຍນອກ.',
|
||||
containerAddress: 'ທີ່ຢູ່ຄອນເທນເນີ',
|
||||
@@ -1030,7 +1060,6 @@ const message = {
|
||||
sortCreated: 'ໃໝ່ລ່າສຸດ',
|
||||
downloadsCount: 'ດາວໂຫຼດ',
|
||||
likes: 'ຖືກໃຈ',
|
||||
taskQueue: 'ຄິວດາວໂຫຼດ',
|
||||
progress: 'ຄວາມຄືບໜ້າ',
|
||||
localDownloaded: 'ດາວໂຫຼດແລ້ວ',
|
||||
modelInfo: 'ລາຍລະອຽດໂມເດວ',
|
||||
|
||||
@@ -1028,6 +1028,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'Menjana satu imej minimum sebelum menyimpan dan mungkin dikenakan caj',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Kategori model',
|
||||
modelName: 'Nama model',
|
||||
modelArchitecture: 'Jenis model',
|
||||
backboneParams: 'Parameter teras MoE',
|
||||
activeParams: 'Parameter aktif',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Saiz pemberat',
|
||||
undisclosed: 'Tidak didedahkan',
|
||||
featuredModels: 'Model pilihan',
|
||||
deploy: 'Kerahkan',
|
||||
approxWeightSize: 'Kira-kira {0} GB',
|
||||
services: 'Perkhidmatan model',
|
||||
addService: 'Tambah perkhidmatan model',
|
||||
addEngine: 'Tambah enjin inferens',
|
||||
onlineModel: 'Model dalam talian',
|
||||
instanceDeployment: 'Pengerahan mengikut instans',
|
||||
nvidiaDeployment: 'Pengerahan dioptimumkan untuk NVIDIA',
|
||||
engines: 'Enjin inferens',
|
||||
modelManagement: 'Pengurusan model',
|
||||
serviceDetails: 'Butiran perkhidmatan',
|
||||
deployments: 'Pengerahan setempat',
|
||||
files: 'Fail model',
|
||||
connectExternal: 'Sambungkan perkhidmatan luaran',
|
||||
deploymentTemplates: 'Templat arahan permulaan',
|
||||
engineProvider: 'Enjin / Pembekal',
|
||||
endpoint: 'Titik akhir',
|
||||
cloudAccount: 'Akaun awan',
|
||||
connectionStatus: 'Status sambungan',
|
||||
unverified: 'Belum disahkan',
|
||||
manage: 'Urus',
|
||||
loadFailed: 'Gagal memuatkan. Muat semula dan cuba lagi.',
|
||||
model: 'Model',
|
||||
localModel: 'Model Tempatan',
|
||||
create_helper: 'Tarik "{0}"',
|
||||
@@ -1044,7 +1075,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'Perintah permulaan mesti menggunakan port {0}; jika tidak, perkhidmatan tidak dapat diakses.',
|
||||
ascendVisibleDevices: 'Peranti Ascend boleh dilihat (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Segerakkan ke akaun model',
|
||||
modelAccountAddressHelper:
|
||||
'Alamat kontena sesuai untuk kontena agen; 127.0.0.1, IP sistem, atau alamat tersuai sesuai untuk AI Gateway atau perkhidmatan luaran.',
|
||||
containerAddress: 'Alamat kontena',
|
||||
@@ -1067,7 +1097,6 @@ const message = {
|
||||
sortCreated: 'Terbaru dibuat',
|
||||
downloadsCount: 'Muat turun',
|
||||
likes: 'Suka',
|
||||
taskQueue: 'Baris Gilir Muat Turun',
|
||||
progress: 'Kemajuan',
|
||||
localDownloaded: 'Dimuat turun',
|
||||
modelInfo: 'Butiran Model',
|
||||
|
||||
@@ -1023,6 +1023,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'Gera uma imagem mínima antes de salvar e pode gerar custos',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Categoria do modelo',
|
||||
modelName: 'Nome do modelo',
|
||||
modelArchitecture: 'Tipo de modelo',
|
||||
backboneParams: 'Parâmetros base do MoE',
|
||||
activeParams: 'Parâmetros ativos',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Tamanho dos pesos',
|
||||
undisclosed: 'Não divulgado',
|
||||
featuredModels: 'Modelos em destaque',
|
||||
deploy: 'Implantar',
|
||||
approxWeightSize: 'Aprox. {0} GB',
|
||||
services: 'Serviços de modelos',
|
||||
addService: 'Adicionar serviço de modelo',
|
||||
addEngine: 'Adicionar mecanismo de inferência',
|
||||
onlineModel: 'Modelos online',
|
||||
instanceDeployment: 'Implantação por instância',
|
||||
nvidiaDeployment: 'Implantação otimizada para NVIDIA',
|
||||
engines: 'Mecanismos de inferência',
|
||||
modelManagement: 'Gerenciamento de modelos',
|
||||
serviceDetails: 'Detalhes do serviço',
|
||||
deployments: 'Implantações locais',
|
||||
files: 'Arquivos de modelos',
|
||||
connectExternal: 'Conectar serviço externo',
|
||||
deploymentTemplates: 'Modelos de comandos de inicialização',
|
||||
engineProvider: 'Mecanismo / Provedor',
|
||||
endpoint: 'Endpoint',
|
||||
cloudAccount: 'Conta na nuvem',
|
||||
connectionStatus: 'Status da conexão',
|
||||
unverified: 'Não verificado',
|
||||
manage: 'Gerenciar',
|
||||
loadFailed: 'Falha ao carregar. Atualize a página e tente novamente.',
|
||||
model: 'Modelo',
|
||||
localModel: 'Modelos locais',
|
||||
create_helper: 'Puxar "{0}"',
|
||||
@@ -1039,7 +1070,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'O comando de inicialização deve usar a porta {0}; caso contrário, o serviço ficará inacessível.',
|
||||
ascendVisibleDevices: 'Dispositivos Ascend visíveis (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Sincronizar com conta de modelo',
|
||||
modelAccountAddressHelper:
|
||||
'Use o endereço do contêiner para agentes; 127.0.0.1, IP do sistema ou endereço personalizado para AI Gateway ou serviços externos.',
|
||||
containerAddress: 'Endereço do contêiner',
|
||||
@@ -1062,7 +1092,6 @@ const message = {
|
||||
sortCreated: 'Criados recentemente',
|
||||
downloadsCount: 'Downloads',
|
||||
likes: 'Curtidas',
|
||||
taskQueue: 'Fila de downloads',
|
||||
progress: 'Progresso',
|
||||
localDownloaded: 'Baixados',
|
||||
modelInfo: 'Detalhes do modelo',
|
||||
|
||||
@@ -1017,6 +1017,37 @@ const message = {
|
||||
'Перед сохранением создаётся одно минимальное изображение; возможна оплата',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Категория модели',
|
||||
modelName: 'Название модели',
|
||||
modelArchitecture: 'Тип модели',
|
||||
backboneParams: 'Параметры основы MoE',
|
||||
activeParams: 'Активные параметры',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Размер весов',
|
||||
undisclosed: 'Не раскрыто',
|
||||
featuredModels: 'Рекомендуемые модели',
|
||||
deploy: 'Развернуть',
|
||||
approxWeightSize: 'Около {0} ГБ',
|
||||
services: 'Сервисы моделей',
|
||||
addService: 'Добавить сервис модели',
|
||||
addEngine: 'Добавить движок инференса',
|
||||
onlineModel: 'Онлайн-модели',
|
||||
instanceDeployment: 'Развертывание по экземплярам',
|
||||
nvidiaDeployment: 'Развертывание с оптимизацией для NVIDIA',
|
||||
engines: 'Движки инференса',
|
||||
modelManagement: 'Управление моделями',
|
||||
serviceDetails: 'Сведения о сервисе',
|
||||
deployments: 'Локальные развертывания',
|
||||
files: 'Файлы моделей',
|
||||
connectExternal: 'Подключить внешний сервис',
|
||||
deploymentTemplates: 'Шаблоны команд запуска',
|
||||
engineProvider: 'Движок / Поставщик',
|
||||
endpoint: 'Конечная точка',
|
||||
cloudAccount: 'Облачная учетная запись',
|
||||
connectionStatus: 'Состояние подключения',
|
||||
unverified: 'Не проверено',
|
||||
manage: 'Управление',
|
||||
loadFailed: 'Не удалось загрузить. Обновите страницу и повторите попытку.',
|
||||
model: 'Модель',
|
||||
localModel: 'Локальные модели',
|
||||
create_helper: 'Загрузить "{0}"',
|
||||
@@ -1032,7 +1063,6 @@ const message = {
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: 'Команда запуска должна использовать порт {0}, иначе сервис будет недоступен.',
|
||||
ascendVisibleDevices: 'Видимые устройства Ascend (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Синхронизировать с аккаунтом модели',
|
||||
modelAccountAddressHelper:
|
||||
'Адрес контейнера подходит для контейнеров агентов; 127.0.0.1, системный IP или пользовательский адрес подходит для AI Gateway или внешних сервисов.',
|
||||
containerAddress: 'Адрес контейнера',
|
||||
@@ -1055,7 +1085,6 @@ const message = {
|
||||
sortCreated: 'Недавно создано',
|
||||
downloadsCount: 'Загрузки',
|
||||
likes: 'Лайки',
|
||||
taskQueue: 'Очередь загрузок',
|
||||
progress: 'Прогресс',
|
||||
localDownloaded: 'Загружено',
|
||||
modelInfo: 'Сведения о модели',
|
||||
|
||||
@@ -1027,6 +1027,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: 'Kaydetmeden önce bir küçük görsel üretir ve ücret doğurabilir',
|
||||
},
|
||||
model: {
|
||||
modelCategory: 'Model kategorisi',
|
||||
modelName: 'Model adı',
|
||||
modelArchitecture: 'Model türü',
|
||||
backboneParams: 'MoE omurga parametreleri',
|
||||
activeParams: 'Etkin parametreler',
|
||||
engram: 'Engram',
|
||||
weightFootprint: 'Ağırlık boyutu',
|
||||
undisclosed: 'Açıklanmadı',
|
||||
featuredModels: 'Öne çıkan modeller',
|
||||
deploy: 'Dağıt',
|
||||
approxWeightSize: 'Yaklaşık {0} GB',
|
||||
services: 'Model hizmetleri',
|
||||
addService: 'Model hizmeti ekle',
|
||||
addEngine: 'Çıkarım motoru ekle',
|
||||
onlineModel: 'Çevrimiçi modeller',
|
||||
instanceDeployment: 'Örnek bazında dağıtım',
|
||||
nvidiaDeployment: 'NVIDIA için optimize edilmiş dağıtım',
|
||||
engines: 'Çıkarım motorları',
|
||||
modelManagement: 'Model yönetimi',
|
||||
serviceDetails: 'Hizmet ayrıntıları',
|
||||
deployments: 'Yerel dağıtımlar',
|
||||
files: 'Model dosyaları',
|
||||
connectExternal: 'Harici hizmete bağlan',
|
||||
deploymentTemplates: 'Başlatma komutu şablonları',
|
||||
engineProvider: 'Motor / Sağlayıcı',
|
||||
endpoint: 'Uç nokta',
|
||||
cloudAccount: 'Bulut hesabı',
|
||||
connectionStatus: 'Bağlantı durumu',
|
||||
unverified: 'Doğrulanmadı',
|
||||
manage: 'Yönet',
|
||||
loadFailed: 'Yüklenemedi. Yenileyip tekrar deneyin.',
|
||||
model: 'Model',
|
||||
localModel: 'Yerel Modeller',
|
||||
create_helper: '"{0}" çek',
|
||||
@@ -1043,7 +1074,6 @@ const message = {
|
||||
vllmCommandPortHelper:
|
||||
'Başlatma komutu {0} numaralı bağlantı noktasını kullanmalıdır; aksi halde hizmete erişilemez.',
|
||||
ascendVisibleDevices: 'Görünür Ascend cihazları (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Model hesabına senkronize et',
|
||||
modelAccountAddressHelper:
|
||||
'Konteyner adresi ajan konteynerleri içindir; 127.0.0.1, sistem IP veya özel adres AI Gateway ya da harici servisler içindir.',
|
||||
containerAddress: 'Konteyner adresi',
|
||||
@@ -1066,7 +1096,6 @@ const message = {
|
||||
sortCreated: 'Yeni oluşturulanlar',
|
||||
downloadsCount: 'İndirmeler',
|
||||
likes: 'Beğeniler',
|
||||
taskQueue: 'İndirme Kuyruğu',
|
||||
progress: 'İlerleme',
|
||||
localDownloaded: 'İndirilenler',
|
||||
modelInfo: 'Model ayrıntıları',
|
||||
|
||||
@@ -959,6 +959,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: '儲存前會實際產生一張最小圖片,可能產生費用',
|
||||
},
|
||||
model: {
|
||||
modelCategory: '模型類別',
|
||||
modelName: '模型名稱',
|
||||
modelArchitecture: '模型類型',
|
||||
backboneParams: 'MoE 骨幹參數',
|
||||
activeParams: '啟用參數',
|
||||
engram: 'Engram',
|
||||
weightFootprint: '權重體積',
|
||||
undisclosed: '未公開',
|
||||
featuredModels: '精選模型',
|
||||
deploy: '部署',
|
||||
approxWeightSize: '約 {0} GB',
|
||||
services: '模型服務',
|
||||
addService: '新增模型服務',
|
||||
addEngine: '新增推理引擎',
|
||||
onlineModel: '線上模型',
|
||||
instanceDeployment: '按執行個體部署',
|
||||
nvidiaDeployment: 'NVIDIA 最佳化部署',
|
||||
engines: '推理引擎',
|
||||
modelManagement: '模型管理',
|
||||
serviceDetails: '服務詳情',
|
||||
deployments: '本機部署',
|
||||
files: '模型檔案',
|
||||
connectExternal: '接入外部服務',
|
||||
deploymentTemplates: '啟動命令範本',
|
||||
engineProvider: '引擎 / 供應商',
|
||||
endpoint: '端點',
|
||||
cloudAccount: '雲端帳號',
|
||||
connectionStatus: '連線狀態',
|
||||
unverified: '未驗證',
|
||||
manage: '管理',
|
||||
loadFailed: '載入失敗,請重新整理後重試',
|
||||
model: '模型',
|
||||
localModel: '本地模型',
|
||||
create_helper: '拉取 "{0}"',
|
||||
@@ -973,7 +1004,6 @@ const message = {
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '啟動命令必須使用 {0} 連接埠,否則服務將無法存取。',
|
||||
ascendVisibleDevices: 'Ascend 可見裝置(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '同步到模型帳號',
|
||||
modelAccountAddressHelper:
|
||||
'容器地址適合智能體容器呼叫;127.0.0.1、本機 IP 或自訂地址適合 AI 閘道或外部服務呼叫。',
|
||||
containerAddress: '容器地址',
|
||||
@@ -995,7 +1025,6 @@ const message = {
|
||||
sortCreated: '最新建立',
|
||||
downloadsCount: '下載量',
|
||||
likes: '點讚',
|
||||
taskQueue: '下載佇列',
|
||||
progress: '進度',
|
||||
localDownloaded: '已下載',
|
||||
modelInfo: '模型詳情',
|
||||
|
||||
@@ -972,6 +972,37 @@ const message = {
|
||||
validateImageAvailabilityHelper: '保存前会真实生成一张最小图片,可能产生费用',
|
||||
},
|
||||
model: {
|
||||
modelCategory: '模型类别',
|
||||
modelName: '模型名称',
|
||||
modelArchitecture: '模型类型',
|
||||
backboneParams: 'MoE 骨干参数',
|
||||
activeParams: '激活参数',
|
||||
engram: 'Engram',
|
||||
weightFootprint: '权重体积',
|
||||
undisclosed: '未公开',
|
||||
featuredModels: '精选模型',
|
||||
deploy: '部署',
|
||||
approxWeightSize: '约 {0} GB',
|
||||
services: '模型服务',
|
||||
addService: '添加模型服务',
|
||||
addEngine: '添加推理引擎',
|
||||
onlineModel: '在线模型',
|
||||
instanceDeployment: '按实例部署',
|
||||
nvidiaDeployment: 'NVIDIA 优化部署',
|
||||
engines: '推理引擎',
|
||||
modelManagement: '模型管理',
|
||||
serviceDetails: '服务详情',
|
||||
deployments: '本地部署',
|
||||
files: '模型文件',
|
||||
connectExternal: '接入外部服务',
|
||||
deploymentTemplates: '启动命令模板',
|
||||
engineProvider: '引擎 / 供应商',
|
||||
endpoint: '端点',
|
||||
cloudAccount: '云端账号',
|
||||
connectionStatus: '连接状态',
|
||||
unverified: '未验证',
|
||||
manage: '管理',
|
||||
loadFailed: '加载失败,请刷新重试',
|
||||
model: '模型',
|
||||
localModel: '本地模型',
|
||||
create_helper: '拉取 "{0}"',
|
||||
@@ -986,7 +1017,6 @@ const message = {
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '启动命令必须使用 {0} 端口,否则服务将无法访问。',
|
||||
ascendVisibleDevices: 'Ascend 可见设备(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '同步到模型账号',
|
||||
modelAccountAddressHelper:
|
||||
'容器地址适合智能体容器调用;127.0.0.1、本机 IP 或自定义地址适合 AI 网关或外部服务调用。',
|
||||
containerAddress: '容器地址',
|
||||
@@ -1008,7 +1038,6 @@ const message = {
|
||||
sortCreated: '最新创建',
|
||||
downloadsCount: '下载量',
|
||||
likes: '点赞',
|
||||
taskQueue: '下载队列',
|
||||
progress: '进度',
|
||||
localDownloaded: '已下载',
|
||||
modelInfo: '模型详情',
|
||||
|
||||
@@ -11,16 +11,6 @@ const aiRouter = {
|
||||
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.agent',
|
||||
permission: 'ai_agent_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/model/account',
|
||||
name: 'AIModel',
|
||||
@@ -37,11 +27,32 @@ const aiRouter = {
|
||||
name: 'LocalModel',
|
||||
component: () => import('@/views/ai/model/index.vue'),
|
||||
meta: {
|
||||
title: 'aiTools.model.localModel',
|
||||
title: 'aiTools.model.deployments',
|
||||
activeMenu: '/ai/model/account',
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/model/files',
|
||||
hidden: true,
|
||||
name: 'ModelFiles',
|
||||
component: () => import('@/views/ai/model/index.vue'),
|
||||
meta: {
|
||||
title: 'aiTools.model.files',
|
||||
activeMenu: '/ai/model/account',
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/agents/agent',
|
||||
name: 'Agents',
|
||||
component: () => import('@/views/ai/agents/agent/index.vue'),
|
||||
meta: {
|
||||
icon: 'p-jiqiren2',
|
||||
title: 'aiTools.agents.agent',
|
||||
permission: 'ai_agent_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/mcp',
|
||||
name: 'MCPServer',
|
||||
|
||||
@@ -28,6 +28,14 @@ const volcengineLogo: AgentProviderLogo = {
|
||||
};
|
||||
|
||||
const providerLogos: Record<string, AgentProviderLogo> = {
|
||||
nvidia: {
|
||||
src: asset('nvidia.ico'),
|
||||
mark: 'NV',
|
||||
background: '#ffffff',
|
||||
color: '#76b900',
|
||||
borderColor: '#dcdfe6',
|
||||
source: 'https://www.nvidia.com/favicon.ico',
|
||||
},
|
||||
ollama: {
|
||||
src: asset('ollama.webp'),
|
||||
mark: 'OL',
|
||||
|
||||
@@ -38,6 +38,9 @@ export const jumpToInstall = (type: string, key: string) => {
|
||||
},
|
||||
});
|
||||
return true;
|
||||
case 'ollama':
|
||||
router.push({ path: '/ai/model/local', query: { engine: 'ollama', uncached: 'true', open: 'create' } });
|
||||
return true;
|
||||
case 'vllm':
|
||||
if (globalStore.isProductPro) {
|
||||
router.push({
|
||||
|
||||
@@ -156,7 +156,7 @@ import {
|
||||
} from '@/utils/agent';
|
||||
import { App } from '@/api/interface/app';
|
||||
import AdvancedSetting from '@/components/advanced-setting/index.vue';
|
||||
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
import AccountAddDialog from '@/views/ai/model/components/account-form/index.vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import i18n from '@/lang';
|
||||
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutContent>
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission type="primary" @click="openCreate">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select
|
||||
v-model="apiType"
|
||||
clearable
|
||||
filterable
|
||||
class="api-type-filter"
|
||||
:placeholder="'API ' + $t('commons.table.type')"
|
||||
@change="handleAPITypeChange"
|
||||
>
|
||||
<el-option v-for="item in apiTypeOptions" :key="item" :label="item" :value="item">
|
||||
<ApiTypeTag :api-type="item" />
|
||||
</el-option>
|
||||
</el-select>
|
||||
<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="200" />
|
||||
<el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="200">
|
||||
<template #default="{ row }">
|
||||
<ProviderLabel :provider="row.provider" :display-name="row.providerName" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="'API ' + $t('commons.table.type')" prop="apiType" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<ApiTypeTag :api-type="row.apiType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Base URL" prop="baseUrl" min-width="200" />
|
||||
<el-table-column label="API Key" 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="verificationTagType(row)">
|
||||
{{ verificationLabel(row) }}
|
||||
</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"
|
||||
width="180"
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<AddDialog ref="addRef" @search="search" />
|
||||
<ModelPoolDialog ref="modelPoolRef" @updated="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import AddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
import ModelPoolDialog from '@/views/ai/agents/model/pool/index.vue';
|
||||
import ProviderLabel from '@/components/agent-provider-label/index.vue';
|
||||
import ApiTypeTag from '@/components/api-type-tag/index.vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { isAgentAccountVerificationSkipped } from '@/utils/agent';
|
||||
|
||||
const items = ref<AI.AgentAccountItem[]>([]);
|
||||
const addRef = ref();
|
||||
const modelPoolRef = ref();
|
||||
const searchName = ref('');
|
||||
const apiType = ref('');
|
||||
const apiTypeOptions = ref<string[]>([]);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
click: (row: AI.AgentAccountItem) => onEdit(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.modelPool'),
|
||||
click: (row: AI.AgentAccountItem) => onManageModelPool(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
click: (row: AI.AgentAccountItem) => onDelete(row),
|
||||
},
|
||||
];
|
||||
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
useSearchState(
|
||||
'ai/agents/model/index',
|
||||
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
|
||||
{ onRevisit: () => search() },
|
||||
);
|
||||
|
||||
const search = async () => {
|
||||
const req: AI.AgentAccountSearch = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
provider: '',
|
||||
apiType: apiType.value,
|
||||
name: searchName.value || '',
|
||||
};
|
||||
const res = await pageAgentAccounts(req);
|
||||
items.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total || 0;
|
||||
};
|
||||
|
||||
const handleAPITypeChange = () => {
|
||||
paginationConfig.currentPage = 1;
|
||||
search();
|
||||
};
|
||||
|
||||
const loadAPITypes = async () => {
|
||||
const res = await getAgentProviders();
|
||||
apiTypeOptions.value = Array.from(
|
||||
new Set((res.data || []).flatMap((provider) => provider.apiTypes.map((item) => item.apiType))),
|
||||
).sort();
|
||||
};
|
||||
|
||||
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,
|
||||
rememberApiKey: row.rememberApiKey,
|
||||
apiType: row.apiType,
|
||||
authMode: row.authMode,
|
||||
verifyModel: row.verifyModel,
|
||||
models: row.models,
|
||||
remark: row.remark,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onManageModelPool = (row: AI.AgentAccountItem) => {
|
||||
if (modelPoolRef.value?.open) {
|
||||
modelPoolRef.value.open(row);
|
||||
}
|
||||
};
|
||||
|
||||
const verificationLabel = (row: AI.AgentAccountItem) => {
|
||||
if (isAgentAccountVerificationSkipped(row.provider)) {
|
||||
return i18n.global.t('aiTools.agents.verifySkipped');
|
||||
}
|
||||
return row.verified ? 'OK' : 'N/A';
|
||||
};
|
||||
|
||||
const verificationTagType = (row: AI.AgentAccountItem) => {
|
||||
if (isAgentAccountVerificationSkipped(row.provider)) {
|
||||
return 'info';
|
||||
}
|
||||
return row.verified ? 'success' : 'info';
|
||||
};
|
||||
|
||||
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.delete', [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 Promise.all([search(), loadAPITypes()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-type-filter {
|
||||
width: 220px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="form-section-title">{{ $t('aiTools.agents.account') }}</div>
|
||||
<span class="input-help model-account-helper">{{ $t('aiTools.model.modelAccountAddressHelper') }}</span>
|
||||
<el-form-item
|
||||
:label="$t('aiTools.agents.allowedOrigins')"
|
||||
prop="modelAccountBaseURLType"
|
||||
:rules="Rules.requiredSelect"
|
||||
>
|
||||
<el-radio-group v-model="form.modelAccountBaseURLType">
|
||||
<el-radio-button
|
||||
v-for="item in addressTypes"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:disabled="hostMode && item.value === 'container'"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.modelAccountBaseURLType === 'custom'"
|
||||
label="Base URL"
|
||||
prop="modelAccountBaseURL"
|
||||
:rules="customURLRules"
|
||||
>
|
||||
<el-input v-model.trim="form.modelAccountBaseURL" placeholder="http://192.168.1.100:8000/v1" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="Base URL">
|
||||
<el-input :model-value="baseURL" readonly />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const props = defineProps<{
|
||||
form: {
|
||||
name: string;
|
||||
containerName: string;
|
||||
modelAccountBaseURLType: string;
|
||||
modelAccountBaseURL: string;
|
||||
};
|
||||
hostPort: number;
|
||||
containerPort: string;
|
||||
hostMode?: boolean;
|
||||
}>();
|
||||
const systemIP = ref('');
|
||||
const addressTypes = computed(() => [
|
||||
{ label: i18n.global.t('aiTools.model.containerAddress'), value: 'container' },
|
||||
{ label: '127.0.0.1', value: 'localhost' },
|
||||
{ label: i18n.global.t('setting.systemIP'), value: 'systemIP' },
|
||||
{ label: i18n.global.t('aiTools.model.customAddress'), value: 'custom' },
|
||||
]);
|
||||
const customURLRules = [
|
||||
Rules.requiredInput,
|
||||
{ pattern: /^https?:\/\/.+/i, message: i18n.global.t('aiTools.agents.allowedOriginsInvalid'), trigger: 'blur' },
|
||||
];
|
||||
const baseURL = computed(() => {
|
||||
if (props.form.modelAccountBaseURLType === 'custom') return props.form.modelAccountBaseURL;
|
||||
if (props.form.modelAccountBaseURLType === 'localhost') return `http://127.0.0.1:${props.hostPort}/v1`;
|
||||
if (props.form.modelAccountBaseURLType === 'systemIP')
|
||||
return `http://${systemIP.value || '127.0.0.1'}:${props.hostPort}/v1`;
|
||||
return `http://${props.form.containerName || props.form.name}:${props.containerPort}/v1`;
|
||||
});
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await getAgentSettingInfo();
|
||||
systemIP.value = data.systemIP || '';
|
||||
} catch {
|
||||
systemIP.value = '';
|
||||
}
|
||||
});
|
||||
defineExpose({ baseURL });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.model-account-helper {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
+21
-6
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="headerTitle" size="large" @close="handleClose">
|
||||
<component
|
||||
:is="inline ? 'div' : DrawerPro"
|
||||
v-bind="inline ? {} : { modelValue: open, header: headerTitle, size: 'large' }"
|
||||
@update:model-value="open = $event"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" autocomplete="off" v-loading="loading">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
@@ -229,7 +234,7 @@
|
||||
<span class="input-help">{{ $t('aiTools.agents.syncAgentsHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<template v-if="!inline" #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button v-permission :disabled="loading" type="primary" @click="submit">
|
||||
@@ -237,7 +242,7 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -257,7 +262,10 @@ import { MsgError } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import ProviderLogo from '@/components/agent-provider-logo/index.vue';
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
import DrawerPro from '@/components/drawer-pro/index.vue';
|
||||
|
||||
const props = defineProps({ inline: Boolean });
|
||||
const emit = defineEmits(['search', 'close']);
|
||||
|
||||
const open = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -291,7 +299,7 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const headerTitle = computed(() =>
|
||||
form.id ? i18n.global.t('commons.button.edit') : i18n.global.t('commons.button.create'),
|
||||
form.id ? i18n.global.t('commons.button.edit') : i18n.global.t('aiTools.model.connectExternal'),
|
||||
);
|
||||
|
||||
const selectedProvider = computed(() => providers.value[form.provider]);
|
||||
@@ -641,6 +649,7 @@ const submit = async () => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
formRef.value?.resetFields();
|
||||
loading.value = false;
|
||||
form.id = 0;
|
||||
@@ -769,12 +778,18 @@ watch([() => form.apiKey, () => form.baseURL], () => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(open, (value) => {
|
||||
if (props.inline && !value) handleClose();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProviders();
|
||||
if (!props.inline) await loadProviders();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open: openDrawer,
|
||||
submit,
|
||||
loading,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<DialogPro
|
||||
v-model="drawerVisible"
|
||||
:title="$t(engine === 'vllm' ? 'aiTools.model.serviceDetails' : 'database.databaseConnInfo')"
|
||||
:size="isMobile ? 'full' : 'normal'"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="connection-details">
|
||||
<div v-if="form.name" class="service-heading">
|
||||
<el-tag effect="plain">
|
||||
{{ engine === 'ollama' ? 'Ollama' : engine === 'vllm' ? 'vLLM' : 'TensorRT LLM' }}
|
||||
</el-tag>
|
||||
<strong>{{ form.name }}</strong>
|
||||
</div>
|
||||
<section v-if="engine === 'vllm'" class="detail-card">
|
||||
<h3 class="detail-title">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
{{ $t('aiTools.model.modelDir') }}
|
||||
</h3>
|
||||
<div class="detail-value model-directory">
|
||||
<code>{{ form.modelDir || '—' }}</code>
|
||||
<CopyButton v-if="form.modelDir" :content="form.modelDir" />
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-card">
|
||||
<h3 class="detail-title">
|
||||
<el-icon><Connection /></el-icon>
|
||||
{{ $t('database.containerConn') }}
|
||||
</h3>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('app.containerName') }}</span>
|
||||
<div class="detail-value">
|
||||
<code>{{ form.containerName }}</code>
|
||||
<CopyButton :content="form.containerName" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.containerPort" class="detail-row">
|
||||
<span class="detail-label">{{ $t('commons.table.port') }}</span>
|
||||
<div class="detail-value">
|
||||
<code>{{ form.containerPort }}</code>
|
||||
<CopyButton :content="form.containerPort" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="detail-help">{{ $t('aiTools.model.container_conn_helper') }}</p>
|
||||
</section>
|
||||
<section class="detail-card">
|
||||
<h3 class="detail-title">
|
||||
<el-icon><Link /></el-icon>
|
||||
{{ $t(bindDomain.connUrl ? 'setting.proxyUrl' : 'database.remoteConn') }}
|
||||
</h3>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">{{ $t('database.connAddress') }}</span>
|
||||
<div class="detail-value">
|
||||
<code>{{ externalAddress }}</code>
|
||||
<CopyButton :content="externalAddress" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="engine !== 'ollama' && !bindDomain.connUrl && form.port && !form.endpoint"
|
||||
class="detail-row"
|
||||
>
|
||||
<span class="detail-label">{{ $t('commons.table.port') }}</span>
|
||||
<div class="detail-value">
|
||||
<code>{{ form.port }}</code>
|
||||
<CopyButton :content="form.port + ''" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="detail-help">{{ $t('database.remoteConnHelper2') }}</p>
|
||||
</section>
|
||||
<el-button v-if="engine === 'ollama'" @click="openWebUI">OpenWebUI</el-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">
|
||||
{{ $t('commons.button.close') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
<PortJumpDialog ref="portRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { Connection, FolderOpened, Link } from '@element-plus/icons-vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { getAppPort } from '@/api/modules/app';
|
||||
import { useRouter } from 'vue-router';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { getBindDomain } from '@/api/modules/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const { currentNode, isMobile } = useGlobalStore();
|
||||
const emit = defineEmits(['close']);
|
||||
const router = useRouter();
|
||||
const portRef = ref();
|
||||
const engine = ref('ollama');
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const form = reactive({
|
||||
name: '',
|
||||
modelDir: '',
|
||||
systemIP: '',
|
||||
containerName: '',
|
||||
port: 0 as number | string,
|
||||
containerPort: '11434',
|
||||
endpoint: '',
|
||||
});
|
||||
const bindDomain = ref({
|
||||
connUrl: '',
|
||||
});
|
||||
const externalAddress = computed(() => {
|
||||
if (bindDomain.value.connUrl) return bindDomain.value.connUrl;
|
||||
if (form.endpoint) return form.endpoint;
|
||||
if (engine.value !== 'ollama' || !form.port || !form.systemIP) return form.systemIP;
|
||||
const host = form.systemIP.includes(':') && !form.systemIP.startsWith('[') ? `[${form.systemIP}]` : form.systemIP;
|
||||
return `http://${host}:${form.port}`;
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
name?: string;
|
||||
modelDir?: string;
|
||||
port: number | string;
|
||||
containerPort?: string;
|
||||
endpoint?: string;
|
||||
engine?: string;
|
||||
containerName: string;
|
||||
appinstallID?: number;
|
||||
}
|
||||
|
||||
const acceptParams = (param: DialogProps): void => {
|
||||
engine.value = param.engine || 'ollama';
|
||||
form.name = param.name || '';
|
||||
form.modelDir = param.modelDir || '';
|
||||
form.containerPort = param.containerPort ?? '11434';
|
||||
form.endpoint = param.endpoint || '';
|
||||
bindDomain.value.connUrl = '';
|
||||
form.containerName = param.containerName;
|
||||
form.port = param.port;
|
||||
loadSystemIP();
|
||||
loadBindDomain(param.appinstallID);
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const openWebUI = async () => {
|
||||
const node = currentNode.value;
|
||||
const { data } = await getAppPort('ollama-webui', '');
|
||||
if (node !== currentNode.value || !drawerVisible.value) return;
|
||||
if (data) {
|
||||
portRef.value.acceptParams({ port: data });
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(i18n.global.t('app.checkInstalledWarn', ['OpenWebUI']), 'OpenWebUI', {
|
||||
confirmButtonText: i18n.global.t('database.goInstall'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
});
|
||||
if (node !== currentNode.value || !drawerVisible.value) return;
|
||||
await router.push({ name: 'AppAll', query: { install: 'ollama-webui' } });
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const loadSystemIP = async () => {
|
||||
if (currentNode.value !== 'local') {
|
||||
form.systemIP = currentNode.value || i18n.global.t('database.localIP');
|
||||
return;
|
||||
}
|
||||
const res = await getAgentSettingInfo();
|
||||
form.systemIP = res.data.systemIP || i18n.global.t('database.localIP');
|
||||
};
|
||||
|
||||
const loadBindDomain = async (appInstallID: number) => {
|
||||
if (appInstallID == undefined || appInstallID <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getBindDomain({
|
||||
appInstallID: appInstallID,
|
||||
});
|
||||
if (res.data.websiteID > 0) {
|
||||
bindDomain.value.connUrl = res.data.connUrl;
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.connection-details {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.service-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
strong {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.detail-card {
|
||||
padding: 18px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
}
|
||||
.detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
.el-icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
& + & {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
.detail-label,
|
||||
.detail-help {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.detail-value {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
:deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.model-directory {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.detail-help {
|
||||
margin: 14px 0 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.detail-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="visible"
|
||||
:header="$t(enginesOnly ? 'aiTools.model.addEngine' : 'aiTools.model.addService')"
|
||||
:size="isMobile ? 'full' : '50%'"
|
||||
>
|
||||
<div class="engine-options">
|
||||
<el-button
|
||||
v-if="!enginesOnly && !vllmOnly"
|
||||
class="engine-option"
|
||||
:class="{ 'is-selected': selected === 'account' }"
|
||||
:aria-pressed="selected === 'account'"
|
||||
@click="selected = 'account'"
|
||||
>
|
||||
<el-icon><Cloudy /></el-icon>
|
||||
<span class="engine-content">
|
||||
<span class="engine-name">{{ $t('aiTools.model.onlineModel') }}</span>
|
||||
<span class="engine-description">{{ $t('aiTools.model.cloudAccount') }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
class="engine-option"
|
||||
:class="{ 'is-selected': selected === 'vllm' }"
|
||||
:aria-pressed="selected === 'vllm'"
|
||||
@click="selected = 'vllm'"
|
||||
>
|
||||
<ProviderLogo provider="vllm" :size="18" />
|
||||
<span class="engine-content">
|
||||
<span class="engine-name">vLLM</span>
|
||||
<span class="engine-description">{{ $t('aiTools.model.instanceDeployment') }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!vllmOnly"
|
||||
class="engine-option"
|
||||
:class="{ 'is-selected': selected === 'ollama' }"
|
||||
:aria-pressed="selected === 'ollama'"
|
||||
@click="selected = 'ollama'"
|
||||
>
|
||||
<ProviderLogo provider="ollama" :size="18" />
|
||||
<span class="engine-content">
|
||||
<span class="engine-name">Ollama</span>
|
||||
<span class="engine-description" :class="{ 'engine-running': ollama?.status === 'Running' }">
|
||||
{{
|
||||
ollama?.isExist
|
||||
? $t('commons.status.' + ollama.status.toLowerCase())
|
||||
: $t('aiTools.model.modelManagement')
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isFxplay && !vllmOnly"
|
||||
class="engine-option"
|
||||
:class="{ 'is-selected': selected === 'tensorrt' }"
|
||||
:aria-pressed="selected === 'tensorrt'"
|
||||
@click="selected = 'tensorrt'"
|
||||
>
|
||||
<ProviderLogo provider="nvidia" :size="18" />
|
||||
<span class="engine-content">
|
||||
<span class="engine-name">TensorRT LLM</span>
|
||||
<span class="engine-description">{{ $t('aiTools.model.nvidiaDeployment') }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider />
|
||||
<DockerStatus
|
||||
v-if="visible"
|
||||
v-show="selected !== 'account'"
|
||||
v-model:is-active="dockerActive"
|
||||
v-model:is-exist="dockerExists"
|
||||
/>
|
||||
<div v-loading="initializing" class="service-form">
|
||||
<el-alert v-if="loadFailed" :title="$t('aiTools.model.loadFailed')" type="error" :closable="false" />
|
||||
<AccountForm
|
||||
v-if="activeForm === 'account'"
|
||||
ref="accountRef"
|
||||
inline
|
||||
@search="emit('search')"
|
||||
@close="visible = false"
|
||||
/>
|
||||
<component
|
||||
:is="vllmForm"
|
||||
v-if="activeForm === 'vllm' && vllmForm"
|
||||
ref="vllmRef"
|
||||
inline
|
||||
@search="emit('search')"
|
||||
@close="visible = false"
|
||||
@task="openTask"
|
||||
/>
|
||||
<OllamaForm
|
||||
v-if="activeForm === 'ollama'"
|
||||
ref="ollamaRef"
|
||||
inline
|
||||
@search="emit('search')"
|
||||
@close="visible = false"
|
||||
@task="openTask"
|
||||
/>
|
||||
<TensorForm
|
||||
v-if="activeForm === 'tensorrt'"
|
||||
ref="tensorRef"
|
||||
inline
|
||||
@search="emit('search')"
|
||||
@close="visible = false"
|
||||
/>
|
||||
<template v-if="selected === 'vllm' && (!isProductPro || !vllmAvailable)">
|
||||
<el-alert :title="$t('license.vllm')" type="info" :closable="false" />
|
||||
<el-button class="mt-4" type="primary" @click="openVllmLicense">
|
||||
{{ $t('license.levelUpPro') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-if="selected === 'ollama' && ollama?.isExist">
|
||||
<el-button type="primary" :disabled="!ollama" @click="openOllama">
|
||||
{{ $t(ollama?.isExist ? 'aiTools.model.manage' : 'commons.button.install') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="activeForm" #footer>
|
||||
<el-button :disabled="currentForm?.loading" @click="visible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
type="primary"
|
||||
:loading="initializing || currentForm?.loading"
|
||||
:disabled="initializing || loadFailed || !currentForm || currentForm.disabled"
|
||||
@click="currentForm.submit()"
|
||||
>
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<AccountForm ref="editAccountRef" @search="emit('search')" />
|
||||
<TaskLog ref="taskRef" @close="emit('search')" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue';
|
||||
import { Cloudy } from '@element-plus/icons-vue';
|
||||
import ProviderLogo from '@/components/agent-provider-logo/index.vue';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import type { App } from '@/api/interface/app';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { loadOptionalComponent } from '@/extensions/optional';
|
||||
import AccountForm from '../account-form/index.vue';
|
||||
import TensorForm from '@/views/ai/model/tensorrt/operate/index.vue';
|
||||
import OllamaForm from '@/views/ai/model/ollama/operate/index.vue';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import { checkAppInstalled } from '@/api/modules/app';
|
||||
import { vllmAvailable } from '../../deployment';
|
||||
|
||||
const props = defineProps({ enginesOnly: Boolean, vllmOnly: Boolean });
|
||||
const emit = defineEmits(['search', 'manage']);
|
||||
const { isProductPro, isFxplay, isMobile } = useGlobalStore();
|
||||
const visible = ref(false);
|
||||
const selected = ref<'account' | 'vllm' | 'ollama' | 'tensorrt'>('account');
|
||||
const modelDir = ref('');
|
||||
const dockerActive = ref(false);
|
||||
const dockerExists = ref(true);
|
||||
const ollama = ref<App.CheckInstalled>();
|
||||
const accountRef = ref();
|
||||
const editAccountRef = ref();
|
||||
const tensorRef = ref();
|
||||
const ollamaRef = ref();
|
||||
const vllmRef = ref();
|
||||
const taskRef = ref();
|
||||
const vllmForm = shallowRef();
|
||||
const initializing = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const activeForm = computed(() => {
|
||||
if (!visible.value) return '';
|
||||
if (selected.value === 'account') return 'account';
|
||||
if (!dockerActive.value || !dockerExists.value) return '';
|
||||
if (selected.value === 'vllm' && isProductPro.value && vllmAvailable) return 'vllm';
|
||||
if (selected.value === 'ollama' && ollama.value && !ollama.value.isExist) return 'ollama';
|
||||
if (selected.value === 'tensorrt' && isFxplay.value) return 'tensorrt';
|
||||
return '';
|
||||
});
|
||||
|
||||
const currentForm = computed(() => {
|
||||
if (activeForm.value === 'account') return accountRef.value;
|
||||
if (activeForm.value === 'vllm') return vllmRef.value;
|
||||
if (activeForm.value === 'ollama') return ollamaRef.value;
|
||||
if (activeForm.value === 'tensorrt') return tensorRef.value;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
watch(
|
||||
activeForm,
|
||||
async (form, _, onCleanup) => {
|
||||
let cancelled = false;
|
||||
onCleanup(() => (cancelled = true));
|
||||
loadFailed.value = false;
|
||||
initializing.value = Boolean(form);
|
||||
if (!form) return;
|
||||
try {
|
||||
if (form === 'vllm' && !vllmForm.value) {
|
||||
const component = await loadOptionalComponent('/src/xpack/views/vllm/operate/index.vue');
|
||||
if (cancelled) return;
|
||||
vllmForm.value = component;
|
||||
}
|
||||
await nextTick();
|
||||
if (cancelled) return;
|
||||
if (form === 'account') await accountRef.value.open();
|
||||
if (form === 'vllm') await vllmRef.value.openCreate(modelDir.value);
|
||||
if (form === 'ollama') await ollamaRef.value.openCreate();
|
||||
if (form === 'tensorrt') tensorRef.value.openCreate();
|
||||
} catch {
|
||||
if (!cancelled) loadFailed.value = true;
|
||||
} finally {
|
||||
if (!cancelled) initializing.value = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
const open = async (engine?: 'ollama' | 'vllm' | 'tensorrt', defaultModelDir = '') => {
|
||||
loadFailed.value = false;
|
||||
modelDir.value = defaultModelDir;
|
||||
selected.value = props.vllmOnly ? 'vllm' : engine || (props.enginesOnly ? 'vllm' : 'account');
|
||||
dockerActive.value = false;
|
||||
dockerExists.value = true;
|
||||
ollama.value = undefined;
|
||||
visible.value = true;
|
||||
if (props.vllmOnly) return;
|
||||
const { data } = await checkAppInstalled('ollama', '');
|
||||
if (visible.value) ollama.value = data;
|
||||
};
|
||||
|
||||
const openAccount = (account: AI.AgentAccountItem) => {
|
||||
editAccountRef.value.open({ ...account, baseURL: account.baseUrl });
|
||||
};
|
||||
|
||||
const openOllama = async () => {
|
||||
if (!ollama.value) return;
|
||||
visible.value = false;
|
||||
emit('manage', { engine: 'ollama', id: ollama.value.appInstallId, name: ollama.value.name });
|
||||
};
|
||||
|
||||
const openVllmLicense = () => {
|
||||
visible.value = false;
|
||||
emit('manage', { engine: 'vllm', id: 0, name: '' });
|
||||
};
|
||||
const openTask = (taskID: string) => taskRef.value.openWithTaskID(taskID);
|
||||
|
||||
defineExpose({ open, openAccount });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.engine-options {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
.engine-option {
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 10px 8px;
|
||||
border-radius: 6px;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
:deep(> span) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.el-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-primary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
&.is-selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
.engine-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.engine-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.engine-description {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.engine-name,
|
||||
.engine-description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.engine-running {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.service-form {
|
||||
min-height: 100px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.engine-options {
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="drawerVisible"
|
||||
:header="header"
|
||||
:resource="instance?.name"
|
||||
:size="isMobile ? 'full' : 'large'"
|
||||
@close="emit('search')"
|
||||
>
|
||||
<template #content>
|
||||
<div class="deployment-action" v-loading="loading">
|
||||
<el-alert v-if="failed" :title="$t('aiTools.model.loadFailed')" type="error" :closable="false" />
|
||||
<el-empty v-else-if="missing" :description="$t('commons.res.notFound')" />
|
||||
<template v-else-if="visible && instance && !loading && view !== VllmPlaceholder">
|
||||
<div v-if="instance.engine !== 'ollama'" class="service-card">
|
||||
<div class="flex flex-wrap gap-3 items-center mb-4">
|
||||
<strong>{{ engineLabels[instance.engine] }} {{ instance.version }}</strong>
|
||||
<Status :status="instance.status" />
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<el-button
|
||||
v-for="button in visibleButtons"
|
||||
:key="button.label"
|
||||
:disabled="
|
||||
deleteLoading ||
|
||||
(button.disabled?.(instance) ?? false) ||
|
||||
(button.permission && !hasManagePermissionAccess())
|
||||
"
|
||||
@click="button.click(instance)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions v-if="instance.engine !== 'ollama'" :column="1" border>
|
||||
<el-descriptions-item :label="$t('commons.table.name')">
|
||||
{{ instance.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('menu.container')">
|
||||
{{ instance.containerName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('aiTools.model.modelDir')">
|
||||
{{ instance.modelDir || '—' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<OllamaModels
|
||||
v-if="visible && !standalone && instance?.engine === 'ollama' && !failed && !missing"
|
||||
:key="instance.id"
|
||||
:instance-name="instance.name"
|
||||
/>
|
||||
<VllmPlaceholder v-if="visible && view === VllmPlaceholder" />
|
||||
</div>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<component
|
||||
:is="view"
|
||||
v-if="visible && view && view !== VllmPlaceholder && !failed && !missing"
|
||||
ref="contentRef"
|
||||
v-bind="
|
||||
view === EngineConnection
|
||||
? {}
|
||||
: view === ContainerLogs
|
||||
? { highlightDiff: 210 }
|
||||
: view === TaskLog
|
||||
? { width: '70%' }
|
||||
: { dialog: true }
|
||||
"
|
||||
@close="returnToManagement"
|
||||
@search="emit('search')"
|
||||
@task="openTask"
|
||||
/>
|
||||
<DialogPro
|
||||
v-model="deleteVisible"
|
||||
:title="$t('commons.button.delete') + ' - ' + (deleting?.name || '')"
|
||||
size="small"
|
||||
>
|
||||
<p>{{ $t('commons.msg.delete', [deleting?.name || '']) }}</p>
|
||||
<el-checkbox v-if="deleting?.engine === 'vllm'" v-model="forceDelete" :label="$t('website.forceDelete')" />
|
||||
<template #footer>
|
||||
<el-button :disabled="deleteLoading" @click="deleteVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button v-permission type="primary" :loading="deleteLoading" @click="deleteDeployment">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, nextTick, ref, shallowRef, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { loadOptionalComponent } from '@/extensions/optional';
|
||||
import { checkAppInstalled, searchAppInstalled } from '@/api/modules/app';
|
||||
import { pageTensorRTLLM, deleteTensorRTLLM } from '@/api/modules/ai';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import { hasPermission } from '@/utils/rbac';
|
||||
import { hasManagePermissionAccess } from '@/utils/permission';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import OllamaModels from '../../ollama/models/index.vue';
|
||||
import EngineConnection from '../connection/index.vue';
|
||||
import TensorForm from '../../tensorrt/operate/index.vue';
|
||||
import VllmPlaceholder from '../../vllm/index.vue';
|
||||
import ContainerLogs from '@/components/log/container-drawer/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import { readPages, loadVllmAPI, vllmAvailable, type Deployment } from '../../deployment';
|
||||
|
||||
const emit = defineEmits(['search', 'upgrade']);
|
||||
const { isMobile, currentNode, isProductPro, isFxplay } = useGlobalStore();
|
||||
const route = useRoute();
|
||||
const visible = ref(false);
|
||||
const standalone = ref(false);
|
||||
const drawerVisible = computed({
|
||||
get: () => visible.value && !standalone.value,
|
||||
set: (value) => {
|
||||
visible.value = value;
|
||||
},
|
||||
});
|
||||
const loading = ref(false);
|
||||
const failed = ref(false);
|
||||
const missing = ref(false);
|
||||
const header = ref('');
|
||||
const instance = ref<Deployment>();
|
||||
const actionName = ref('manage');
|
||||
const deleteVisible = ref(false);
|
||||
const deleting = ref<Deployment>();
|
||||
const forceDelete = ref(false);
|
||||
const deleteLoading = ref(false);
|
||||
const deleted = ref(false);
|
||||
const engineLabels = { vllm: 'vLLM', ollama: 'Ollama', tensorrt: 'TensorRT LLM' };
|
||||
const view = shallowRef();
|
||||
const contentRef = ref();
|
||||
let request = 0;
|
||||
const openTask = async (taskID: string) => {
|
||||
emit('search');
|
||||
visible.value = true;
|
||||
actionName.value = 'task';
|
||||
view.value = markRaw(TaskLog);
|
||||
await nextTick();
|
||||
contentRef.value?.openWithTaskID(taskID);
|
||||
};
|
||||
const openUpgrade = async (installId: number, version: number) => {
|
||||
const installed = await readPages((page) =>
|
||||
searchAppInstalled({ ...page, name: instance.value.name, update: true }),
|
||||
);
|
||||
if (version !== request || !visible.value) return;
|
||||
const app = installed.find((item) => item.id === installId);
|
||||
if (!app) {
|
||||
missing.value = true;
|
||||
if (standalone.value) MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
if (standalone.value) visible.value = false;
|
||||
emit('upgrade', app);
|
||||
};
|
||||
const open = async (
|
||||
row: Pick<Deployment, 'engine' | 'id' | 'name'> & Partial<Deployment>,
|
||||
action = 'manage',
|
||||
outside = false,
|
||||
) => {
|
||||
const version = ++request;
|
||||
standalone.value = outside;
|
||||
if (row.engine === 'tensorrt' && !isFxplay.value) return;
|
||||
instance.value = { version: '', containerName: '', status: '', modelDir: '', models: [], ...row };
|
||||
actionName.value = action;
|
||||
deleted.value = false;
|
||||
header.value = `${engineLabels[row.engine]} ${i18n.global.t('aiTools.model.manage')}`;
|
||||
view.value = undefined;
|
||||
failed.value = false;
|
||||
missing.value = false;
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
if (row.engine === 'vllm' && (!isProductPro.value || !vllmAvailable)) {
|
||||
view.value = markRaw(VllmPlaceholder);
|
||||
return;
|
||||
}
|
||||
if (row.engine === 'ollama') {
|
||||
const { data } = await checkAppInstalled('ollama', row.name);
|
||||
if (version !== request || !visible.value) return;
|
||||
if (!data.isExist) {
|
||||
missing.value = true;
|
||||
if (standalone.value) MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
instance.value = {
|
||||
...instance.value,
|
||||
id: data.appInstallId,
|
||||
version: data.version,
|
||||
status: data.status,
|
||||
containerName: data.containerName,
|
||||
};
|
||||
if (action === 'upgrade') {
|
||||
await openUpgrade(data.appInstallId, version);
|
||||
return;
|
||||
}
|
||||
const component = action === 'connection' ? EngineConnection : action === 'log' ? ContainerLogs : undefined;
|
||||
view.value = component ? markRaw(component) : undefined;
|
||||
await nextTick();
|
||||
if (action === 'connection')
|
||||
contentRef.value?.acceptParams({
|
||||
name: data.name,
|
||||
engine: 'ollama',
|
||||
port: data.httpPort,
|
||||
containerName: data.containerName,
|
||||
appinstallID: data.appInstallId,
|
||||
});
|
||||
if (action === 'log')
|
||||
contentRef.value?.acceptParams({ containerID: data.containerName, container: data.containerName });
|
||||
return;
|
||||
}
|
||||
const items =
|
||||
row.engine === 'vllm'
|
||||
? await readPages((page) => loadVllmAPI().then((api) => api.pageVllm({ ...page, info: row.name })))
|
||||
: await readPages((page) => pageTensorRTLLM({ ...page, name: row.name }));
|
||||
const detail = items.find((item) => item.id === row.id);
|
||||
if (version !== request || !visible.value) return;
|
||||
if (!detail) {
|
||||
missing.value = true;
|
||||
if (standalone.value) MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
instance.value = {
|
||||
...instance.value,
|
||||
status: detail.status,
|
||||
containerName: detail.containerName,
|
||||
modelDir: detail.modelDir,
|
||||
version: 'appVersion' in detail ? detail.appVersion : detail.version,
|
||||
upgradable: 'upgradable' in detail && detail.upgradable,
|
||||
};
|
||||
if (action === 'connection') {
|
||||
view.value = markRaw(EngineConnection);
|
||||
await nextTick();
|
||||
contentRef.value?.acceptParams({
|
||||
containerName: detail.containerName,
|
||||
port:
|
||||
'port' in detail
|
||||
? detail.port
|
||||
: (detail.exposedPorts || []).map((item) => item.hostPort).join(', '),
|
||||
containerPort:
|
||||
'port' in detail ? '' : (detail.exposedPorts || []).map((item) => item.containerPort).join(', '),
|
||||
endpoint: 'modelAccountBaseURL' in detail ? detail.modelAccountBaseURL : '',
|
||||
engine: row.engine,
|
||||
name: row.name,
|
||||
modelDir: detail.modelDir,
|
||||
});
|
||||
} else if (action === 'delete') {
|
||||
deleting.value = instance.value;
|
||||
forceDelete.value = false;
|
||||
deleteVisible.value = true;
|
||||
} else if (action === 'config') {
|
||||
const component =
|
||||
row.engine === 'vllm'
|
||||
? await loadOptionalComponent('/src/xpack/views/vllm/operate/index.vue')
|
||||
: TensorForm;
|
||||
if (version !== request || !visible.value) return;
|
||||
view.value = markRaw(component);
|
||||
await nextTick();
|
||||
await contentRef.value?.openEdit(detail);
|
||||
} else if (action === 'monitor') {
|
||||
const component = await loadOptionalComponent('/src/xpack/views/vllm/monitor/index.vue');
|
||||
if (version !== request || !visible.value) return;
|
||||
view.value = markRaw(component);
|
||||
await nextTick();
|
||||
contentRef.value?.acceptParams(detail);
|
||||
} else if (action === 'upgrade' && 'appInstallId' in detail) {
|
||||
await openUpgrade(detail.appInstallId, version);
|
||||
} else if (action === 'log') {
|
||||
const task = 'appInstallId' in detail && ['Installing', 'Uninstalling'].includes(detail.status);
|
||||
view.value = markRaw(task ? TaskLog : ContainerLogs);
|
||||
await nextTick();
|
||||
if (task) {
|
||||
contentRef.value?.openWithResourceID(
|
||||
'App',
|
||||
detail.status === 'Installing' ? 'TaskInstall' : 'TaskUninstall',
|
||||
detail.appInstallId,
|
||||
);
|
||||
} else {
|
||||
contentRef.value?.acceptParams({
|
||||
containerID: detail.containerName,
|
||||
container: detail.containerName,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (version === request) {
|
||||
failed.value = true;
|
||||
if (standalone.value) MsgError(i18n.global.t('aiTools.model.loadFailed'));
|
||||
}
|
||||
} finally {
|
||||
if (version === request) loading.value = false;
|
||||
}
|
||||
};
|
||||
watch(visible, (value) => {
|
||||
if (!value) request++;
|
||||
});
|
||||
watch([currentNode, () => route.fullPath, isProductPro, isFxplay], () => {
|
||||
request++;
|
||||
visible.value = false;
|
||||
deleteVisible.value = false;
|
||||
deleting.value = undefined;
|
||||
});
|
||||
const operationDisabled = (row: Deployment) =>
|
||||
deleteLoading.value ||
|
||||
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Waiting', 'Uninstalling'].includes(row.status);
|
||||
const deleteDeployment = async () => {
|
||||
const version = request;
|
||||
const row = deleting.value;
|
||||
if (!row) return;
|
||||
deleteLoading.value = true;
|
||||
try {
|
||||
if (row.engine === 'tensorrt') {
|
||||
await deleteTensorRTLLM({ id: row.id });
|
||||
} else {
|
||||
const taskID = newUUID();
|
||||
await (
|
||||
await loadVllmAPI()
|
||||
).operateVllm({ id: row.id, operate: 'delete', taskID, forceDelete: forceDelete.value });
|
||||
if (version !== request || !visible.value) return;
|
||||
await openTask(taskID);
|
||||
}
|
||||
if (version !== request || !visible.value) return;
|
||||
deleted.value = true;
|
||||
deleteVisible.value = false;
|
||||
emit('search');
|
||||
if (row.engine === 'tensorrt') visible.value = false;
|
||||
} finally {
|
||||
deleteLoading.value = false;
|
||||
}
|
||||
};
|
||||
const deploymentButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => open(row, 'config'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('menu.monitor'),
|
||||
show: (row: Deployment) => row.engine === 'vllm',
|
||||
click: (row: Deployment) => open(row, 'monitor'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.upgrade'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.upgradable && hasPermission('app_manage'),
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => open(row, 'upgrade'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
disabled: (row: Deployment) => operationDisabled(row) && row.status !== 'Uninstalling',
|
||||
click: (row: Deployment) => {
|
||||
deleting.value = row;
|
||||
forceDelete.value = false;
|
||||
deleteVisible.value = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
const visibleButtons = computed(() =>
|
||||
instance.value ? deploymentButtons.filter((button) => !button.show || button.show(instance.value)) : [],
|
||||
);
|
||||
const returnToManagement = async () => {
|
||||
const previousView = view.value;
|
||||
await nextTick();
|
||||
if (view.value !== previousView) return;
|
||||
if (actionName.value === 'task' || actionName.value === 'upgrade') emit('search');
|
||||
if (deleted.value || standalone.value) visible.value = false;
|
||||
else if (instance.value) open(instance.value);
|
||||
};
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.service-card {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
}
|
||||
.service-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
.el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.deployment-action {
|
||||
min-height: 180px;
|
||||
overflow-wrap: anywhere;
|
||||
:deep(.content-container__main > .el-card) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
--el-card-padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+9
-2
@@ -2,7 +2,7 @@
|
||||
<DrawerPro v-model="open" :header="headerTitle" size="large" @close="handleClose">
|
||||
<template #content>
|
||||
<div v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<div v-if="!readOnly" class="toolbar">
|
||||
<el-button v-permission type="primary" @click="openCreate">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
@@ -10,7 +10,12 @@
|
||||
<ComplexTable :data="items">
|
||||
<el-table-column :label="$t('aiTools.model.model')" prop="id" min-width="220" />
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" />
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
|
||||
<fu-table-operations
|
||||
v-if="!readOnly"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
/>
|
||||
</ComplexTable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,6 +65,7 @@ import { Rules } from '@/global/form-rules';
|
||||
const emit = defineEmits(['updated']);
|
||||
|
||||
const open = ref(false);
|
||||
const readOnly = ref(false);
|
||||
const editorOpen = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
@@ -207,6 +213,7 @@ const handleClose = () => {
|
||||
};
|
||||
|
||||
const openDrawer = async (row: AI.AgentAccountItem) => {
|
||||
readOnly.value = Boolean(row.appInstallId);
|
||||
account.id = row.id;
|
||||
account.name = row.name;
|
||||
account.provider = row.provider;
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ReqPage, ResPage, ResultData } from '@/api/interface';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import { pageTensorRTLLM, searchOllamaModel } from '@/api/modules/ai';
|
||||
import { checkAppInstalled, searchAppInstalled } from '@/api/modules/app';
|
||||
|
||||
export interface Deployment {
|
||||
id: number;
|
||||
engine: 'vllm' | 'ollama' | 'tensorrt';
|
||||
name: string;
|
||||
version: string;
|
||||
containerName: string;
|
||||
modelDir: string;
|
||||
models: string[];
|
||||
status: string;
|
||||
upgradable?: boolean;
|
||||
}
|
||||
|
||||
interface VllmItem {
|
||||
appInstallId: number;
|
||||
path: string;
|
||||
upgradable: boolean;
|
||||
id: number;
|
||||
servedModelName: string;
|
||||
name: string;
|
||||
appVersion: string;
|
||||
port: number;
|
||||
modelAccountBaseURL: string;
|
||||
containerName: string;
|
||||
modelDir: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface VllmAPI {
|
||||
pageVllm: (req: ReqPage & { info: string }) => Promise<ResultData<ResPage<VllmItem>>>;
|
||||
operateVllm: (req: { id: number; operate: string; taskID: string; forceDelete?: boolean }) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const vllmModules = import.meta.glob<VllmAPI>('@/xpack/api/modules/vllm.ts');
|
||||
export const vllmAvailable = Object.keys(vllmModules).length > 0;
|
||||
|
||||
export async function loadVllmAPI() {
|
||||
return Object.values(vllmModules)[0]();
|
||||
}
|
||||
|
||||
export async function readPages<T>(search: (page: ReqPage) => Promise<ResultData<ResPage<T>>>) {
|
||||
const items: T[] = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const res = await search({ page, pageSize: 100 });
|
||||
const batch = res.data.items || [];
|
||||
items.push(...batch);
|
||||
if (items.length >= res.data.total || batch.length === 0) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadModelOverview(pro: boolean, fxplay: boolean) {
|
||||
const [vllm, tensors, ollama] = await Promise.all([
|
||||
pro && vllmAvailable
|
||||
? loadVllmAPI().then((api) => readPages((page) => api.pageVllm({ ...page, info: '' })))
|
||||
: Promise.resolve([]),
|
||||
fxplay ? readPages<AI.TensorRTLLMDTO>((page) => pageTensorRTLLM({ ...page, name: '' })) : Promise.resolve([]),
|
||||
checkAppInstalled('ollama', ''),
|
||||
]);
|
||||
const deployments: Deployment[] = vllm.map((item) => ({
|
||||
id: item.id,
|
||||
engine: 'vllm',
|
||||
upgradable: item.upgradable,
|
||||
name: item.name,
|
||||
version: item.appVersion,
|
||||
containerName: item.containerName,
|
||||
modelDir: item.modelDir,
|
||||
models: item.servedModelName ? [item.servedModelName] : [],
|
||||
status: item.status,
|
||||
}));
|
||||
deployments.push(
|
||||
...tensors.map((item): Deployment => ({
|
||||
id: item.id!,
|
||||
engine: 'tensorrt',
|
||||
name: item.name,
|
||||
version: item.version,
|
||||
containerName: item.containerName,
|
||||
modelDir: item.modelDir,
|
||||
models: [],
|
||||
status: item.status || '',
|
||||
})),
|
||||
);
|
||||
if (ollama.data.isExist) {
|
||||
const [models, updates] = await Promise.all([
|
||||
readPages<AI.OllamaModelInfo>((page) => searchOllamaModel(page)),
|
||||
readPages((page) => searchAppInstalled({ ...page, name: ollama.data.name, update: true })),
|
||||
]);
|
||||
deployments.push({
|
||||
id: ollama.data.appInstallId,
|
||||
engine: 'ollama',
|
||||
name: ollama.data.name || 'Ollama',
|
||||
version: ollama.data.version,
|
||||
upgradable: updates.some((app) => app.id === ollama.data.appInstallId && app.canUpdate),
|
||||
containerName: ollama.data.containerName,
|
||||
modelDir: '',
|
||||
models: models.filter((model) => model.status === 'Success').map((model) => model.name),
|
||||
status: ollama.data.status,
|
||||
});
|
||||
}
|
||||
return deployments;
|
||||
}
|
||||
@@ -1,36 +1,766 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton :buttons="buttons" />
|
||||
<component :is="currentComponent" />
|
||||
<div class="model-page">
|
||||
<RouterButton :buttons="tabs" />
|
||||
<component
|
||||
:is="downloaderView"
|
||||
v-if="isFiles && downloaderView"
|
||||
@deploy="(modelDir) => createRef.open('vllm', modelDir)"
|
||||
/>
|
||||
<LayoutContent v-else-if="isFiles">
|
||||
<template #main><el-empty :description="$t('license.levelUpPro')" /></template>
|
||||
</LayoutContent>
|
||||
<LocalView v-else-if="isManagement" />
|
||||
<template v-else>
|
||||
<LayoutContent v-loading="loading">
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission type="primary" @click="createRef.open()">
|
||||
{{ $t(isLocal ? 'aiTools.model.addEngine' : 'aiTools.model.addService') }}
|
||||
</el-button>
|
||||
<el-button v-if="isLocal && isProductPro && vllmAvailable" v-permission @click="openTemplates">
|
||||
{{ $t('aiTools.model.deploymentTemplates') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select
|
||||
v-if="!isLocal"
|
||||
v-model="apiType"
|
||||
clearable
|
||||
filterable
|
||||
placeholder=""
|
||||
class="model-filter"
|
||||
@change="resetPageAndSearch"
|
||||
>
|
||||
<template #prefix>{{ 'API ' + $t('commons.table.type') }}</template>
|
||||
<el-option v-for="item in apiTypeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="isLocal"
|
||||
v-model="engine"
|
||||
clearable
|
||||
placeholder=""
|
||||
class="model-filter"
|
||||
@change="resetPageAndSearch"
|
||||
>
|
||||
<template #prefix>{{ $t('aiTools.model.engines') }}</template>
|
||||
<el-option label="Ollama" value="ollama" />
|
||||
<el-option label="vLLM" value="vllm" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="provider"
|
||||
clearable
|
||||
filterable
|
||||
class="model-filter"
|
||||
placeholder=""
|
||||
@change="resetPageAndSearch"
|
||||
>
|
||||
<template #prefix>{{ $t('aiTools.agents.provider') }}</template>
|
||||
<el-option
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<ProviderLabel :provider="item.value" :display-name="item.label" />
|
||||
</el-option>
|
||||
</el-select>
|
||||
<TableSearch v-model:searchName="searchName" class="model-search" @search="resetPageAndSearch" />
|
||||
<TableRefresh @search="search" />
|
||||
<TableSetting v-if="isLocal" title="model-deployment-refresh" @search="!loading && search()" />
|
||||
<fu-table-column-select
|
||||
v-else
|
||||
:columns="accountColumns"
|
||||
trigger="hover"
|
||||
:title="$t('commons.table.selectColumn')"
|
||||
:only-icon="true"
|
||||
/>
|
||||
</template>
|
||||
<template #main>
|
||||
<el-alert
|
||||
v-if="loadFailed"
|
||||
type="error"
|
||||
:closable="false"
|
||||
:title="$t('aiTools.model.loadFailed')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<ComplexTable
|
||||
v-if="!isLocal"
|
||||
:data="connections"
|
||||
row-key="id"
|
||||
:pagination-config="pagination"
|
||||
:columns="accountColumns"
|
||||
local-key="modelServiceColumn"
|
||||
@search="search"
|
||||
>
|
||||
<el-table-column prop="name" :label="$t('commons.table.name')" min-width="220" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="service-name">
|
||||
<div class="service-title cell-ellipsis" :title="row.name">{{ row.name }}</div>
|
||||
<el-tag class="service-source-tag" type="info" size="small">
|
||||
{{
|
||||
$t(
|
||||
!row.appInstallId
|
||||
? 'aiTools.model.cloudAccount'
|
||||
: 'aiTools.model.localModel',
|
||||
)
|
||||
}}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="provider" :label="$t('aiTools.model.engineProvider')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-ellipsis">
|
||||
<ProviderLabel :provider="row.provider" :display-name="row.providerName" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="apiType"
|
||||
:label="'API ' + $t('commons.table.type')"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.apiType || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="models" :label="$t('aiTools.model.model')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-for="(model, index) in (row.models || []).slice(0, 2)"
|
||||
:key="model.id"
|
||||
class="model-line"
|
||||
>
|
||||
<span class="cell-ellipsis" :title="model.id">{{ model.id }}</span>
|
||||
<el-button
|
||||
v-if="index === 1 && row.models.length > 2"
|
||||
link
|
||||
type="primary"
|
||||
class="model-more"
|
||||
@click="poolRef.open(row)"
|
||||
>
|
||||
+{{ row.models.length - 2 }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-if="!row.models?.length">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verified" :label="$t('aiTools.model.connectionStatus')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="connectionStatus(row).type">
|
||||
{{ connectionStatus(row).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="baseUrl"
|
||||
:label="$t('aiTools.model.endpoint')"
|
||||
min-width="280"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.baseUrl || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.createdAt')"
|
||||
:formatter="dateFormat"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
:buttons="accountButtons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
:ellipsis="3"
|
||||
width="220"
|
||||
/>
|
||||
</ComplexTable>
|
||||
<ComplexTable
|
||||
v-else
|
||||
:data="deploymentPage"
|
||||
:pagination-config="pagination"
|
||||
@search="resetPageIfNeeded"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.name')" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="manageRef.open(row, 'connection', true)">
|
||||
{{ row.name }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('app.version')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ row.version || '—' }}</span>
|
||||
<el-button
|
||||
v-if="row.upgradable && hasPermission('app_manage')"
|
||||
v-permission
|
||||
link
|
||||
type="primary"
|
||||
class="shrink-0"
|
||||
:disabled="operationDisabled(row)"
|
||||
@click="manageRef.open(row, 'upgrade', true)"
|
||||
>
|
||||
{{ $t('commons.button.upgrade') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.model.model')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(model, index) in row.models.slice(0, 2)" :key="model" class="model-line">
|
||||
<span class="cell-ellipsis" :title="model">{{ model }}</span>
|
||||
<el-button
|
||||
v-if="row.engine === 'ollama' && index === 1 && row.models.length > 2"
|
||||
link
|
||||
type="primary"
|
||||
class="model-more"
|
||||
@click="manageDeployment(row)"
|
||||
>
|
||||
+{{ row.models.length - 2 }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-if="!row.models.length">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-dropdown placement="bottom" :disabled="statusDisabled(row)">
|
||||
<Status v-permission :status="row.status" operate :disabled="statusDisabled(row)" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
:disabled="statusDisabled(row) || row.status === 'Running'"
|
||||
@click="operate(row, 'start')"
|
||||
>
|
||||
{{ $t('commons.operate.start') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:disabled="
|
||||
statusDisabled(row) || ['Stopped', 'Exited'].includes(row.status)
|
||||
"
|
||||
@click="operate(row, 'stop')"
|
||||
>
|
||||
{{ $t('commons.operate.stop') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:disabled="statusDisabled(row)"
|
||||
@click="operate(row, 'restart')"
|
||||
>
|
||||
{{ $t('commons.button.restart') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('container.source')" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 'Running' && row.containerName">
|
||||
<ContainerResource
|
||||
v-if="resourceStats[row.containerName]"
|
||||
:stats="resourceStats[row.containerName]"
|
||||
/>
|
||||
<el-icon
|
||||
v-else-if="resourceStats[row.containerName] === undefined"
|
||||
class="is-loading"
|
||||
>
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-tooltip v-else :content="$t('aiTools.model.loadFailed')">
|
||||
<span>—</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
:buttons="deploymentButtons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
:ellipsis="4"
|
||||
width="240"
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
<CreateService
|
||||
ref="createRef"
|
||||
:engines-only="isLocal || isFiles"
|
||||
:vllm-only="isFiles"
|
||||
@search="search"
|
||||
@manage="manageDeployment"
|
||||
/>
|
||||
<ManageDeployment
|
||||
ref="manageRef"
|
||||
@search="search"
|
||||
@upgrade="(app) => upgradeRef.acceptParams(app, 'upgrade')"
|
||||
/>
|
||||
<AppUpgrade ref="upgradeRef" @close="search" />
|
||||
<AppDelete :key="`delete:${currentNode}:${route.fullPath}`" ref="appDeleteRef" @close="search" />
|
||||
<AppResources :key="`resources:${currentNode}:${route.fullPath}`" ref="appResourcesRef" @close="search" />
|
||||
<TaskLog ref="taskRef" width="70%" @close="search" />
|
||||
<ModelPool ref="poolRef" @updated="search" />
|
||||
<component :is="templateView" v-if="templateView" ref="templateRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, nextTick, reactive, ref, shallowRef, toRef, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import type { Container } from '@/api/interface/container';
|
||||
import { containerListStats } from '@/api/modules/container';
|
||||
import ContainerResource from '@/components/container-resource/index.vue';
|
||||
import type { FuTableColumnConfig } from '@/components/table/shared';
|
||||
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts, operateTensorRTLLM } from '@/api/modules/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { getAgentProviderDisplayName, isAgentAccountVerificationSkipped } from '@/utils/agent';
|
||||
import { loadOptionalComponent } from '@/extensions/optional';
|
||||
import i18n from '@/lang';
|
||||
import AccountView from '@/views/ai/agents/model/index.vue';
|
||||
import LocalView from '@/views/ai/model/local/index.vue';
|
||||
import { appInstalledDeleteCheck, installedOp, searchAppInstalled } from '@/api/modules/app';
|
||||
import { hasPermission } from '@/utils/rbac';
|
||||
import { hasManagePermissionAccess } from '@/utils/permission';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import LocalView from './local/index.vue';
|
||||
import CreateService from './components/create/index.vue';
|
||||
import ManageDeployment from './components/deployment-manage/index.vue';
|
||||
import AppUpgrade from '@/views/app-store/installed/upgrade/index.vue';
|
||||
import AppDelete from '@/views/app-store/installed/delete/index.vue';
|
||||
import AppResources from '@/views/app-store/installed/check/index.vue';
|
||||
import ModelPool from './components/model-pool/index.vue';
|
||||
import ProviderLabel from '@/components/agent-provider-label/index.vue';
|
||||
import { loadModelOverview, loadVllmAPI, readPages, vllmAvailable, type Deployment } from './deployment';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const buttons = computed<RouterButton[]>(() => {
|
||||
return [
|
||||
const router = useRouter();
|
||||
const pagePath = route.path;
|
||||
const isCurrentPage = computed(() => route.path === pagePath);
|
||||
const { isProductPro, isFxplay, isIntl, currentNode } = useGlobalStore();
|
||||
const isFiles = pagePath === '/ai/model/files';
|
||||
const isLocal = pagePath === '/ai/model/local';
|
||||
const isManagement = computed(
|
||||
() => isCurrentPage.value && isLocal && ['vllm', 'tensorrt'].includes(String(route.query.tab)),
|
||||
);
|
||||
const tabs = computed(() => [
|
||||
{ label: i18n.global.t('aiTools.model.services'), path: '/ai/model/account' },
|
||||
{ label: i18n.global.t('aiTools.model.files'), path: '/ai/model/files' },
|
||||
{ label: i18n.global.t('aiTools.model.engines'), path: '/ai/model/local' },
|
||||
]);
|
||||
const downloaderModules = import.meta.glob('/src/xpack/views/model-downloader/index.vue');
|
||||
const downloaderView = Object.keys(downloaderModules).length
|
||||
? defineAsyncComponent(() => loadOptionalComponent('/src/xpack/views/model-downloader/index.vue'))
|
||||
: null;
|
||||
const createRef = ref();
|
||||
const manageRef = ref();
|
||||
const upgradeRef = ref();
|
||||
const appDeleteRef = ref();
|
||||
const appResourcesRef = ref();
|
||||
const poolRef = ref();
|
||||
const templateRef = ref();
|
||||
const templateView = shallowRef();
|
||||
const loading = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const connections = ref<AI.AgentAccountItem[]>([]);
|
||||
const accountColumns = ref<FuTableColumnConfig[]>([]);
|
||||
const providers = ref<AI.ProviderInfo[]>([]);
|
||||
watch(
|
||||
currentNode,
|
||||
async (_, __, onCleanup) => {
|
||||
if (isLocal || isFiles) return;
|
||||
let cancelled = false;
|
||||
onCleanup(() => (cancelled = true));
|
||||
providers.value = [];
|
||||
try {
|
||||
const { data } = await getAgentProviders();
|
||||
if (!cancelled) providers.value = data || [];
|
||||
} catch {
|
||||
if (!cancelled) providers.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const deployments = ref<Deployment[]>([]);
|
||||
const apiType = ref('');
|
||||
const engine = ref('');
|
||||
const provider = ref('');
|
||||
const searchName = ref('');
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
if (!isLocal && !isFiles) {
|
||||
useSearchState(
|
||||
'ai/agents/model/index',
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.account'),
|
||||
path: '/ai/model/account',
|
||||
search: searchName,
|
||||
apiType,
|
||||
provider,
|
||||
page: toRef(pagination, 'currentPage'),
|
||||
pageSize: toRef(pagination, 'pageSize'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.model.localModel'),
|
||||
path: '/ai/model/local',
|
||||
onRevisit: () => {
|
||||
if (typeof route.query.name === 'string' && route.query.name !== searchName.value) {
|
||||
searchName.value = route.query.name;
|
||||
resetPage();
|
||||
}
|
||||
return search();
|
||||
},
|
||||
},
|
||||
];
|
||||
);
|
||||
}
|
||||
const apiTypeOptions = computed(() =>
|
||||
Array.from(new Set(providers.value.flatMap((item) => item.apiTypes.map((api) => api.apiType)))).sort(),
|
||||
);
|
||||
const providerOptions = computed(() =>
|
||||
providers.value
|
||||
.filter((item) => !isIntl.value || !['ark-coding-plan', 'bailian-coding-plan'].includes(item.provider))
|
||||
.map((item) => ({
|
||||
value: item.provider,
|
||||
label: getAgentProviderDisplayName(item.provider, item.displayName),
|
||||
})),
|
||||
);
|
||||
const filteredDeployments = computed(() => {
|
||||
const keyword = searchName.value.trim().toLowerCase();
|
||||
return deployments.value.filter(
|
||||
(row) =>
|
||||
(!engine.value || row.engine === engine.value) &&
|
||||
(!keyword || [row.name, row.modelDir, ...row.models].join(' ').toLowerCase().includes(keyword)),
|
||||
);
|
||||
});
|
||||
const deploymentPage = computed(() =>
|
||||
filteredDeployments.value.slice(
|
||||
(pagination.currentPage - 1) * pagination.pageSize,
|
||||
pagination.currentPage * pagination.pageSize,
|
||||
),
|
||||
);
|
||||
const resourceStats = ref<Record<string, Container.ContainerListStats | null>>({});
|
||||
watch(
|
||||
[deploymentPage, isCurrentPage, isManagement, currentNode],
|
||||
async ([rows, active, management, node], _, onCleanup) => {
|
||||
resourceStats.value = {};
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => {
|
||||
controller.abort();
|
||||
});
|
||||
if (!active || !isLocal || management) return;
|
||||
const names = [
|
||||
...new Set(
|
||||
rows.filter((row) => row.status === 'Running' && row.containerName).map((row) => row.containerName),
|
||||
),
|
||||
];
|
||||
if (!names.length) return;
|
||||
try {
|
||||
const { data } = await containerListStats(node, undefined, {
|
||||
signal: controller.signal,
|
||||
skipErrorMessage: true,
|
||||
});
|
||||
if (controller.signal.aborted) return;
|
||||
const statsByName = new Map((data || []).map((item) => [item.containerName, item]));
|
||||
resourceStats.value = Object.fromEntries(names.map((name) => [name, statsByName.get(name) || null]));
|
||||
} catch {
|
||||
if (!controller.signal.aborted) resourceStats.value = Object.fromEntries(names.map((name) => [name, null]));
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const resetPage = () => {
|
||||
pagination.currentPage = 1;
|
||||
};
|
||||
const resetPageAndSearch = () => {
|
||||
resetPage();
|
||||
search();
|
||||
};
|
||||
const resetPageIfNeeded = () => {
|
||||
pagination.currentPage = Math.min(
|
||||
pagination.currentPage,
|
||||
Math.max(1, Math.ceil(pagination.total / pagination.pageSize)),
|
||||
);
|
||||
};
|
||||
watch(filteredDeployments, () => {
|
||||
if (!isLocal) return;
|
||||
pagination.total = filteredDeployments.value.length;
|
||||
resetPageIfNeeded();
|
||||
});
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
if (route.path === '/ai/model/local') {
|
||||
return LocalView;
|
||||
let searchVersion = 0;
|
||||
const search = async () => {
|
||||
const version = ++searchVersion;
|
||||
if (!isCurrentPage.value || isFiles || isManagement.value) return;
|
||||
loading.value = true;
|
||||
loadFailed.value = false;
|
||||
try {
|
||||
if (isLocal) {
|
||||
const data = await loadModelOverview(isProductPro.value, isFxplay.value);
|
||||
if (version !== searchVersion) return;
|
||||
deployments.value = data;
|
||||
} else {
|
||||
const result = await pageAgentAccounts({
|
||||
page: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
apiType: apiType.value,
|
||||
provider: provider.value,
|
||||
name: searchName.value,
|
||||
});
|
||||
if (version !== searchVersion) return;
|
||||
connections.value = result.data.items || [];
|
||||
pagination.total = result.data.total;
|
||||
const previousPage = pagination.currentPage;
|
||||
resetPageIfNeeded();
|
||||
if (previousPage !== pagination.currentPage) await search();
|
||||
}
|
||||
} catch {
|
||||
if (version !== searchVersion) return;
|
||||
connections.value = [];
|
||||
pagination.total = 0;
|
||||
deployments.value = [];
|
||||
loadFailed.value = true;
|
||||
} finally {
|
||||
if (version === searchVersion) loading.value = false;
|
||||
}
|
||||
return AccountView;
|
||||
});
|
||||
};
|
||||
|
||||
const connectionStatus = (row: AI.AgentAccountItem) => {
|
||||
if (isAgentAccountVerificationSkipped(row.provider)) {
|
||||
return { type: 'info' as const, label: i18n.global.t('aiTools.agents.verifySkipped') };
|
||||
}
|
||||
return {
|
||||
type: row.verified ? ('success' as const) : ('warning' as const),
|
||||
label: i18n.global.t(row.verified ? 'aiTools.agents.verified' : 'aiTools.model.unverified'),
|
||||
};
|
||||
};
|
||||
const manageDeployment = (row: Deployment) => manageRef.value.open(row);
|
||||
const accountButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
click: (row: AI.AgentAccountItem) => createRef.value.openAccount(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.agents.modelPool'),
|
||||
click: (row: AI.AgentAccountItem) => poolRef.value.open(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
show: (row: AI.AgentAccountItem) => !row.appInstallId || row.provider === 'ollama',
|
||||
click: async (row: AI.AgentAccountItem) => {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('commons.msg.delete', [row.name]),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
await deleteAgentAccount({ id: row.id });
|
||||
await search();
|
||||
},
|
||||
},
|
||||
];
|
||||
const taskRef = ref();
|
||||
const pending = ref('');
|
||||
const operationDisabled = (row: Deployment) =>
|
||||
Boolean(pending.value) ||
|
||||
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Waiting', 'Uninstalling'].includes(row.status);
|
||||
const statusDisabled = (row: Deployment) =>
|
||||
operationDisabled(row) || !hasManagePermissionAccess() || (row.engine === 'ollama' && !hasPermission('app_manage'));
|
||||
const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart') => {
|
||||
if (statusDisabled(row)) return;
|
||||
const node = currentNode.value;
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('app.operatorHelper', [i18n.global.t('commons.operate.' + operation)]),
|
||||
i18n.global.t('commons.operate.' + operation),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
if (node !== currentNode.value || statusDisabled(row)) return;
|
||||
pending.value = `${row.engine}:${row.id}`;
|
||||
try {
|
||||
if (row.engine === 'ollama') {
|
||||
await installedOp({ installId: row.id, operate: operation });
|
||||
} else if (row.engine === 'tensorrt') {
|
||||
await operateTensorRTLLM({ id: row.id, operate: operation });
|
||||
} else {
|
||||
const taskID = newUUID();
|
||||
await (await loadVllmAPI()).operateVllm({ id: row.id, operate: operation, taskID });
|
||||
if (node !== currentNode.value) return;
|
||||
if (row.version.toLowerCase().startsWith('nvidia-gb10-dspark-')) {
|
||||
await search();
|
||||
taskRef.value.openWithTaskID(taskID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (node !== currentNode.value) return;
|
||||
await search();
|
||||
} finally {
|
||||
pending.value = '';
|
||||
}
|
||||
};
|
||||
const deploymentButtons = [
|
||||
{
|
||||
label: i18n.global.t('aiTools.model.manage'),
|
||||
show: (row: Deployment) => row.engine !== 'vllm',
|
||||
click: manageDeployment,
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'vllm',
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'config', true),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('menu.monitor'),
|
||||
show: (row: Deployment) => row.engine === 'vllm',
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'monitor', true),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.log'),
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'log', true),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'ollama' && hasPermission('app_manage'),
|
||||
disabled: operationDisabled,
|
||||
click: async (row: Deployment) => {
|
||||
const node = currentNode.value;
|
||||
const path = route.fullPath;
|
||||
pending.value = `${row.engine}:${row.id}`;
|
||||
try {
|
||||
const [apps, resources] = await Promise.all([
|
||||
readPages((page) => searchAppInstalled({ ...page, name: row.name, update: false }, node)),
|
||||
appInstalledDeleteCheck(row.id, node),
|
||||
]);
|
||||
if (node !== currentNode.value || path !== route.fullPath) return;
|
||||
const app = apps.find((item) => item.id === row.id && item.appKey === 'ollama');
|
||||
if (!app) {
|
||||
MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
if (resources.data?.length) {
|
||||
appResourcesRef.value.acceptParams({ items: resources.data, key: app.appKey, installID: app.id });
|
||||
} else {
|
||||
await appDeleteRef.value.acceptParams(app);
|
||||
}
|
||||
} finally {
|
||||
pending.value = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'vllm',
|
||||
disabled: (row: Deployment) => operationDisabled(row) && row.status !== 'Uninstalling',
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'delete', true),
|
||||
},
|
||||
];
|
||||
const openTemplates = async () => {
|
||||
templateView.value = await loadOptionalComponent('/src/xpack/views/vllm/template/index.vue');
|
||||
await nextTick();
|
||||
templateRef.value.acceptParams();
|
||||
};
|
||||
watch(
|
||||
[
|
||||
() => route.path,
|
||||
() => route.query.tab,
|
||||
() => route.query.name,
|
||||
() => route.query.open,
|
||||
currentNode,
|
||||
isProductPro,
|
||||
isFxplay,
|
||||
],
|
||||
async (_, previous, onCleanup) => {
|
||||
let cancelled = false;
|
||||
onCleanup(() => (cancelled = true));
|
||||
if (!isCurrentPage.value) {
|
||||
searchVersion++;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
if (isLocal && route.query.tab === 'downloader') {
|
||||
await router.replace('/ai/model/files');
|
||||
return;
|
||||
}
|
||||
if (isLocal || isFiles) {
|
||||
apiType.value = '';
|
||||
engine.value = '';
|
||||
provider.value = '';
|
||||
searchName.value = '';
|
||||
resetPage();
|
||||
} else {
|
||||
if (previous.length && previous[0] !== pagePath) return;
|
||||
if (typeof route.query.name === 'string' && route.query.name !== searchName.value) {
|
||||
searchName.value = route.query.name;
|
||||
resetPage();
|
||||
}
|
||||
}
|
||||
await search();
|
||||
if (cancelled) return;
|
||||
if (
|
||||
isCurrentPage.value &&
|
||||
isLocal &&
|
||||
route.query.open === 'create' &&
|
||||
(route.query.engine === 'ollama' || route.query.tab === 'ollama')
|
||||
) {
|
||||
await nextTick();
|
||||
if (cancelled) return;
|
||||
await createRef.value.open('ollama');
|
||||
if (cancelled) return;
|
||||
const query = { ...route.query };
|
||||
delete query.open;
|
||||
delete query.tab;
|
||||
await router.replace({ path: route.path, query });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.service-name {
|
||||
min-width: 0;
|
||||
}
|
||||
.service-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.service-source-tag {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cell-ellipsis {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.model-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& + & {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
.model-more {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.model-filter {
|
||||
width: 220px;
|
||||
}
|
||||
.model-search {
|
||||
width: 220px;
|
||||
:deep(.search-button) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.model-filter,
|
||||
.model-search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,134 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="content-container__search">
|
||||
<el-card>
|
||||
<div>
|
||||
<el-button
|
||||
v-for="button in buttons"
|
||||
:key="button.value"
|
||||
class="tag-button"
|
||||
:class="currentTab === button.value ? '' : 'no-active'"
|
||||
:type="currentTab === button.value ? 'primary' : ''"
|
||||
@click="handleChange(button.value)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="local-model-content">
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
<el-button class="mt-4" icon="Back" @click="router.push('/ai/model/local')">
|
||||
{{ $t('aiTools.model.engines') }}
|
||||
</el-button>
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import OllamaView from '@/views/ai/model/ollama/index.vue';
|
||||
import TensorRTView from '@/views/ai/model/tensorrt/index.vue';
|
||||
import VllmPlaceholder from '@/views/ai/model/vllm/index.vue';
|
||||
import { loadOptionalComponent } from '@/extensions/optional';
|
||||
import i18n from '@/lang';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { vllmAvailable } from '../deployment';
|
||||
|
||||
const VllmView = defineAsyncComponent(() => loadOptionalComponent('/src/xpack/views/vllm/index.vue'));
|
||||
const ModelDownloaderView = defineAsyncComponent(() =>
|
||||
loadOptionalComponent('/src/xpack/views/model-downloader/index.vue'),
|
||||
);
|
||||
|
||||
type LocalTab = 'ollama' | 'vllm' | 'tensorrt' | 'downloader';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isFxplay, isProductPro } = useGlobalStore();
|
||||
|
||||
const tabLabels: Record<LocalTab, string> = {
|
||||
ollama: 'Ollama',
|
||||
vllm: 'vLLM',
|
||||
tensorrt: 'TensorRT LLM',
|
||||
downloader: i18n.global.t('aiTools.model.downloader'),
|
||||
};
|
||||
|
||||
const buttons = computed<Array<{ label: string; value: LocalTab }>>(() => {
|
||||
const items: Array<{ label: string; value: LocalTab }> = [
|
||||
{ label: tabLabels.vllm, value: 'vllm' },
|
||||
{ label: tabLabels.ollama, value: 'ollama' },
|
||||
];
|
||||
if (isFxplay.value) {
|
||||
items.push({ label: tabLabels.tensorrt, value: 'tensorrt' });
|
||||
}
|
||||
items.push({ label: tabLabels.downloader, value: 'downloader' });
|
||||
return items;
|
||||
});
|
||||
|
||||
const currentTab = computed<LocalTab>(() => {
|
||||
const tab = route.query.tab;
|
||||
if (tab === 'ollama' || tab === 'vllm' || tab === 'downloader' || (tab === 'tensorrt' && isFxplay.value)) {
|
||||
return tab;
|
||||
}
|
||||
if (tab === 'tensorrt') {
|
||||
return 'ollama';
|
||||
}
|
||||
return isProductPro.value ? 'vllm' : 'ollama';
|
||||
});
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
switch (currentTab.value) {
|
||||
case 'vllm':
|
||||
return VllmView;
|
||||
case 'tensorrt':
|
||||
return TensorRTView;
|
||||
case 'downloader':
|
||||
return ModelDownloaderView;
|
||||
default:
|
||||
return OllamaView;
|
||||
}
|
||||
if (route.query.tab === 'vllm') return isProductPro.value && vllmAvailable ? VllmView : VllmPlaceholder;
|
||||
if (route.query.tab === 'tensorrt' && isFxplay.value) return TensorRTView;
|
||||
return VllmPlaceholder;
|
||||
});
|
||||
|
||||
const handleChange = async (target: LocalTab) => {
|
||||
await router.replace({
|
||||
path: '/ai/model/local',
|
||||
query: {
|
||||
...route.query,
|
||||
tab: target,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => route.query.tab, isFxplay],
|
||||
async ([tab, fxplay]) => {
|
||||
if (tab !== 'tensorrt' || fxplay) {
|
||||
return;
|
||||
}
|
||||
await router.replace({
|
||||
path: '/ai/model/local',
|
||||
query: {
|
||||
...route.query,
|
||||
tab: 'ollama',
|
||||
},
|
||||
});
|
||||
async () => {
|
||||
if (route.query.tab === 'tensorrt' && !isFxplay.value) await router.replace('/ai/model/local');
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-container__search {
|
||||
margin-top: 7px;
|
||||
|
||||
:deep(.el-card) {
|
||||
--el-card-padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.local-model-content {
|
||||
:deep(.content-container__app) {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
:deep(.content-container__main) {
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('commons.button.add')" @close="handleClose">
|
||||
<DialogPro size="normal" v-model="drawerVisible" :title="$t('commons.button.add')" @close="handleClose">
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<span class="flx-align-center">
|
||||
@@ -32,7 +32,7 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('database.databaseConnInfo')" @close="handleClose" size="small">
|
||||
<el-form @submit.prevent v-loading="loading" :model="form" label-position="top">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form-item :label="$t('database.containerConn')">
|
||||
<el-card class="mini-border-card">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('database.connAddress')">
|
||||
{{ form.containerName }}
|
||||
<CopyButton :content="form.containerName" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.port')">
|
||||
11434
|
||||
<CopyButton content="11434" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<span class="input-help">
|
||||
{{ $t('aiTools.model.container_conn_helper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.proxyUrl')" v-if="bindDomain.connUrl != ''">
|
||||
<el-card class="mini-border-card">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('database.connAddress')">
|
||||
{{ bindDomain.connUrl }}
|
||||
<CopyButton :content="bindDomain.connUrl" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<span class="input-help">
|
||||
{{ $t('database.remoteConnHelper2') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.remoteConn')" v-else>
|
||||
<el-card class="mini-border-card">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('database.connAddress')">
|
||||
{{ form.systemIP }}
|
||||
<CopyButton :content="form.systemIP" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.port')">
|
||||
{{ form.port }}
|
||||
<CopyButton :content="form.port + ''" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<span class="input-help">
|
||||
{{ $t('database.remoteConnHelper2') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="drawerVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { getBindDomain } from '@/api/modules/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const { currentNode } = useGlobalStore();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const form = reactive({
|
||||
systemIP: '',
|
||||
containerName: '',
|
||||
port: 0,
|
||||
|
||||
remoteIP: '',
|
||||
});
|
||||
const bindDomain = ref({
|
||||
connUrl: '',
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
port: number;
|
||||
containerName: string;
|
||||
appinstallID: number;
|
||||
}
|
||||
|
||||
const acceptParams = (param: DialogProps): void => {
|
||||
form.containerName = param.containerName;
|
||||
form.port = param.port;
|
||||
loadSystemIP();
|
||||
loadBindDomain(param.appinstallID);
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const loadSystemIP = async () => {
|
||||
if (currentNode.value !== 'local') {
|
||||
form.systemIP = currentNode.value || i18n.global.t('database.localIP');
|
||||
return;
|
||||
}
|
||||
const res = await getAgentSettingInfo();
|
||||
form.systemIP = res.data.systemIP || i18n.global.t('database.localIP');
|
||||
};
|
||||
|
||||
const loadBindDomain = async (appInstallID: number) => {
|
||||
if (appInstallID == undefined || appInstallID <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getBindDomain({
|
||||
appInstallID: appInstallID,
|
||||
});
|
||||
if (res.data.websiteID > 0) {
|
||||
bindDomain.value.connUrl = res.data.connUrl;
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.copy_button {
|
||||
border-radius: 0px;
|
||||
border-left-width: 0px;
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('aiTools.proxy.proxy')" @close="handleClose" size="large">
|
||||
<DialogPro v-model="open" :title="$t('aiTools.proxy.proxy')" @close="handleClose" size="large">
|
||||
<div v-loading="loading">
|
||||
<el-form ref="formRef" label-position="top" @submit.prevent :model="req" :rules="rules">
|
||||
<el-alert class="common-prompt" :closable="false" type="warning">
|
||||
@@ -89,7 +89,7 @@
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
+57
-180
@@ -1,67 +1,28 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" @click="bindDomain">
|
||||
{{ $t('aiTools.proxy.proxy') }}
|
||||
</el-button>
|
||||
<el-divider />
|
||||
<div class="model-section-heading">
|
||||
<h3 class="section-title">{{ $t('aiTools.model.modelManagement') }}</h3>
|
||||
</div>
|
||||
<LayoutContent title="Ollama">
|
||||
<template #app>
|
||||
<AppStatus
|
||||
app-key="ollama"
|
||||
v-model:loading="loading"
|
||||
:hide-setting="true"
|
||||
v-model:mask-show="maskShow"
|
||||
v-model:appInstallID="appInstallID"
|
||||
@is-exist="checkExist"
|
||||
ref="appStatusRef"
|
||||
></AppStatus>
|
||||
</template>
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<span>{{ $t('runtime.systemRestartHelper') }}</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" type="primary" @click="onCreate()">
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" type="primary" @click="onCreate">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
plain
|
||||
type="primary"
|
||||
:disabled="modelInfo.status !== 'Running'"
|
||||
@click="bindDomain"
|
||||
>
|
||||
{{ $t('aiTools.proxy.proxy') }}
|
||||
</el-button>
|
||||
<el-button :disabled="modelInfo.status !== 'Running'" @click="onLoadConn" type="primary" plain>
|
||||
{{ $t('database.databaseConnInfo') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
:disabled="modelInfo.status !== 'Running'"
|
||||
type="primary"
|
||||
plain
|
||||
@click="onSync()"
|
||||
>
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" @click="onSync">
|
||||
{{ $t('database.loadFromRemote') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="modelInfo.status !== 'Running'"
|
||||
icon="Position"
|
||||
@click="goDashboard()"
|
||||
type="primary"
|
||||
plain
|
||||
>
|
||||
OpenWebUI
|
||||
</el-button>
|
||||
|
||||
<el-button v-permission plain :disabled="selects.length === 0" type="primary" @click="onDelete(null)">
|
||||
<el-button v-permission :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
<TableSetting title="model-refresh" @search="search()" />
|
||||
<TableSearch @search="search" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search" />
|
||||
<TableSetting title="model-refresh" @search="!loading && search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
@@ -69,15 +30,19 @@
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:class="{ mask: maskShow }"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<el-table-column type="selection" :selectable="selectable" fix />
|
||||
<el-table-column :label="$t('aiTools.model.model')" prop="name" min-width="90">
|
||||
<el-table-column :label="$t('aiTools.model.model')" prop="name" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-text v-if="row.size" type="primary" class="cursor-pointer" @click="onLoad(row.name)">
|
||||
<el-text
|
||||
v-if="row.size && modelInfo.status === 'Running'"
|
||||
type="primary"
|
||||
class="cursor-pointer"
|
||||
@click="onLoad(row.name)"
|
||||
>
|
||||
{{ row.name }}
|
||||
</el-text>
|
||||
<span v-else>{{ row.name }}</span>
|
||||
@@ -110,57 +75,24 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.button.log')">
|
||||
<template #default="{ row }">
|
||||
<el-button @click="onLoadLog(row)" link type="primary">
|
||||
{{ $t('website.check') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
min-width="80"
|
||||
min-width="170"
|
||||
:label="$t('commons.table.date')"
|
||||
prop="createdAt"
|
||||
:formatter="dateFormat"
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="isMobile ? 0 : 10"
|
||||
:min-width="isMobile ? 'auto' : 200"
|
||||
:ellipsis="isMobile ? 0 : 1"
|
||||
:width="isMobile ? 'auto' : 110"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
|
||||
<el-card v-if="modelInfo.status != 'Running' && !loading && maskShow" class="mask-prompt">
|
||||
<span v-if="modelInfo.isExist">
|
||||
{{ $t('commons.service.serviceNotStarted', ['Ollama']) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('app.checkInstalledWarn', ['Ollama']) }}
|
||||
<el-button @click="goInstall('ollama')" link icon="Position" type="primary">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-card>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<DialogPro v-model="dashboardVisible" :title="$t('app.checkTitle')" size="mini">
|
||||
<div class="flex justify-center items-center gap-2 flex-wrap">
|
||||
{{ $t('app.checkInstalledWarn', ['OpenWebUI']) }}
|
||||
<el-link icon="Position" @click="goInstall('ollama-webui')" type="primary">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-link>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dashboardVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
<el-form class="mt-4 mb-1" ref="deleteForm" label-position="left">
|
||||
@@ -176,28 +108,22 @@
|
||||
<AddDialog ref="addRef" @search="search" @log="onLoadLog" />
|
||||
<Del ref="delRef" @search="search" />
|
||||
<Terminal ref="terminalRef" />
|
||||
<Conn ref="connRef" />
|
||||
<CodemirrorDrawer ref="detailRef" />
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
<DialogPro v-model="detailVisible" :title="$t('commons.button.view')" size="large">
|
||||
<CodemirrorPro v-model="detailInfo" :height-diff="300" readonly mode="json" />
|
||||
</DialogPro>
|
||||
<BindDomain ref="bindDomainRef" />
|
||||
|
||||
<TaskLog ref="taskLogRef" width="70%" @close="search" />
|
||||
<TaskLog ref="taskLogRef" width="70%" @close="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
|
||||
import Conn from '@/views/ai/model/ollama/conn/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import Terminal from '@/views/ai/model/ollama/terminal/index.vue';
|
||||
import Del from '@/views/ai/model/ollama/del/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { App } from '@/api/interface/app';
|
||||
import {
|
||||
deleteOllamaModel,
|
||||
loadOllamaModel,
|
||||
@@ -206,27 +132,22 @@ import {
|
||||
syncOllamaModel,
|
||||
} from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAppPort } from '@/api/modules/app';
|
||||
import { checkAppInstalled } from '@/api/modules/app';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { MsgInfo, MsgSuccess } from '@/utils/message';
|
||||
import BindDomain from '@/views/ai/model/ollama/domain/index.vue';
|
||||
import { routerToNameWithQuery } from '@/utils/router';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const props = defineProps({ instanceName: String });
|
||||
const { isMobile } = useGlobalStore();
|
||||
const loading = ref(false);
|
||||
const selects = ref<any>([]);
|
||||
const maskShow = ref(false);
|
||||
const addRef = ref();
|
||||
const detailRef = ref();
|
||||
const detailVisible = ref(false);
|
||||
const detailInfo = ref('');
|
||||
const delRef = ref();
|
||||
const connRef = ref();
|
||||
const terminalRef = ref();
|
||||
const openWebUIPort = ref();
|
||||
const dashboardVisible = ref(false);
|
||||
const dialogPortJumpRef = ref();
|
||||
const appStatusRef = ref();
|
||||
const bindDomainRef = ref();
|
||||
const taskLogRef = ref();
|
||||
const data = ref();
|
||||
@@ -247,13 +168,7 @@ const opRef = ref();
|
||||
const operateIDs = ref();
|
||||
const forceDelete = ref();
|
||||
|
||||
const modelInfo = reactive({
|
||||
status: '',
|
||||
container: '',
|
||||
isExist: null,
|
||||
version: '',
|
||||
port: 11434,
|
||||
});
|
||||
const modelInfo = reactive({ status: '' });
|
||||
|
||||
function selectable(row) {
|
||||
return row.status !== 'Waiting';
|
||||
@@ -286,7 +201,7 @@ const onSync = async () => {
|
||||
await syncOllamaModel()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.data) {
|
||||
if (res.data?.length) {
|
||||
delRef.value.acceptParams({ list: res.data });
|
||||
} else {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
@@ -298,58 +213,16 @@ const onSync = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadConn = async () => {
|
||||
connRef.value.acceptParams({
|
||||
port: modelInfo.port,
|
||||
containerName: modelInfo.container,
|
||||
appinstallID: appInstallID.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onLoad = async (name: string) => {
|
||||
const res = await loadOllamaModel(name);
|
||||
let detailInfo = res.data;
|
||||
let param = {
|
||||
header: i18n.global.t('commons.button.view'),
|
||||
detailInfo: detailInfo,
|
||||
mode: 'json',
|
||||
};
|
||||
detailRef.value!.acceptParams(param);
|
||||
};
|
||||
|
||||
const goDashboard = async () => {
|
||||
if (openWebUIPort.value === 0) {
|
||||
dashboardVisible.value = true;
|
||||
return;
|
||||
}
|
||||
dialogPortJumpRef.value.acceptParams({ port: openWebUIPort.value });
|
||||
detailInfo.value = res.data;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const bindDomain = () => {
|
||||
bindDomainRef.value.acceptParams(appInstallID.value);
|
||||
};
|
||||
|
||||
const goInstall = (name: string) => {
|
||||
routerToNameWithQuery('AppAll', { install: name });
|
||||
};
|
||||
|
||||
const loadWebUIPort = async () => {
|
||||
const res = await getAppPort('ollama-webui', '');
|
||||
openWebUIPort.value = res.data;
|
||||
};
|
||||
|
||||
const checkExist = (data: App.CheckInstalled) => {
|
||||
modelInfo.isExist = data.isExist;
|
||||
modelInfo.status = data.status;
|
||||
modelInfo.version = data.version;
|
||||
modelInfo.container = data.containerName;
|
||||
modelInfo.port = data.httpPort;
|
||||
|
||||
if (modelInfo.isExist && modelInfo.status === 'Running') {
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitDelete = async () => {
|
||||
loading.value = true;
|
||||
await deleteOllamaModel(operateIDs.value, forceDelete.value)
|
||||
@@ -409,6 +282,7 @@ const onDelete = async (row: AI.OllamaModelInfo) => {
|
||||
const onLoadLog = (row: any) => {
|
||||
if (row.taskID) {
|
||||
openTaskLog(row.taskID);
|
||||
return;
|
||||
}
|
||||
if (row.from === 'remote') {
|
||||
MsgInfo(i18n.global.t('aiTools.model.from_remote'));
|
||||
@@ -418,6 +292,7 @@ const onLoadLog = (row: any) => {
|
||||
MsgInfo(i18n.global.t('aiTools.model.no_logs'));
|
||||
return;
|
||||
}
|
||||
|
||||
taskLogRef.value.openWithResourceID('AI', 'TaskPull', row.id);
|
||||
};
|
||||
|
||||
@@ -428,7 +303,7 @@ const buttons = [
|
||||
terminalRef.value.acceptParams({ name: row.name });
|
||||
},
|
||||
disabled: (row: any) => {
|
||||
return row.status !== 'Success';
|
||||
return row.status !== 'Success' || modelInfo.status !== 'Running';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -438,9 +313,13 @@ const buttons = [
|
||||
onReCreate(row.name);
|
||||
},
|
||||
disabled: (row: any) => {
|
||||
return row.status === 'Success' || row.status === 'Waiting';
|
||||
return row.status === 'Success' || row.status === 'Waiting' || modelInfo.status !== 'Running';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.log'),
|
||||
click: onLoadLog,
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
@@ -453,25 +332,23 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
loadWebUIPort();
|
||||
});
|
||||
const refresh = async () => {
|
||||
const { data } = await checkAppInstalled('ollama', props.instanceName || '');
|
||||
appInstallID.value = data.appInstallId;
|
||||
modelInfo.status = data.status;
|
||||
await search();
|
||||
};
|
||||
onMounted(refresh);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iconInTable {
|
||||
margin-left: 5px;
|
||||
margin-top: 3px;
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.jumpAdd {
|
||||
margin-top: 10px;
|
||||
margin-left: 15px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tagClass {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
.model-section-heading {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<component
|
||||
:is="inline ? 'div' : DrawerPro"
|
||||
v-bind="inline ? {} : { modelValue: open, header: $t('commons.button.install'), size: 'large' }"
|
||||
@update:model-value="open = $event"
|
||||
@close="handleClose"
|
||||
>
|
||||
<AppInstallForm ref="formRef" v-model="form" :loading="loading">
|
||||
<template #after-params>
|
||||
<AccountAddress
|
||||
ref="accountRef"
|
||||
:form="form"
|
||||
:host-port="Number(form.params.PANEL_APP_PORT_HTTP) || 11434"
|
||||
container-port="11434"
|
||||
/>
|
||||
</template>
|
||||
</AppInstallForm>
|
||||
<template v-if="!inline" #footer>
|
||||
<el-button :disabled="loading" @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading || !form.appDetailId"
|
||||
@click="submit"
|
||||
>
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { installApp } from '@/api/modules/app';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import DrawerPro from '@/components/drawer-pro/index.vue';
|
||||
import AppInstallForm from '@/views/app-store/detail/form/index.vue';
|
||||
import AccountAddress from '../../components/account-address/index.vue';
|
||||
|
||||
const props = defineProps({ inline: Boolean });
|
||||
const emit = defineEmits(['search', 'task', 'close']);
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const formRef = ref<InstanceType<typeof AppInstallForm>>();
|
||||
const accountRef = ref();
|
||||
const form = ref({
|
||||
appDetailId: 0,
|
||||
name: '',
|
||||
params: {} as Record<string, any>,
|
||||
containerName: '',
|
||||
modelAccountBaseURLType: 'container',
|
||||
modelAccountBaseURL: '',
|
||||
});
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
formRef.value?.resetForm();
|
||||
emit('close');
|
||||
};
|
||||
const openCreate = async () => {
|
||||
open.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
await nextTick();
|
||||
if (!open.value) return;
|
||||
formRef.value.resetForm();
|
||||
formRef.value.setFormData({
|
||||
restartPolicy: 'unless-stopped',
|
||||
modelAccountBaseURLType: 'container',
|
||||
modelAccountBaseURL: '',
|
||||
});
|
||||
await formRef.value.initForm('ollama', true);
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const submit = async () => {
|
||||
if (!form.value.appDetailId || !(await formRef.value?.validate())) return;
|
||||
const data = formRef.value.getFormData();
|
||||
if (data.editCompose && !data.dockerCompose.trim()) {
|
||||
MsgError(i18n.global.t('app.composeNullErr'));
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const taskID = newUUID();
|
||||
try {
|
||||
await installApp({
|
||||
...data,
|
||||
modelAccountBaseURL: accountRef.value.baseURL,
|
||||
taskID,
|
||||
});
|
||||
emit('search');
|
||||
emit('task', taskID);
|
||||
open.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => form.value.name,
|
||||
(name) => {
|
||||
form.value.containerName = name;
|
||||
},
|
||||
);
|
||||
watch(open, (value) => {
|
||||
if (props.inline && !value) handleClose();
|
||||
});
|
||||
defineExpose({ openCreate, submit, loading, disabled: computed(() => !form.value.appDetailId) });
|
||||
</script>
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="open"
|
||||
:title="$t('menu.terminal')"
|
||||
@close="handleClose"
|
||||
:resource="title"
|
||||
:autoClose="false"
|
||||
:fullScreen="true"
|
||||
>
|
||||
<Terminal class="mt-2" style="height: calc(100vh - 175px)" ref="terminalRef"></Terminal>
|
||||
<DialogPro v-model="open" :title="$t('menu.terminal')" size="w-70" @close="handleClose">
|
||||
<Terminal class="mt-2" style="height: 55vh" ref="terminalRef"></Terminal>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -16,23 +9,26 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { nextTick, onBeforeUnmount, ref } from 'vue';
|
||||
import Terminal from '@/components/terminal/index.vue';
|
||||
import { closeOllamaModel } from '@/api/modules/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const title = ref();
|
||||
const { currentNode } = useGlobalStore();
|
||||
const open = ref(false);
|
||||
const itemName = ref();
|
||||
const terminalRef = ref();
|
||||
let operateNode = '';
|
||||
|
||||
interface DialogProps {
|
||||
name: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
operateNode = currentNode.value;
|
||||
itemName.value = params.name;
|
||||
open.value = true;
|
||||
initTerm();
|
||||
@@ -40,7 +36,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
|
||||
const initTerm = () => {
|
||||
nextTick(() => {
|
||||
terminalRef.value.acceptParams({
|
||||
if (!open.value) return;
|
||||
terminalRef.value?.acceptParams({
|
||||
endpoint: '/api/v2/hosts/terminal/container',
|
||||
args: `source=ollama&name=${itemName.value}`,
|
||||
error: '',
|
||||
@@ -50,7 +47,10 @@ const initTerm = () => {
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
await closeOllamaModel(itemName.value)
|
||||
const name = itemName.value;
|
||||
if (!name) return;
|
||||
itemName.value = '';
|
||||
await closeOllamaModel(name, operateNode)
|
||||
.then(() => {
|
||||
terminalRef.value?.onClose();
|
||||
})
|
||||
@@ -60,9 +60,10 @@ const onClose = async () => {
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
onClose();
|
||||
open.value = false;
|
||||
onClose();
|
||||
}
|
||||
onBeforeUnmount(handleClose);
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import OperateDialog from './operate/index.vue';
|
||||
import ComposeLogs from '@/components/log/compose/index.vue';
|
||||
@@ -111,7 +112,8 @@ useSearchState(
|
||||
},
|
||||
{ onRevisit: () => search() },
|
||||
);
|
||||
const searchName = ref();
|
||||
const route = useRoute();
|
||||
const searchName = ref(typeof route.query.name === 'string' ? route.query.name : '');
|
||||
const opRef = ref();
|
||||
const dialogRef = ref();
|
||||
const composeLogRef = ref();
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<DrawerPro :header="$t('commons.button.' + mode)" v-model="drawerVisiable" size="large" @close="handleClose">
|
||||
<component
|
||||
:is="inline ? 'div' : dialog ? DialogPro : DrawerPro"
|
||||
v-bind="
|
||||
inline
|
||||
? {}
|
||||
: {
|
||||
modelValue: drawerVisiable,
|
||||
header: $t('commons.button.' + mode),
|
||||
title: $t('commons.button.' + mode),
|
||||
size: 'large',
|
||||
}
|
||||
"
|
||||
@update:model-value="drawerVisiable = $event"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-alert :title="$t('aiTools.tensorRT.imageAlert')" class="common-prompt" :closable="false" type="warning" />
|
||||
<el-form ref="formRef" label-position="top" :model="tensorRTLLM" :rules="rules" v-loading="loading">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
@@ -43,7 +57,7 @@
|
||||
</el-form-item>
|
||||
<NodeConfig v-model="tensorRTLLM" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<template v-if="!inline" #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button v-permission :disabled="loading" type="primary" @click="onSubmit">
|
||||
@@ -52,10 +66,12 @@
|
||||
</span>
|
||||
</template>
|
||||
<FileList ref="modelDirRef" @choose="getModelDir" />
|
||||
</DrawerPro>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DialogPro from '@/components/dialog-pro/index.vue';
|
||||
const props = defineProps({ dialog: Boolean, inline: Boolean });
|
||||
import NodeConfig from '@/views/website/runtime//components/node-config.vue';
|
||||
import DrawerPro from '@/components/drawer-pro/index.vue';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
@@ -90,7 +106,7 @@ const newTensorRTLLM = () => {
|
||||
};
|
||||
const modelDirRef = ref();
|
||||
const tensorRTLLM = ref(newTensorRTLLM());
|
||||
const emit = defineEmits(['search']);
|
||||
const emit = defineEmits(['search', 'close']);
|
||||
|
||||
const openCreate = (): void => {
|
||||
mode.value = 'create';
|
||||
@@ -119,6 +135,7 @@ const openEdit = (rowData: any): void => {
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisiable.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const getModelDir = (path: string) => {
|
||||
@@ -186,8 +203,14 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(drawerVisiable, (value) => {
|
||||
if (props.inline && !value) handleClose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
openCreate,
|
||||
submit: onSubmit,
|
||||
loading,
|
||||
openEdit,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -258,7 +258,9 @@ onMounted(async () => {
|
||||
key: installKey.value,
|
||||
},
|
||||
};
|
||||
installRef.value.acceptParams(params);
|
||||
if (!['ollama', 'vllm'].includes(installKey.value) || !jumpToInstall('', installKey.value)) {
|
||||
installRef.value.acceptParams(params);
|
||||
}
|
||||
}
|
||||
search(req);
|
||||
if (isProductPro.value) {
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
:propStart="'params.'"
|
||||
/>
|
||||
|
||||
<slot name="after-params" />
|
||||
|
||||
<el-form-item prop="advanced">
|
||||
<el-checkbox v-model="formData.advanced" :label="$t('app.advanced')" size="large" />
|
||||
</el-form-item>
|
||||
@@ -320,19 +322,23 @@ const loadInstallDefaultConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const initForm = async (appKey: string) => {
|
||||
const initForm = async (appKey: string, allowPort?: boolean) => {
|
||||
operateNode.value = undefined;
|
||||
env.value = undefined;
|
||||
masterNodeAddr.value = undefined;
|
||||
formData.value.name = appKey.replace(/^local/, '');
|
||||
await loadInstallDefaultConfig();
|
||||
if (allowPort === undefined) {
|
||||
await loadInstallDefaultConfig();
|
||||
} else {
|
||||
formData.value.allowPort = allowPort;
|
||||
}
|
||||
const res = await getAppByKey(appKey);
|
||||
currentApp.value = res.data;
|
||||
appVersions.value = currentApp.value.versions;
|
||||
if (appVersions.value.length > 0) {
|
||||
const defaultVersion = appVersions.value[0];
|
||||
formData.value.version = defaultVersion;
|
||||
getVersionDetail(defaultVersion);
|
||||
await getVersionDetail(defaultVersion);
|
||||
}
|
||||
if (isOffline.value) {
|
||||
formData.value.pullImage = false;
|
||||
|
||||
@@ -178,72 +178,38 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.state !== 'running'">—</span>
|
||||
<div v-else-if="row.hasLoad">
|
||||
<div class="source-font">CPU: {{ row.cpuPercent.toFixed(2) }}%</div>
|
||||
<div class="float-left source-font">
|
||||
{{ $t('monitor.memory') }}: {{ row.memoryPercent.toFixed(2) }}%
|
||||
</div>
|
||||
<el-popover placement="right" width="500px" class="float-right">
|
||||
<template #reference>
|
||||
<svg-icon iconName="p-xiangqing" className="resource-detail-icon"></svg-icon>
|
||||
<ContainerResource v-else-if="row.hasLoad" :stats="row">
|
||||
<el-descriptions-item v-if="row.hasLoadSize">
|
||||
<template #label>
|
||||
{{ $t('container.sizeRw') }}
|
||||
<el-tooltip :content="$t('container.sizeRwHelper')">
|
||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions direction="vertical" border :column="3" size="small">
|
||||
<el-descriptions-item :label="$t('container.cpuUsage')">
|
||||
{{ computeCPU(row.cpuTotalUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.cpuTotal')">
|
||||
{{ computeCPU(row.systemUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.core')">
|
||||
{{ row.percpuUsage }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :label="$t('container.memUsage')">
|
||||
{{ computeSizeForDocker(row.memoryUsage) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.memCache')">
|
||||
{{ computeSizeForDocker(row.memoryCache) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.memTotal')">
|
||||
{{ computeSizeForDocker(row.memoryLimit) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="row.hasLoadSize">
|
||||
<template #label>
|
||||
{{ $t('container.sizeRw') }}
|
||||
<el-tooltip :content="$t('container.sizeRwHelper')">
|
||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
{{ computeSize2(row.sizeRw) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
:label="$t('container.sizeRootFs')"
|
||||
v-if="row.hasLoadSize"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('container.sizeRootFs') }}
|
||||
<el-tooltip :content="$t('container.sizeRootFsHelper')">
|
||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
{{ computeSize2(row.sizeRootFs) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-button
|
||||
class="mt-2"
|
||||
v-if="!row.hasLoadSize"
|
||||
size="small"
|
||||
link
|
||||
type="primary"
|
||||
@click="loadSize(row)"
|
||||
>
|
||||
{{ $t('container.loadSize') }}
|
||||
</el-button>
|
||||
{{ computeSize2(row.sizeRw) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.sizeRootFs')" v-if="row.hasLoadSize">
|
||||
<template #label>
|
||||
{{ $t('container.sizeRootFs') }}
|
||||
<el-tooltip :content="$t('container.sizeRootFsHelper')">
|
||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
{{ computeSize2(row.sizeRootFs) }}
|
||||
</el-descriptions-item>
|
||||
<template #footer>
|
||||
<el-button
|
||||
class="mt-2"
|
||||
v-if="!row.hasLoadSize"
|
||||
size="small"
|
||||
link
|
||||
type="primary"
|
||||
@click="loadSize(row)"
|
||||
>
|
||||
{{ $t('container.loadSize') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ContainerResource>
|
||||
<div v-else>
|
||||
<el-button link loading></el-button>
|
||||
</div>
|
||||
@@ -520,7 +486,8 @@ import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
||||
import router from '@/routers';
|
||||
import { computeSize2, computeSizeForDocker, computeCPU } from '@/utils/size';
|
||||
import { computeSize2 } from '@/utils/size';
|
||||
import ContainerResource from '@/components/container-resource/index.vue';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { updateCommonDescription } from '@/api/modules/setting';
|
||||
|
||||
@@ -789,7 +756,7 @@ const loadContainers = async (background = false, column?: any) => {
|
||||
}
|
||||
const ids = containers.filter((item) => item.state === 'running').map((item) => item.containerID);
|
||||
if (ids.length > 0) {
|
||||
const stats = await containerListStats(undefined, ids, controller.signal);
|
||||
const stats = await containerListStats(undefined, ids, { signal: controller.signal });
|
||||
if (!controller.signal.aborted) {
|
||||
applyStatsToRows(stats.data || []);
|
||||
}
|
||||
@@ -1141,14 +1108,6 @@ const buttons = [
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.source-font {
|
||||
font-size: 12px;
|
||||
}
|
||||
.resource-detail-icon {
|
||||
margin-top: -3px;
|
||||
font-size: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag-button {
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
|
||||
Reference in New Issue
Block a user