diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 45b63f924..8cda5d933 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -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"` diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index be5274fba..e3d1502d4 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -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"` diff --git a/agent/app/dto/request/app.go b/agent/app/dto/request/app.go index 0091a50f6..eeaea57df 100644 --- a/agent/app/dto/request/app.go +++ b/agent/app/dto/request/app.go @@ -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 } diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go index afae905ac..27cca6abe 100644 --- a/agent/app/model/agent_account.go +++ b/agent/app/model/agent_account.go @@ -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"` diff --git a/agent/app/repo/agent_account.go b/agent/app/repo/agent_account.go index 18246cab5..dde50b4d9 100644 --- a/agent/app/repo/agent_account.go +++ b/agent/app/repo/agent_account.go @@ -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)) diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index d49f1ddb9..bf90f4d24 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -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 { diff --git a/agent/app/service/agents_local.go b/agent/app/service/agents_local.go new file mode 100644 index 000000000..f2f7ae90d --- /dev/null +++ b/agent/app/service/agents_local.go @@ -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 + } +} diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 1386f91fe..0d549cf26 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -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) diff --git a/agent/app/service/ai.go b/agent/app/service/ai.go index 5ab8af8be..ce567f63c 100644 --- a/agent/app/service/ai.go +++ b/agent/app/service/ai.go @@ -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 { diff --git a/agent/app/service/app.go b/agent/app/service/app.go index f919235e5..3ae08288a 100644 --- a/agent/app/service/app.go +++ b/agent/app/service/app.go @@ -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 } diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index a8b5db48b..585202d74 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -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 { diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 07529ac18..0d823f411 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -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) } }() } diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index c15edb197..86f9a8920 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -117,6 +117,7 @@ func agentDBMigrations() []*gormigrate.Migration { migrations.AddVLLMSampleInterval, migrations.AddNativeAgentInstalls, migrations.AddQuickDirectory, + migrations.AddLocalModelAccountLink, } } diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index b5b821c2f..3b520fbc9 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -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) + }, +} diff --git a/core/constant/common.go b/core/constant/common.go index fff4d09c5..39fa6cff2 100644 --- a/core/constant/common.go +++ b/core/constant/common.go @@ -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": {}, diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index c7598d4b6..b09471ab3 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -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}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 6b3ab9186..b0f1aa5e0 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -69,5 +69,6 @@ func coreMigrations() []*gormigrate.Migration { migrations.MoveVirtualMachineMenuToXpack, migrations.AddAPIKeys, migrations.AddOperationLogAPIKey, + migrations.UpdateAiMenuOrder, } } diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index 5f5bdf1ff..4c505c219 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -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 { diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index bbaa9a1cb..b1157258a 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -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; diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index 314b880e8..7e37f1c0e 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -136,6 +136,8 @@ export namespace App { } export interface AppInstall { + modelAccountBaseURLType?: string; + modelAccountBaseURL?: string; appDetailId: number; params: any; taskID: string; diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index ebc50dbac..cb1f65718 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -177,6 +177,7 @@ export namespace Container { } export interface ContainerListStats { containerID: string; + containerName: string; cpuTotalUsage: number; systemUsage: number; cpuPercent: number; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 68a0ba51b..96b041dcb 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -22,8 +22,13 @@ export const loadOllamaModel = (name: string) => { export const syncOllamaModel = () => { return http.post>(`/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) => { diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index a98ab5fa7..7a48f2ab1 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -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>(`/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) => { diff --git a/frontend/src/assets/images/ai-providers/nvidia.ico b/frontend/src/assets/images/ai-providers/nvidia.ico new file mode 100644 index 000000000..424df8720 Binary files /dev/null and b/frontend/src/assets/images/ai-providers/nvidia.ico differ diff --git a/frontend/src/components/container-resource/index.vue b/frontend/src/components/container-resource/index.vue new file mode 100644 index 000000000..f8400cb1a --- /dev/null +++ b/frontend/src/components/container-resource/index.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f9871eaa4..8962f92c6 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 695edc8a1..00ed9d4fa 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index c5862f365..2c3494bea 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -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: 'جزئیات مدل', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 6d8ebf1a2..509e17dae 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: 'モデル詳細', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 4643d6cb9..10205091b 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '모델 상세 정보', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 2b9e893a9..8ccf38423 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -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: 'ລາຍລະອຽດໂມເດວ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 9ef15c9e7..8c4886008 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1386e8e0f..9afb7875a 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e13aaa2eb..8400f0e9b 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'Сведения о модели', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 94483c987..420df9647 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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ı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index e94a5a616..7e0b33557 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '模型詳情', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 5d9858eee..bc7289603 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '模型详情', diff --git a/frontend/src/routers/modules/ai.ts b/frontend/src/routers/modules/ai.ts index 9fd582635..5077c1f6d 100644 --- a/frontend/src/routers/modules/ai.ts +++ b/frontend/src/routers/modules/ai.ts @@ -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', diff --git a/frontend/src/utils/agent-provider-logo.ts b/frontend/src/utils/agent-provider-logo.ts index cb0bde4a9..e7cc96b28 100644 --- a/frontend/src/utils/agent-provider-logo.ts +++ b/frontend/src/utils/agent-provider-logo.ts @@ -28,6 +28,14 @@ const volcengineLogo: AgentProviderLogo = { }; const providerLogos: Record = { + 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', diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index aae22f57f..6bd854e74 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -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({ diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index 2482663e1..7227def47 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -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'; diff --git a/frontend/src/views/ai/agents/model/index.vue b/frontend/src/views/ai/agents/model/index.vue deleted file mode 100644 index c18031415..000000000 --- a/frontend/src/views/ai/agents/model/index.vue +++ /dev/null @@ -1,221 +0,0 @@ - - - - - diff --git a/frontend/src/views/ai/model/components/account-address/index.vue b/frontend/src/views/ai/model/components/account-address/index.vue new file mode 100644 index 000000000..f9070da68 --- /dev/null +++ b/frontend/src/views/ai/model/components/account-address/index.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/model/components/account-form/index.vue similarity index 97% rename from frontend/src/views/ai/agents/model/add/index.vue rename to frontend/src/views/ai/model/components/account-form/index.vue index f955e83da..ecb3f7d88 100644 --- a/frontend/src/views/ai/agents/model/add/index.vue +++ b/frontend/src/views/ai/model/components/account-form/index.vue @@ -1,5 +1,10 @@ diff --git a/frontend/src/views/ai/model/ollama/operate/index.vue b/frontend/src/views/ai/model/ollama/operate/index.vue new file mode 100644 index 000000000..b4310c09a --- /dev/null +++ b/frontend/src/views/ai/model/ollama/operate/index.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/views/ai/model/ollama/terminal/index.vue b/frontend/src/views/ai/model/ollama/terminal/index.vue index 1fb15f94e..ac1110f55 100644 --- a/frontend/src/views/ai/model/ollama/terminal/index.vue +++ b/frontend/src/views/ai/model/ollama/terminal/index.vue @@ -1,13 +1,6 @@ diff --git a/frontend/src/views/app-store/apps/index.vue b/frontend/src/views/app-store/apps/index.vue index a1ef3fb2b..5acc99476 100644 --- a/frontend/src/views/app-store/apps/index.vue +++ b/frontend/src/views/app-store/apps/index.vue @@ -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) { diff --git a/frontend/src/views/app-store/detail/form/index.vue b/frontend/src/views/app-store/detail/form/index.vue index c4a962d85..fbe1488af 100644 --- a/frontend/src/views/app-store/detail/form/index.vue +++ b/frontend/src/views/app-store/detail/form/index.vue @@ -48,6 +48,8 @@ :propStart="'params.'" /> + + @@ -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; diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index e2e209fba..e80bea076 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -178,72 +178,38 @@ >