mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-03 16:05:26 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30381a3c68 | ||
|
|
37170ae81a |
@@ -305,6 +305,15 @@ func (b *BaseApi) PageLocalDownloadedModels(c *gin.Context) {
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
func (b *BaseApi) ListLocalModelOptions(c *gin.Context) {
|
||||
items, err := modelDownloaderService.ListLocalModelOptions()
|
||||
if err != nil {
|
||||
handleModelDownloaderError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, items)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete locally downloaded model
|
||||
// @Accept json
|
||||
|
||||
@@ -92,6 +92,12 @@ type ModelDownloadTaskItem struct {
|
||||
CanRemoveRecord bool `json:"canRemoveRecord"`
|
||||
}
|
||||
|
||||
type LocalModelOption struct {
|
||||
Name string `json:"name"`
|
||||
Organization string `json:"organization"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type LocalModelSearch struct {
|
||||
DownloadType string `json:"downloadType" validate:"omitempty,oneof=all tokenizer"`
|
||||
PageInfo
|
||||
@@ -121,6 +127,7 @@ type FeaturedModel struct {
|
||||
Sort int `json:"sort" yaml:"sort"`
|
||||
Family string `json:"family" yaml:"category"`
|
||||
RepoID string `json:"repoID" yaml:"name"`
|
||||
WeightProvider string `json:"weightProvider" yaml:"weightProvider"`
|
||||
Sources []string `json:"sources" yaml:"sources"`
|
||||
SourceModelIDs map[string]string `json:"sourceModelIds" yaml:"sourceModelIds"`
|
||||
Architecture string `json:"architecture" yaml:"modelType"`
|
||||
@@ -133,11 +140,13 @@ type FeaturedModel struct {
|
||||
|
||||
type FeaturedModelSearch struct {
|
||||
PageInfo
|
||||
Family string `json:"family"`
|
||||
Info string `json:"info"`
|
||||
Family string `json:"family"`
|
||||
WeightProvider string `json:"weightProvider"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type FeaturedModelPage struct {
|
||||
PageResult
|
||||
Families []string `json:"families"`
|
||||
Families []string `json:"families"`
|
||||
WeightProviders []string `json:"weightProviders"`
|
||||
}
|
||||
|
||||
@@ -20,11 +20,15 @@ type NginxParam struct {
|
||||
// NginxBrotliRes carries the brotli settings together with where they live.
|
||||
// ManagedExternally is true when the user defined brotli by hand, in which
|
||||
// case the panel only reports the values and must not write its own copy.
|
||||
// ManagedUnavailable is true when the panel could not wire the managed
|
||||
// ManagedPending is true when nginx does not load the managed configuration
|
||||
// yet but saving will wire it in, so the reported values are not in effect
|
||||
// until then.
|
||||
// ManagedUnavailable is true when the panel cannot wire the managed
|
||||
// configuration into nginx.conf at all, so the reported values are inert.
|
||||
type NginxBrotliRes struct {
|
||||
Params []NginxParam `json:"params"`
|
||||
ManagedExternally bool `json:"managedExternally"`
|
||||
ManagedPending bool `json:"managedPending"`
|
||||
ManagedUnavailable bool `json:"managedUnavailable"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ type FeaturedModel struct {
|
||||
Sort int `json:"sort"`
|
||||
Family string `json:"family"`
|
||||
RepoID string `json:"repoID"`
|
||||
WeightProvider string `json:"weightProvider"`
|
||||
Sources []string `json:"sources" gorm:"serializer:json"`
|
||||
SourceModelIDs map[string]string `json:"sourceModelIds" gorm:"serializer:json"`
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
type FeaturedModelRepo struct{}
|
||||
|
||||
type IFeaturedModelRepo interface {
|
||||
Page(page, size int, family, info string) (int64, []model.FeaturedModel, error)
|
||||
Page(page, size int, family, weightProvider, info string) (int64, []model.FeaturedModel, error)
|
||||
Families() ([]string, error)
|
||||
WeightProviders() ([]string, error)
|
||||
SyncAll(models []model.FeaturedModel, version string) error
|
||||
}
|
||||
|
||||
@@ -20,13 +21,16 @@ func NewIFeaturedModelRepo() IFeaturedModelRepo {
|
||||
return &FeaturedModelRepo{}
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) Page(page, size int, family, info string) (int64, []model.FeaturedModel, error) {
|
||||
func (r *FeaturedModelRepo) Page(page, size int, family, weightProvider, info string) (int64, []model.FeaturedModel, error) {
|
||||
var total int64
|
||||
var items []model.FeaturedModel
|
||||
db := global.DB.Model(&model.FeaturedModel{})
|
||||
if family != "" {
|
||||
db = db.Where("family = ?", family)
|
||||
}
|
||||
if weightProvider != "" {
|
||||
db = db.Where("weight_provider = ?", weightProvider)
|
||||
}
|
||||
if info != "" {
|
||||
db = db.Where("LOWER(repo_id) LIKE ?", "%"+strings.ToLower(info)+"%")
|
||||
}
|
||||
@@ -45,6 +49,12 @@ func (r *FeaturedModelRepo) Families() ([]string, error) {
|
||||
return families, err
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) WeightProviders() ([]string, error) {
|
||||
providers := make([]string, 0)
|
||||
err := global.DB.Model(&model.FeaturedModel{}).Where("weight_provider <> ?", "").Distinct("weight_provider").Order("weight_provider asc").Pluck("weight_provider", &providers).Error
|
||||
return providers, err
|
||||
}
|
||||
|
||||
func (r *FeaturedModelRepo) SyncAll(models []model.FeaturedModel, version string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("1 = 1").Delete(&model.FeaturedModel{}).Error; err != nil {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
var featuredModelsSyncMu sync.Mutex
|
||||
|
||||
func (m *ModelDownloaderService) PageFeaturedModels(req dto.FeaturedModelSearch) (*dto.FeaturedModelPage, error) {
|
||||
total, models, err := featuredModelRepo.Page(req.Page, req.PageSize, strings.TrimSpace(req.Family), strings.TrimSpace(req.Info))
|
||||
total, models, err := featuredModelRepo.Page(req.Page, req.PageSize, strings.TrimSpace(req.Family), strings.TrimSpace(req.WeightProvider), strings.TrimSpace(req.Info))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -31,8 +31,8 @@ func (m *ModelDownloaderService) PageFeaturedModels(req dto.FeaturedModelSearch)
|
||||
}
|
||||
items = append(items, dto.FeaturedModel{
|
||||
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
|
||||
SourceModelIDs: item.SourceModelIDs,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
SourceModelIDs: item.SourceModelIDs, WeightProvider: item.WeightProvider,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
|
||||
Description: description,
|
||||
})
|
||||
@@ -41,7 +41,11 @@ func (m *ModelDownloaderService) PageFeaturedModels(req dto.FeaturedModelSearch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.FeaturedModelPage{PageResult: dto.PageResult{Total: total, Items: items}, Families: families}, nil
|
||||
weightProviders, err := featuredModelRepo.WeightProviders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.FeaturedModelPage{PageResult: dto.PageResult{Total: total, Items: items}, Families: families, WeightProviders: weightProviders}, nil
|
||||
}
|
||||
|
||||
func (m *ModelDownloaderService) SyncFeaturedModels() error {
|
||||
@@ -74,6 +78,12 @@ func (m *ModelDownloaderService) SyncFeaturedModels() error {
|
||||
item.Key = strings.TrimSpace(item.Key)
|
||||
item.Family = strings.TrimSpace(item.Family)
|
||||
item.RepoID = strings.TrimSpace(item.RepoID)
|
||||
item.WeightProvider = strings.TrimSpace(item.WeightProvider)
|
||||
if item.WeightProvider == "" {
|
||||
if provider, _, ok := strings.Cut(item.RepoID, "/"); ok {
|
||||
item.WeightProvider = provider
|
||||
}
|
||||
}
|
||||
if item.Key == "" || item.Family == "" || item.RepoID == "" || (item.Architecture != "MoE" && item.Architecture != "Dense") || item.WeightGB < 0 {
|
||||
return fmt.Errorf("invalid featured model: %s", item.Key)
|
||||
}
|
||||
@@ -95,8 +105,8 @@ func (m *ModelDownloaderService) SyncFeaturedModels() error {
|
||||
}
|
||||
models = append(models, model.FeaturedModel{
|
||||
Key: item.Key, Sort: item.Sort, Family: item.Family, RepoID: item.RepoID, Sources: item.Sources,
|
||||
SourceModelIDs: item.SourceModelIDs,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
SourceModelIDs: item.SourceModelIDs, WeightProvider: item.WeightProvider,
|
||||
Architecture: item.Architecture, BackboneParams: item.BackboneParams,
|
||||
ActiveParams: item.ActiveParams, Engram: item.Engram, WeightGB: item.WeightGB,
|
||||
Description: string(description),
|
||||
})
|
||||
|
||||
@@ -59,6 +59,7 @@ type IModelDownloaderService interface {
|
||||
RemoveTask(req dto.ModelDownloadTaskOperate) error
|
||||
RecoverInterruptedTasks() error
|
||||
ListLocalModels(req dto.LocalModelSearch) (int64, []dto.LocalModelItem, error)
|
||||
ListLocalModelOptions() ([]dto.LocalModelOption, error)
|
||||
DeleteLocalModel(req dto.LocalModelDelete) error
|
||||
}
|
||||
|
||||
@@ -393,6 +394,10 @@ func (m *ModelDownloaderService) ListLocalModels(req dto.LocalModelSearch) (int6
|
||||
return int64(len(items)), pageLocalModels(items, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
func (m *ModelDownloaderService) ListLocalModelOptions() ([]dto.LocalModelOption, error) {
|
||||
return listLocalModelOptions(loadModelDownloaderDir())
|
||||
}
|
||||
|
||||
func (m *ModelDownloaderService) DeleteLocalModel(req dto.LocalModelDelete) error {
|
||||
if req.Name != strings.TrimSpace(req.Name) {
|
||||
return newModelDownloaderInputError("invalid model directory")
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
)
|
||||
|
||||
func listLocalModelOptions(root string) ([]dto.LocalModelOption, error) {
|
||||
items := make([]dto.LocalModelOption, 0)
|
||||
entries, err := os.ReadDir(root)
|
||||
if os.IsNotExist(err) {
|
||||
return items, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scan func(string, string) error
|
||||
scan = func(path, organization string) error {
|
||||
children, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range children {
|
||||
if child.IsDir() || child.Type()&os.ModeSymlink != 0 || strings.HasPrefix(child.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
name := child.Name()
|
||||
if name == "config.json" || name == "params.json" {
|
||||
items = append(items, dto.LocalModelOption{Name: filepath.Base(path), Organization: organization, Path: path})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if organization == "" {
|
||||
for _, child := range children {
|
||||
if child.IsDir() && !strings.HasPrefix(child.Name(), ".") {
|
||||
if err := scan(filepath.Join(path, child.Name()), filepath.Base(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
|
||||
if err := scan(filepath.Join(root, entry.Name()), ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
left, right := strings.ToLower(items[i].Name), strings.ToLower(items[j].Name)
|
||||
if left == right {
|
||||
return items[i].Path < items[j].Path
|
||||
}
|
||||
return left < right
|
||||
})
|
||||
return items, nil
|
||||
}
|
||||
@@ -45,6 +45,10 @@ var (
|
||||
// nginxHTTPBlockStartRe locates the http block opening, the fallback
|
||||
// insertion point, and captures its indentation.
|
||||
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
|
||||
|
||||
// errNginxHTTPIncludeNoInsertionPoint tells a config the panel cannot edit
|
||||
// safely apart from an I/O failure while editing it.
|
||||
errNginxHTTPIncludeNoInsertionPoint = errors.New("no insertion point for the http.d include in nginx.conf")
|
||||
)
|
||||
|
||||
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
|
||||
@@ -96,7 +100,19 @@ func insertNginxHTTPInclude(content string) (string, error) {
|
||||
indent := content[m[2]:m[3]] + " "
|
||||
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
|
||||
}
|
||||
return "", errors.New("no insertion point for the http.d include in nginx.conf")
|
||||
return "", errNginxHTTPIncludeNoInsertionPoint
|
||||
}
|
||||
|
||||
// nginxHTTPIncludeInsertable reports whether nginx.conf already loads http.d
|
||||
// or the include can be added automatically. Nothing is written: the
|
||||
// insertion is computed in memory and discarded.
|
||||
func nginxHTTPIncludeInsertable(install model.AppInstall) bool {
|
||||
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = insertNginxHTTPInclude(string(content))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -83,9 +84,15 @@ func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
|
||||
if _, err := insertNginxHTTPInclude("events {}\n"); err == nil {
|
||||
_, err := insertNginxHTTPInclude("events {}\n")
|
||||
if err == nil {
|
||||
t.Fatal("a config without an http block must be rejected so callers can degrade")
|
||||
}
|
||||
// The save path maps this error to an actionable message and must be able
|
||||
// to tell it apart from an I/O failure.
|
||||
if !errors.Is(err, errNginxHTTPIncludeNoInsertionPoint) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
|
||||
|
||||
@@ -224,12 +224,9 @@ func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||
}
|
||||
res := &response.NginxBrotliRes{
|
||||
ManagedExternally: managedExternally,
|
||||
// Without the include, values the panel would write would never reach
|
||||
// nginx, so they are reported as unavailable rather than shown as if
|
||||
// they were in effect.
|
||||
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
|
||||
res := &response.NginxBrotliRes{ManagedExternally: managedExternally}
|
||||
if !managedExternally {
|
||||
res.ManagedPending, res.ManagedUnavailable = nginxBrotliManagedState(install)
|
||||
}
|
||||
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
|
||||
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
|
||||
@@ -237,6 +234,28 @@ func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// nginxBrotliManagedState reports whether the managed brotli file is missing
|
||||
// from what nginx loads, and if so whether saving from the settings page can
|
||||
// fix that. Either way the reported values are not in effect, so they must
|
||||
// not be presented as if they were.
|
||||
//
|
||||
// A missing include is the common case for installs that enabled brotli
|
||||
// before the panel managed its directives, and the save path inserts it. Only
|
||||
// a config without a safe insertion point is reported as unavailable.
|
||||
func nginxBrotliManagedState(install model.AppInstall) (pending, unavailable bool) {
|
||||
if !nginxHTTPIncludePresent(install) {
|
||||
if nginxHTTPIncludeInsertable(install) {
|
||||
return true, false
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
if _, err := os.Stat(path.Join(nginxHTTPConfigDir(install), fileName)); os.IsNotExist(err) {
|
||||
return true, false
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
|
||||
// in any of the files nginx loads them from.
|
||||
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
|
||||
@@ -313,26 +332,19 @@ func updateNginxBrotliParams(params []dto.NginxParam) error {
|
||||
return updateUserNginxBrotliParams(install, values)
|
||||
}
|
||||
// A managed write needs the include. Installations missing it are
|
||||
// upgraded in place here; when nginx.conf cannot be edited safely the
|
||||
// write is refused with an actionable error instead of writing values
|
||||
// nginx would never load.
|
||||
if !nginxHTTPIncludePresent(install) {
|
||||
configPath := nginxMainConfigPath(install)
|
||||
content, readErr := os.ReadFile(configPath)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
updated, insErr := insertNginxHTTPInclude(string(content))
|
||||
if insErr != nil {
|
||||
// upgraded in place here, through the same path that enabling the
|
||||
// module takes; when nginx.conf cannot be edited safely the write is
|
||||
// refused with an actionable error instead of writing values nginx
|
||||
// would never load.
|
||||
_, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
|
||||
if includeErr != nil {
|
||||
if errors.Is(includeErr, errNginxHTTPIncludeNoInsertionPoint) {
|
||||
return buserr.New("ErrBrotliUnsupported")
|
||||
}
|
||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
||||
return includeErr
|
||||
}
|
||||
if confSnapshot != nil {
|
||||
if err = nginxCheckAndReload(string(confSnapshot), nginxMainConfigPath(install), install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,3 +229,85 @@ func TestValidateNginxBrotliValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newBrotliStateInstall lays out an OpenResty install whose nginx.conf holds
|
||||
// conf, or that has no nginx.conf at all when conf is empty.
|
||||
func newBrotliStateInstall(t *testing.T, conf string) model.AppInstall {
|
||||
t.Helper()
|
||||
installRoot := t.TempDir()
|
||||
confDir := path.Join(installRoot, "openresty", "openresty", "conf")
|
||||
if err := os.MkdirAll(confDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf != "" {
|
||||
if err := os.WriteFile(path.Join(confDir, "nginx.conf"), []byte(conf), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
previous := global.Dir.AppInstallDir
|
||||
global.Dir.AppInstallDir = installRoot
|
||||
t.Cleanup(func() { global.Dir.AppInstallDir = previous })
|
||||
install := model.AppInstall{Name: "openresty"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
return install
|
||||
}
|
||||
|
||||
// Installs that enabled brotli before the panel managed its directives have
|
||||
// no http.d include. Saving inserts it, so the settings page must report the
|
||||
// values as pending rather than as something the panel can never apply.
|
||||
func TestNginxBrotliManagedState(t *testing.T) {
|
||||
withInclude, err := insertNginxHTTPInclude(plainNginxConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
managedFile := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
cases := []struct {
|
||||
name string
|
||||
conf string
|
||||
managedFile bool
|
||||
wantPending bool
|
||||
wantUnavailable bool
|
||||
}{
|
||||
{"include missing but insertable", plainNginxConf, false, true, false},
|
||||
{"no insertion point", "events {}\n", false, false, true},
|
||||
{"nginx.conf unreadable", "", false, false, true},
|
||||
{"include present, managed file missing", withInclude, false, true, false},
|
||||
{"include present and managed file written", withInclude, true, false, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
install := newBrotliStateInstall(t, tc.conf)
|
||||
if tc.managedFile {
|
||||
configDir := nginxHTTPConfigDir(install)
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path.Join(configDir, managedFile), []byte("brotli on;\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
pending, unavailable := nginxBrotliManagedState(install)
|
||||
if pending != tc.wantPending || unavailable != tc.wantUnavailable {
|
||||
t.Fatalf("expected pending=%v unavailable=%v, got pending=%v unavailable=%v",
|
||||
tc.wantPending, tc.wantUnavailable, pending, unavailable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The state is computed on every visit to the settings page; only an explicit
|
||||
// save may change the configuration.
|
||||
func TestNginxBrotliManagedStateDoesNotWrite(t *testing.T) {
|
||||
install := newBrotliStateInstall(t, plainNginxConf)
|
||||
nginxBrotliManagedState(install)
|
||||
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(content) != plainNginxConf {
|
||||
t.Fatal("checking the state must not edit nginx.conf")
|
||||
}
|
||||
if _, err := os.Stat(nginxHTTPConfigDir(install)); !os.IsNotExist(err) {
|
||||
t.Fatal("checking the state must not create the http.d directory")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddModelQuickJump,
|
||||
migrations.MigrateModelDownloader,
|
||||
migrations.MigrateVMDatabase,
|
||||
migrations.AddFeaturedModelWeightProvider,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,21 @@ var MigrateModelDownloader = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var AddFeaturedModelWeightProvider = &gormigrate.Migration{
|
||||
ID: "20261103-add-featured-model-weight-provider",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.FeaturedModel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.FeaturedModel{}).
|
||||
Where("(weight_provider IS NULL OR weight_provider = '') AND instr(repo_id, '/') > 0").
|
||||
Update("weight_provider", gorm.Expr("substr(repo_id, 1, instr(repo_id, '/') - 1)")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Setting{}).Where("key = ?", "FeaturedModelsVersion").Update("value", "").Error
|
||||
},
|
||||
}
|
||||
|
||||
func migrateModelDownloader(db *gorm.DB, legacyPath string) error {
|
||||
var legacy *gorm.DB
|
||||
if _, err := os.Stat(legacyPath); err == nil {
|
||||
|
||||
@@ -27,6 +27,7 @@ func (m *ModelDownloaderRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
router.POST("/tasks/retry", baseApi.RetryModelDownloadTask)
|
||||
router.POST("/tasks/remove", baseApi.RemoveModelDownloadTask)
|
||||
router.POST("/local/search", baseApi.PageLocalDownloadedModels)
|
||||
router.GET("/local/options", baseApi.ListLocalModelOptions)
|
||||
router.POST("/local/delete", baseApi.DeleteLocalDownloadedModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,10 @@ func LoadMenus() string {
|
||||
item := []dto.ShowMenu{
|
||||
{ID: "1", Disabled: true, Title: "menu.home", IsShow: true, Label: "Home-Menu", Path: "/", Sort: 100},
|
||||
{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,
|
||||
{ID: "3", Disabled: false, Title: "menu.aiTools", IsShow: true, Label: "AI-Menu", Path: "/ai/model/files", Sort: 300,
|
||||
Children: []dto.ShowMenu{
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.model", IsShow: true, Label: "AIModel", Path: "/ai/model/account", Sort: 50},
|
||||
{ID: "48", Disabled: false, Title: "aiTools.model.ollamaManagement", IsShow: true, Label: "OllamaModels", Path: "/ai/model/local/ollama", Sort: 25},
|
||||
{ID: "41", Disabled: false, Title: "aiTools.model.vllmManagement", IsShow: true, Label: "AIModel", Path: "/ai/model/files", 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},
|
||||
@@ -204,6 +205,7 @@ func MenuSort() []dto.MenuLabelSort {
|
||||
{Label: "Home-Menu", Sort: 100},
|
||||
{Label: "App-Menu", Sort: 200},
|
||||
{Label: "AI-Menu", Sort: 300},
|
||||
{Label: "OllamaModels", Sort: 25},
|
||||
{Label: "AIModel", Sort: 50},
|
||||
{Label: "Agents", Sort: 100},
|
||||
{Label: "AIProxyManagement", Sort: 150},
|
||||
|
||||
@@ -74,5 +74,6 @@ func coreMigrations() []*gormigrate.Migration {
|
||||
migrations.AddPanelMCP,
|
||||
migrations.AddAPIKeyDeletedAt,
|
||||
migrations.MoveVirtualMachineMenuToTopLevel,
|
||||
migrations.UpdateAiModelManagementMenus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1406,6 +1406,46 @@ var UpdateAiMenuOrderV2 = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateAiModelManagementMenus = &gormigrate.Migration{
|
||||
ID: "20261103-update-ai-model-management-menus",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return helper.UpdateHideMenu(tx, func(menus []dto.ShowMenu) []dto.ShowMenu {
|
||||
for i := range menus {
|
||||
if menus[i].Label != "AI-Menu" {
|
||||
continue
|
||||
}
|
||||
menus[i].Path = "/ai/model/files"
|
||||
children := menus[i].Children
|
||||
showOllama := true
|
||||
hasOllama := false
|
||||
insertIndex := 0
|
||||
for j := range children {
|
||||
switch children[j].Label {
|
||||
case "AIModel":
|
||||
children[j].Title = "aiTools.model.vllmManagement"
|
||||
children[j].Path = "/ai/model/files"
|
||||
showOllama = children[j].IsShow
|
||||
insertIndex = j
|
||||
case "OllamaModels":
|
||||
hasOllama = true
|
||||
}
|
||||
}
|
||||
if !hasOllama {
|
||||
children = append(children, dto.ShowMenu{})
|
||||
copy(children[insertIndex+1:], children[insertIndex:])
|
||||
children[insertIndex] = dto.ShowMenu{
|
||||
ID: "48", Title: "aiTools.model.ollamaManagement", IsShow: showOllama,
|
||||
Label: "OllamaModels", Path: "/ai/model/local/ollama", Sort: 25,
|
||||
}
|
||||
}
|
||||
menus[i].Children = children
|
||||
break
|
||||
}
|
||||
return menus
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateFirewallMenuPath = &gormigrate.Migration{
|
||||
ID: "20260819-update-firewall-menu-path",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
@@ -3,17 +3,20 @@ import { ReqPage, ResPage } from '@/api/interface';
|
||||
export namespace ModelDownloader {
|
||||
export interface FeaturedModelSearch extends ReqPage {
|
||||
family: string;
|
||||
weightProvider: string;
|
||||
info: string;
|
||||
}
|
||||
|
||||
export interface FeaturedModelPage extends ResPage<FeaturedModel> {
|
||||
families: string[];
|
||||
weightProviders: string[];
|
||||
}
|
||||
|
||||
export interface FeaturedModel {
|
||||
description: Record<string, string> | null;
|
||||
family: string;
|
||||
repoID: string;
|
||||
weightProvider: string;
|
||||
sources: Source[];
|
||||
sourceModelIds?: Partial<Record<Source, string>> | null;
|
||||
architecture: 'MoE' | 'Dense';
|
||||
@@ -110,6 +113,12 @@ export namespace ModelDownloader {
|
||||
canRemoveRecord: boolean;
|
||||
}
|
||||
|
||||
export interface LocalModelOption {
|
||||
name: string;
|
||||
organization: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface LocalModelSearch extends ReqPage {
|
||||
downloadType?: DownloadType | '';
|
||||
info: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ export namespace Nginx {
|
||||
export interface NginxBrotliRes {
|
||||
params: NginxParam[];
|
||||
managedExternally: boolean;
|
||||
managedPending: boolean;
|
||||
managedUnavailable: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ export const pageLocalDownloadedModels = (req: ModelDownloader.LocalModelSearch)
|
||||
return http.post<ResPage<ModelDownloader.LocalModelItem>>('/ai/model/downloader/local/search', req);
|
||||
};
|
||||
|
||||
export const listLocalModelOptions = () => {
|
||||
return http.get<ModelDownloader.LocalModelOption[]>('/ai/model/downloader/local/options');
|
||||
};
|
||||
|
||||
export const deleteLocalDownloadedModel = (req: ModelDownloader.LocalModelDelete) => {
|
||||
return http.post('/ai/model/downloader/local/delete', req);
|
||||
};
|
||||
|
||||
@@ -1064,6 +1064,7 @@ const message = {
|
||||
backboneParams: 'MoE backbone parameters',
|
||||
activeParams: 'Active parameters',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Weight provider',
|
||||
weightFootprint: 'Weight size',
|
||||
undisclosed: 'Undisclosed',
|
||||
featuredModels: 'Featured models',
|
||||
@@ -1076,10 +1077,24 @@ const message = {
|
||||
instanceDeployment: 'Deploy by instance',
|
||||
engines: 'Local inference engines',
|
||||
modelManagement: 'Model management',
|
||||
ollamaManagement: 'Ollama management',
|
||||
vllmManagement: 'vLLM management',
|
||||
serviceDetails: 'Service details',
|
||||
deployments: 'Local deployments',
|
||||
files: 'Model hub',
|
||||
deployments: 'Model deployment',
|
||||
files: 'Model preparation',
|
||||
connectExternal: 'Connect external service',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Create Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = model + hardware + launch arguments. Import from {link} or create from a downloaded model.',
|
||||
recipeFormHelper: 'Paste a command from {link}, or fill in the fields manually.',
|
||||
recipeArguments: 'Launch arguments',
|
||||
recipeImportCommand: 'Import command',
|
||||
recipeImportPlaceholder: 'Paste Docker, pip/uv or vllm serve commands to fill in the Recipe automatically',
|
||||
recipeVariablesNotSupported:
|
||||
'Variable references are not supported in commands. Use explicit values; configure environment variables in the environment field.',
|
||||
recipeImportError:
|
||||
'Unable to convert. Include a Docker or vllm serve launch command with a model; paste pip/uv installation commands together with the launch command.',
|
||||
deploymentTemplates: 'vLLM startup command templates',
|
||||
engineProvider: 'Engine / Provider',
|
||||
endpoint: 'Endpoint',
|
||||
@@ -1106,8 +1121,6 @@ const message = {
|
||||
vllmDeployProfile: 'Deployment model',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
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 service',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1115,6 +1128,8 @@ const message = {
|
||||
containerAddress: 'Container address',
|
||||
customAddress: 'Custom address',
|
||||
downloader: 'Downloads',
|
||||
modelDirectoryPlaceholder: 'Search models or enter a full path',
|
||||
selectModelDirectory: 'Select directory',
|
||||
modelDir: 'Model directory',
|
||||
modelSource: 'Model source',
|
||||
repoID: 'Repository ID',
|
||||
@@ -4591,6 +4606,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Brotli compression rate, 0 to 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'Brotli is configured manually in nginx.conf; the panel shows the values in effect and will not overwrite them.',
|
||||
brotliManagedPendingHelper: 'Brotli settings are not in effect yet; they take effect after you save.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'The panel could not add the managed brotli configuration to nginx.conf automatically; the values below will not take effect.',
|
||||
brotliMinLengthHelper: 'Minimum response size to brotli-compress',
|
||||
|
||||
@@ -1066,6 +1066,7 @@ const message = {
|
||||
backboneParams: 'Parámetros base de MoE',
|
||||
activeParams: 'Parámetros activos',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Proveedor de pesos',
|
||||
weightFootprint: 'Tamaño de los pesos',
|
||||
undisclosed: 'No divulgado',
|
||||
featuredModels: 'Modelos destacados',
|
||||
@@ -1078,10 +1079,25 @@ const message = {
|
||||
instanceDeployment: 'Despliegue por instancia',
|
||||
engines: 'Motores de inferencia locales',
|
||||
modelManagement: 'Gestión de modelos',
|
||||
ollamaManagement: 'Gestión de Ollama',
|
||||
vllmManagement: 'Gestión de vLLM',
|
||||
serviceDetails: 'Detalles del servicio',
|
||||
deployments: 'Despliegues locales',
|
||||
files: 'Catálogo de modelos',
|
||||
deployments: 'Despliegue de modelos',
|
||||
files: 'Preparación de modelos',
|
||||
connectExternal: 'Conectar servicio externo',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Crear Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = modelo + hardware + argumentos de inicio. Importe desde {link} o cree una a partir de un modelo descargado.',
|
||||
recipeFormHelper: 'Pegue un comando de {link} o complete los campos manualmente.',
|
||||
recipeArguments: 'Argumentos de inicio',
|
||||
recipeImportCommand: 'Importar comando',
|
||||
recipeImportPlaceholder:
|
||||
'Pegue comandos Docker, pip/uv o vllm serve para completar la Recipe automáticamente',
|
||||
recipeVariablesNotSupported:
|
||||
'No se admiten referencias a variables en los comandos. Use valores concretos; configure las variables de entorno en su campo correspondiente.',
|
||||
recipeImportError:
|
||||
'No se puede convertir. Incluya un comando de inicio Docker o vllm serve con un modelo; pegue los comandos de instalación pip/uv junto con el comando de inicio.',
|
||||
deploymentTemplates: 'Plantillas de comandos de inicio de vLLM',
|
||||
engineProvider: 'Motor / Proveedor',
|
||||
endpoint: 'Punto de conexión',
|
||||
@@ -1109,8 +1125,6 @@ const message = {
|
||||
vllmDeployProfile: 'Modelo de despliegue',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
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 servicio de modelo',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1118,6 +1132,8 @@ const message = {
|
||||
containerAddress: 'Dirección del contenedor',
|
||||
customAddress: 'Dirección personalizada',
|
||||
downloader: 'Gestión de descargas',
|
||||
modelDirectoryPlaceholder: 'Buscar modelos o introducir una ruta completa',
|
||||
selectModelDirectory: 'Seleccionar directorio',
|
||||
modelDir: 'Directorio del modelo',
|
||||
modelSource: 'Fuente del modelo',
|
||||
repoID: 'ID del repositorio',
|
||||
@@ -4650,6 +4666,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Tasa de compresión brotli, de 0 a 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'Brotli está configurado manualmente en nginx.conf; el panel muestra los valores en vigor y no los sobrescribe.',
|
||||
brotliManagedPendingHelper: 'La configuración de brotli aún no está en vigor; se aplicará al guardar.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'El panel no pudo añadir automáticamente la configuración brotli gestionada a nginx.conf; los valores siguientes no tendrán efecto.',
|
||||
brotliMinLengthHelper: 'Tamaño mínimo de respuesta para comprimir con brotli',
|
||||
|
||||
@@ -1047,6 +1047,7 @@ const message = {
|
||||
backboneParams: 'پارامترهای پایه MoE',
|
||||
activeParams: 'پارامترهای فعال',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'ارائهدهنده وزنها',
|
||||
weightFootprint: 'حجم وزنها',
|
||||
undisclosed: 'اعلام نشده',
|
||||
featuredModels: 'مدلهای برگزیده',
|
||||
@@ -1059,10 +1060,25 @@ const message = {
|
||||
instanceDeployment: 'استقرار بر اساس نمونه',
|
||||
engines: 'موتورهای استنتاج محلی',
|
||||
modelManagement: 'مدیریت مدلها',
|
||||
ollamaManagement: 'مدیریت Ollama',
|
||||
vllmManagement: 'مدیریت vLLM',
|
||||
serviceDetails: 'جزئیات سرویس',
|
||||
deployments: 'استقرارهای محلی',
|
||||
files: 'مرکز مدلها',
|
||||
deployments: 'استقرار مدل',
|
||||
files: 'آمادهسازی مدل',
|
||||
connectExternal: 'اتصال به سرویس خارجی',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'ایجاد Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = مدل + سختافزار + آرگومانهای راهاندازی. از {link} وارد کنید یا از مدل دانلودشده بسازید.',
|
||||
recipeFormHelper: 'فرمان را از {link} جایگذاری کنید یا فیلدها را دستی پر کنید.',
|
||||
recipeArguments: 'آرگومانهای راهاندازی',
|
||||
recipeImportCommand: 'وارد کردن فرمان',
|
||||
recipeImportPlaceholder:
|
||||
'فرمانهای Docker، pip/uv یا vllm serve را برای تکمیل خودکار Recipe جایگذاری کنید',
|
||||
recipeVariablesNotSupported:
|
||||
'ارجاع به متغیرها در دستور پشتیبانی نمیشود. مقادیر مشخص وارد کنید؛ متغیرهای محیطی را در بخش مربوط به آنها تنظیم کنید.',
|
||||
recipeImportError:
|
||||
'تبدیل ممکن نیست. فرمان راهاندازی Docker یا vllm serve همراه با مدل را وارد کنید؛ فرمانهای نصب pip/uv را همراه با فرمان راهاندازی جایگذاری کنید.',
|
||||
deploymentTemplates: 'الگوهای فرمان راهاندازی vLLM',
|
||||
engineProvider: 'موتور / ارائهدهنده',
|
||||
endpoint: 'نقطه پایانی',
|
||||
@@ -1089,8 +1105,6 @@ const message = {
|
||||
vllmDeployProfile: 'مدل استقرار',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper:
|
||||
'فرمان راهاندازی باید از پورت {0} استفاده کند؛ در غیر این صورت سرویس قابل دسترسی نخواهد بود.',
|
||||
ascendVisibleDevices: 'دستگاههای قابل مشاهده Ascend (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'همگامسازی با سرویس مدل',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1098,6 +1112,8 @@ const message = {
|
||||
containerAddress: 'آدرس کانتینر',
|
||||
customAddress: 'آدرس سفارشی',
|
||||
downloader: 'مدیریت دانلودها',
|
||||
modelDirectoryPlaceholder: 'جستجوی مدل یا وارد کردن مسیر کامل',
|
||||
selectModelDirectory: 'انتخاب پوشه',
|
||||
modelDir: 'دایرکتوری مدل',
|
||||
modelSource: 'منبع مدل',
|
||||
repoID: 'شناسه مخزن',
|
||||
@@ -4553,6 +4569,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'نرخ فشردهسازی brotli، از 0 تا 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'brotli بهصورت دستی در nginx.conf پیکربندی شده است؛ پنل فقط مقادیر در حال اجرا را نشان میدهد و آنها را بازنویسی نمیکند.',
|
||||
brotliManagedPendingHelper: 'تنظیمات brotli هنوز اعمال نشده است؛ پس از ذخیره اعمال میشود.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'پنل نتوانست پیکربندی مدیریتشده brotli را بهصورت خودکار به nginx.conf اضافه کند؛ مقادیر زیر اعمال نخواهند شد.',
|
||||
brotliMinLengthHelper: 'حداقل اندازه پاسخ برای فشردهسازی با brotli',
|
||||
|
||||
@@ -1051,6 +1051,7 @@ const message = {
|
||||
backboneParams: 'MoE バックボーンパラメータ数',
|
||||
activeParams: 'アクティブパラメータ数',
|
||||
engram: 'Engram',
|
||||
weightProvider: '重みの提供元',
|
||||
weightFootprint: '重みのサイズ',
|
||||
undisclosed: '非公開',
|
||||
featuredModels: '注目のモデル',
|
||||
@@ -1063,10 +1064,24 @@ const message = {
|
||||
instanceDeployment: 'インスタンス単位でデプロイ',
|
||||
engines: 'ローカル推論エンジン',
|
||||
modelManagement: 'モデル管理',
|
||||
ollamaManagement: 'Ollama 管理',
|
||||
vllmManagement: 'vLLM 管理',
|
||||
serviceDetails: 'サービス詳細',
|
||||
deployments: 'ローカルデプロイ',
|
||||
files: 'モデル広場',
|
||||
deployments: 'モデルデプロイ',
|
||||
files: 'モデル準備',
|
||||
connectExternal: '外部サービスに接続',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Recipe を作成',
|
||||
recipeHelper:
|
||||
'Recipe = モデル + ハードウェア + 起動引数。{link} からインポートするか、ダウンロード済みモデルから作成できます。',
|
||||
recipeFormHelper: '{link} のコマンドを貼り付けるか、手動で入力してください。',
|
||||
recipeArguments: '起動引数',
|
||||
recipeImportCommand: 'コマンドをインポート',
|
||||
recipeImportPlaceholder: 'Docker、pip/uv、または vllm serve コマンドを貼り付けて Recipe に自動入力',
|
||||
recipeVariablesNotSupported:
|
||||
'コマンド内の変数参照はサポートされていません。具体的な値を入力し、環境変数は環境変数欄で設定してください。',
|
||||
recipeImportError:
|
||||
'変換できません。モデルを含む Docker または vllm serve の起動コマンドを入力してください。pip/uv のインストールコマンドは起動コマンドと一緒に貼り付けてください。',
|
||||
deploymentTemplates: 'vLLM 起動コマンドテンプレート',
|
||||
engineProvider: 'エンジン / プロバイダー',
|
||||
endpoint: 'エンドポイント',
|
||||
@@ -1094,8 +1109,6 @@ const message = {
|
||||
vllmDeployProfile: 'デプロイモデル',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper:
|
||||
'起動コマンドではポート {0} を使用する必要があります。使用しない場合、サービスにアクセスできません。',
|
||||
ascendVisibleDevices: 'Ascend 可視デバイス(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'モデルサービスに同期',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1103,6 +1116,8 @@ const message = {
|
||||
containerAddress: 'コンテナアドレス',
|
||||
customAddress: 'カスタムアドレス',
|
||||
downloader: 'ダウンロード管理',
|
||||
modelDirectoryPlaceholder: 'モデルを検索するかフルパスを入力',
|
||||
selectModelDirectory: 'ディレクトリを選択',
|
||||
modelDir: 'モデルディレクトリ',
|
||||
modelSource: 'モデルソース',
|
||||
repoID: 'リポジトリ ID',
|
||||
@@ -4574,6 +4589,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'brotli 圧縮率、0 から 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'brotli は nginx.conf で手動設定されています。パネルは現在の有効な値を表示するだけで、上書きしません。',
|
||||
brotliManagedPendingHelper: 'brotli 設定はまだ有効になっていません。保存すると有効になります。',
|
||||
brotliManagedUnavailableHelper:
|
||||
'パネルは管理対象の brotli 設定を nginx.conf に自動で追加できませんでした。以下の設定は有効になりません。',
|
||||
brotliMinLengthHelper: 'brotli で圧縮する最小レスポンスサイズ',
|
||||
|
||||
@@ -1041,6 +1041,7 @@ const message = {
|
||||
backboneParams: 'MoE 백본 매개변수 수',
|
||||
activeParams: '활성 매개변수 수',
|
||||
engram: 'Engram',
|
||||
weightProvider: '가중치 제공자',
|
||||
weightFootprint: '가중치 크기',
|
||||
undisclosed: '비공개',
|
||||
featuredModels: '추천 모델',
|
||||
@@ -1053,10 +1054,24 @@ const message = {
|
||||
instanceDeployment: '인스턴스별 배포',
|
||||
engines: '로컬 추론 엔진',
|
||||
modelManagement: '모델 관리',
|
||||
ollamaManagement: 'Ollama 관리',
|
||||
vllmManagement: 'vLLM 관리',
|
||||
serviceDetails: '서비스 상세 정보',
|
||||
deployments: '로컬 배포',
|
||||
files: '모델 허브',
|
||||
deployments: '모델 배포',
|
||||
files: '모델 준비',
|
||||
connectExternal: '외부 서비스 연결',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Recipe 만들기',
|
||||
recipeHelper:
|
||||
'Recipe = 모델 + 하드웨어 + 시작 인수. {link}에서 가져오거나 다운로드한 모델로 만들 수 있습니다.',
|
||||
recipeFormHelper: '{link}의 명령을 붙여넣거나 직접 입력하세요.',
|
||||
recipeArguments: '시작 인수',
|
||||
recipeImportCommand: '명령 가져오기',
|
||||
recipeImportPlaceholder: 'Docker, pip/uv 또는 vllm serve 명령을 붙여넣어 Recipe 자동 입력',
|
||||
recipeVariablesNotSupported:
|
||||
'명령에서 변수 참조는 지원되지 않습니다. 구체적인 값을 입력하고 환경 변수는 환경 변수 필드에서 설정하세요.',
|
||||
recipeImportError:
|
||||
'변환할 수 없습니다. 모델이 포함된 Docker 또는 vllm serve 시작 명령을 입력하세요. pip/uv 설치 명령은 시작 명령과 함께 붙여넣으세요.',
|
||||
deploymentTemplates: 'vLLM 시작 명령 템플릿',
|
||||
engineProvider: '엔진 / 공급자',
|
||||
endpoint: '엔드포인트',
|
||||
@@ -1083,7 +1098,6 @@ const message = {
|
||||
vllmDeployProfile: '배포 모델',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '시작 명령은 {0} 포트를 사용해야 하며, 그렇지 않으면 서비스에 접근할 수 없습니다.',
|
||||
ascendVisibleDevices: 'Ascend 표시 장치 (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '모델 서비스에 동기화',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1091,6 +1105,8 @@ const message = {
|
||||
containerAddress: '컨테이너 주소',
|
||||
customAddress: '사용자 지정 주소',
|
||||
downloader: '다운로드 관리',
|
||||
modelDirectoryPlaceholder: '모델 검색 또는 전체 경로 입력',
|
||||
selectModelDirectory: '디렉터리 선택',
|
||||
modelDir: '모델 디렉토리',
|
||||
modelSource: '모델 소스',
|
||||
repoID: '저장소 ID',
|
||||
@@ -4492,6 +4508,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'brotli 압축률, 0에서 11까지',
|
||||
brotliManagedExternallyHelper:
|
||||
'brotli가 nginx.conf에 수동으로 구성되어 있습니다. 패널은 적용 중인 값만 표시하며 덮어쓰지 않습니다.',
|
||||
brotliManagedPendingHelper: 'brotli 설정이 아직 적용되지 않았습니다. 저장하면 적용됩니다.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'패널이 관리되는 brotli 구성을 nginx.conf에 자동으로 추가할 수 없습니다. 아래 설정은 적용되지 않습니다.',
|
||||
brotliMinLengthHelper: 'brotli로 압축할 최소 응답 크기',
|
||||
|
||||
@@ -1038,6 +1038,7 @@ const message = {
|
||||
backboneParams: 'ພາຣາມິເຕີຫຼັກຂອງ MoE',
|
||||
activeParams: 'ພາຣາມິເຕີທີ່ໃຊ້ງານ',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'ຜູ້ໃຫ້ບໍລິການນ້ຳໜັກໂມເດວ',
|
||||
weightFootprint: 'ຂະໜາດນ້ຳໜັກ',
|
||||
undisclosed: 'ບໍ່ເປີດເຜີຍ',
|
||||
featuredModels: 'ໂມເດວແນະນຳ',
|
||||
@@ -1050,10 +1051,24 @@ const message = {
|
||||
instanceDeployment: 'ນຳໃຊ້ຕາມອິນສະແຕນ',
|
||||
engines: 'ເຄື່ອງຈັກອະນຸມານພາຍໃນ',
|
||||
modelManagement: 'ຈັດການໂມເດວ',
|
||||
ollamaManagement: 'ການຈັດການ Ollama',
|
||||
vllmManagement: 'ການຈັດການ vLLM',
|
||||
serviceDetails: 'ລາຍລະອຽດບໍລິການ',
|
||||
deployments: 'ການນຳໃຊ້ພາຍໃນເຄື່ອງ',
|
||||
files: 'ສູນລວມໂມເດວ',
|
||||
deployments: 'ການນຳໃຊ້ໂມເດວ',
|
||||
files: 'ການກະກຽມໂມເດວ',
|
||||
connectExternal: 'ເຊື່ອມຕໍ່ບໍລິການພາຍນອກ',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'ສ້າງ Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = ໂມເດວ + ຮາດແວ + ພາລາມິເຕີເລີ່ມຕົ້ນ. ນຳເຂົ້າຈາກ {link} ຫຼືສ້າງຈາກໂມເດວທີ່ດາວໂຫຼດແລ້ວ.',
|
||||
recipeFormHelper: 'ວາງຄຳສັ່ງຈາກ {link} ຫຼືປ້ອນຂໍ້ມູນເອງ.',
|
||||
recipeArguments: 'ພາລາມິເຕີເລີ່ມຕົ້ນ',
|
||||
recipeImportCommand: 'ນຳເຂົ້າຄຳສັ່ງ',
|
||||
recipeImportPlaceholder: 'ວາງຄຳສັ່ງ Docker, pip/uv ຫຼື vllm serve ເພື່ອຕື່ມ Recipe ອັດຕະໂນມັດ',
|
||||
recipeVariablesNotSupported:
|
||||
'ຄຳສັ່ງບໍ່ຮອງຮັບການອ້າງອີງຕົວແປ. ກະລຸນາປ້ອນຄ່າໂດຍກົງ ແລະຕັ້ງຄ່າຕົວແປສະພາບແວດລ້ອມໃນຊ່ອງທີ່ກ່ຽວຂ້ອງ.',
|
||||
recipeImportError:
|
||||
'ບໍ່ສາມາດແປງໄດ້. ກະລຸນາໃສ່ຄຳສັ່ງເລີ່ມຕົ້ນ Docker ຫຼື vllm serve ທີ່ມີໂມເດວ; ວາງຄຳສັ່ງຕິດຕັ້ງ pip/uv ພ້ອມກັບຄຳສັ່ງເລີ່ມຕົ້ນ.',
|
||||
deploymentTemplates: 'ແມ່ແບບຄຳສັ່ງເລີ່ມຕົ້ນ vLLM',
|
||||
engineProvider: 'ເຄື່ອງຈັກ / ຜູ້ໃຫ້ບໍລິການ',
|
||||
endpoint: 'ຈຸດເຊື່ອມຕໍ່',
|
||||
@@ -1080,7 +1095,6 @@ const message = {
|
||||
vllmDeployProfile: 'ໂມເດວສຳລັບຕິດຕັ້ງ',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: 'ຄຳສັ່ງເລີ່ມຕົ້ນຕ້ອງໃຊ້ພອດ {0}; ບໍ່ດັ່ງນັ້ນຈະບໍ່ສາມາດເຂົ້າເຖິງບໍລິການໄດ້.',
|
||||
ascendVisibleDevices: 'ອຸປະກອນ Ascend ທີ່ເຫັນໄດ້ (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'ຊິ້ງຄ໌ໄປຍັງບໍລິການໂມເດວ',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1088,6 +1102,8 @@ const message = {
|
||||
containerAddress: 'ທີ່ຢູ່ຄອນເທນເນີ',
|
||||
customAddress: 'ທີ່ຢູ່ກຳນົດເອງ',
|
||||
downloader: 'ຈັດການການດາວໂຫຼດ',
|
||||
modelDirectoryPlaceholder: 'ຄົ້ນຫາໂມເດວ ຫຼື ປ້ອນເສັ້ນທາງເຕັມ',
|
||||
selectModelDirectory: 'ເລືອກໄດເຣັກທໍຣີ',
|
||||
modelDir: 'ໄດເຣັກທໍຣີໂມເດວ',
|
||||
modelSource: 'ແຫຼ່ງທີ່ມາໂມເດວ',
|
||||
repoID: 'ID ຄັງເກັບ',
|
||||
@@ -4465,6 +4481,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'ອັດຕາການບີບອັດ brotli, 0 ຫາ 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'brotli ຖືກຕັ້ງຄ່າດ້ວຍມືໃນ nginx.conf; ແຜງຄວບຄຸມຈະສະແດງຄ່າທີ່ກຳລັງໃຊ້ຢູ່ ແລະ ຈະບໍ່ຂຽບທັບ.',
|
||||
brotliManagedPendingHelper: 'ການຕັ້ງຄ່າ brotli ຍັງບໍ່ມີຜົນ; ຈະມີຜົນຫຼັງຈາກບັນທຶກ.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'ແຜງຄວບຄຸມບໍ່ສາມາດເພີ່ມການຕັ້ງຄ່າ brotli ທີ່ຈັດການໄປຍັງ nginx.conf ໂດຍອັດຕະໂນມັດໄດ້; ຄ່າຂ້າງລຸ່ມນີ້ຈະບໍ່ມີຜົນ.',
|
||||
brotliMinLengthHelper: 'ຂະໜາດຕອບສະໜອງຂັ້ນຕ່ຳທີ່ຈະບີບອັດດ້ວຍ brotli',
|
||||
|
||||
@@ -1074,6 +1074,7 @@ const message = {
|
||||
backboneParams: 'Parameter teras MoE',
|
||||
activeParams: 'Parameter aktif',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Penyedia pemberat',
|
||||
weightFootprint: 'Saiz pemberat',
|
||||
undisclosed: 'Tidak didedahkan',
|
||||
featuredModels: 'Model pilihan',
|
||||
@@ -1086,10 +1087,25 @@ const message = {
|
||||
instanceDeployment: 'Pengerahan mengikut instans',
|
||||
engines: 'Enjin inferens setempat',
|
||||
modelManagement: 'Pengurusan model',
|
||||
ollamaManagement: 'Pengurusan Ollama',
|
||||
vllmManagement: 'Pengurusan vLLM',
|
||||
serviceDetails: 'Butiran perkhidmatan',
|
||||
deployments: 'Pengerahan setempat',
|
||||
files: 'Hab model',
|
||||
deployments: 'Pengerahan model',
|
||||
files: 'Penyediaan model',
|
||||
connectExternal: 'Sambungkan perkhidmatan luaran',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Cipta Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = model + perkakasan + argumen pelancaran. Import daripada {link} atau cipta daripada model yang dimuat turun.',
|
||||
recipeFormHelper: 'Tampal arahan daripada {link} atau isi medan secara manual.',
|
||||
recipeArguments: 'Argumen pelancaran',
|
||||
recipeImportCommand: 'Import arahan',
|
||||
recipeImportPlaceholder:
|
||||
'Tampal arahan Docker, pip/uv atau vllm serve untuk mengisi Recipe secara automatik',
|
||||
recipeVariablesNotSupported:
|
||||
'Rujukan pemboleh ubah tidak disokong dalam arahan. Masukkan nilai khusus; tetapkan pemboleh ubah persekitaran dalam medan yang berkenaan.',
|
||||
recipeImportError:
|
||||
'Tidak dapat menukar. Sertakan arahan pelancaran Docker atau vllm serve dengan model; tampal arahan pemasangan pip/uv bersama arahan pelancaran.',
|
||||
deploymentTemplates: 'Templat arahan permulaan vLLM',
|
||||
engineProvider: 'Enjin / Pembekal',
|
||||
endpoint: 'Titik akhir',
|
||||
@@ -1116,8 +1132,6 @@ const message = {
|
||||
vllmDeployProfile: 'Model penggunaan',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
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 perkhidmatan model',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1125,6 +1139,8 @@ const message = {
|
||||
containerAddress: 'Alamat kontena',
|
||||
customAddress: 'Alamat tersuai',
|
||||
downloader: 'Pengurusan muat turun',
|
||||
modelDirectoryPlaceholder: 'Cari model atau masukkan laluan penuh',
|
||||
selectModelDirectory: 'Pilih direktori',
|
||||
modelDir: 'Direktori Model',
|
||||
modelSource: 'Sumber model',
|
||||
repoID: 'ID Repositori',
|
||||
@@ -4659,6 +4675,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Kadar pemampatan brotli, 0 hingga 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'Brotli dikonfigurasikan secara manual dalam nginx.conf; panel memaparkan nilai yang berkuat kuasa dan tidak akan menimpanya.',
|
||||
brotliManagedPendingHelper: 'Tetapan brotli belum berkuat kuasa; ia akan berkuat kuasa selepas disimpan.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'Panel tidak dapat menambahkan konfigurasi brotli terurus ke nginx.conf secara automatik; nilai di bawah tidak akan berkuat kuasa.',
|
||||
brotliMinLengthHelper: 'Saiz respons minimum untuk dimampatkan dengan brotli',
|
||||
|
||||
@@ -1070,6 +1070,7 @@ const message = {
|
||||
backboneParams: 'Parâmetros base do MoE',
|
||||
activeParams: 'Parâmetros ativos',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Provedor de pesos',
|
||||
weightFootprint: 'Tamanho dos pesos',
|
||||
undisclosed: 'Não divulgado',
|
||||
featuredModels: 'Modelos em destaque',
|
||||
@@ -1082,10 +1083,25 @@ const message = {
|
||||
instanceDeployment: 'Implantação por instância',
|
||||
engines: 'Mecanismos de inferência locais',
|
||||
modelManagement: 'Gerenciamento de modelos',
|
||||
ollamaManagement: 'Gerenciamento do Ollama',
|
||||
vllmManagement: 'Gerenciamento do vLLM',
|
||||
serviceDetails: 'Detalhes do serviço',
|
||||
deployments: 'Implantações locais',
|
||||
files: 'Central de modelos',
|
||||
deployments: 'Implantação de modelos',
|
||||
files: 'Preparação de modelos',
|
||||
connectExternal: 'Conectar serviço externo',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Criar Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = modelo + hardware + argumentos de inicialização. Importe de {link} ou crie a partir de um modelo baixado.',
|
||||
recipeFormHelper: 'Cole um comando de {link} ou preencha os campos manualmente.',
|
||||
recipeArguments: 'Argumentos de inicialização',
|
||||
recipeImportCommand: 'Importar comando',
|
||||
recipeImportPlaceholder:
|
||||
'Cole comandos Docker, pip/uv ou vllm serve para preencher a Recipe automaticamente',
|
||||
recipeVariablesNotSupported:
|
||||
'Referências a variáveis não são aceitas nos comandos. Use valores explícitos; configure as variáveis de ambiente no campo correspondente.',
|
||||
recipeImportError:
|
||||
'Não foi possível converter. Inclua um comando de inicialização Docker ou vllm serve com um modelo; cole os comandos de instalação pip/uv junto com o comando de inicialização.',
|
||||
deploymentTemplates: 'Modelos de comandos de inicialização do vLLM',
|
||||
engineProvider: 'Mecanismo / Provedor',
|
||||
endpoint: 'Endpoint',
|
||||
@@ -1112,8 +1128,6 @@ const message = {
|
||||
vllmDeployProfile: 'Modelo de implantação',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
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 serviço de modelo',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1121,6 +1135,8 @@ const message = {
|
||||
containerAddress: 'Endereço do contêiner',
|
||||
customAddress: 'Endereço personalizado',
|
||||
downloader: 'Gerenciamento de downloads',
|
||||
modelDirectoryPlaceholder: 'Buscar modelos ou inserir um caminho completo',
|
||||
selectModelDirectory: 'Selecionar diretório',
|
||||
modelDir: 'Diretório do modelo',
|
||||
modelSource: 'Fonte do modelo',
|
||||
repoID: 'ID do repositório',
|
||||
@@ -4678,6 +4694,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Taxa de compressão brotli, de 0 a 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'O brotli está configurado manualmente no nginx.conf; o painel mostra os valores em vigor e não os sobrescreve.',
|
||||
brotliManagedPendingHelper: 'As configurações brotli ainda não estão em vigor; serão aplicadas ao salvar.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'O painel não conseguiu adicionar automaticamente a configuração brotli gerenciada ao nginx.conf; os valores abaixo não terão efeito.',
|
||||
brotliMinLengthHelper: 'Tamanho mínimo da resposta para compressão brotli',
|
||||
|
||||
@@ -1063,6 +1063,7 @@ const message = {
|
||||
backboneParams: 'Параметры основы MoE',
|
||||
activeParams: 'Активные параметры',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Поставщик весов',
|
||||
weightFootprint: 'Размер весов',
|
||||
undisclosed: 'Не раскрыто',
|
||||
featuredModels: 'Рекомендуемые модели',
|
||||
@@ -1075,10 +1076,25 @@ const message = {
|
||||
instanceDeployment: 'Развертывание по экземплярам',
|
||||
engines: 'Локальные движки инференса',
|
||||
modelManagement: 'Управление моделями',
|
||||
ollamaManagement: 'Управление Ollama',
|
||||
vllmManagement: 'Управление vLLM',
|
||||
serviceDetails: 'Сведения о сервисе',
|
||||
deployments: 'Локальные развертывания',
|
||||
files: 'Каталог моделей',
|
||||
deployments: 'Развертывание моделей',
|
||||
files: 'Подготовка моделей',
|
||||
connectExternal: 'Подключить внешний сервис',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Создать Recipe',
|
||||
recipeHelper:
|
||||
'Recipe = модель + оборудование + параметры запуска. Импортируйте с {link} или создайте из загруженной модели.',
|
||||
recipeFormHelper: 'Вставьте команду с {link} или заполните поля вручную.',
|
||||
recipeArguments: 'Параметры запуска',
|
||||
recipeImportCommand: 'Импорт команды',
|
||||
recipeImportPlaceholder:
|
||||
'Вставьте команды Docker, pip/uv или vllm serve для автоматического заполнения Recipe',
|
||||
recipeVariablesNotSupported:
|
||||
'Ссылки на переменные в командах не поддерживаются. Укажите конкретные значения; переменные окружения настройте в соответствующем поле.',
|
||||
recipeImportError:
|
||||
'Не удалось преобразовать. Добавьте команду запуска Docker или vllm serve с моделью; вставляйте команды установки pip/uv вместе с командой запуска.',
|
||||
deploymentTemplates: 'Шаблоны команд запуска vLLM',
|
||||
engineProvider: 'Движок / Поставщик',
|
||||
endpoint: 'Конечная точка',
|
||||
@@ -1106,7 +1122,6 @@ const message = {
|
||||
vllmDeployProfile: 'Модель развертывания',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: 'Команда запуска должна использовать порт {0}, иначе сервис будет недоступен.',
|
||||
ascendVisibleDevices: 'Видимые устройства Ascend (ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: 'Синхронизировать с сервисом модели',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1114,6 +1129,8 @@ const message = {
|
||||
containerAddress: 'Адрес контейнера',
|
||||
customAddress: 'Пользовательский адрес',
|
||||
downloader: 'Управление загрузками',
|
||||
modelDirectoryPlaceholder: 'Найти модель или ввести полный путь',
|
||||
selectModelDirectory: 'Выбрать каталог',
|
||||
modelDir: 'Каталог модели',
|
||||
modelSource: 'Источник модели',
|
||||
repoID: 'ID репозитория',
|
||||
@@ -4649,6 +4666,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Степень сжатия brotli, от 0 до 11',
|
||||
brotliManagedExternallyHelper:
|
||||
'Brotli настроен вручную в nginx.conf; панель показывает действующие значения и не перезаписывает их.',
|
||||
brotliManagedPendingHelper: 'Настройки brotli ещё не действуют; они вступят в силу после сохранения.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'Панель не смогла автоматически добавить управляемую конфигурацию brotli в nginx.conf; значения ниже не вступят в силу.',
|
||||
brotliMinLengthHelper: 'Минимальный размер ответа для сжатия brotli',
|
||||
|
||||
@@ -1072,6 +1072,7 @@ const message = {
|
||||
backboneParams: 'MoE omurga parametreleri',
|
||||
activeParams: 'Etkin parametreler',
|
||||
engram: 'Engram',
|
||||
weightProvider: 'Ağırlık sağlayıcısı',
|
||||
weightFootprint: 'Ağırlık boyutu',
|
||||
undisclosed: 'Açıklanmadı',
|
||||
featuredModels: 'Öne çıkan modeller',
|
||||
@@ -1084,10 +1085,25 @@ const message = {
|
||||
instanceDeployment: 'Örnek bazında dağıtım',
|
||||
engines: 'Yerel çıkarım motorları',
|
||||
modelManagement: 'Model yönetimi',
|
||||
ollamaManagement: 'Ollama yönetimi',
|
||||
vllmManagement: 'vLLM yönetimi',
|
||||
serviceDetails: 'Hizmet ayrıntıları',
|
||||
deployments: 'Yerel dağıtımlar',
|
||||
files: 'Model merkezi',
|
||||
deployments: 'Model dağıtımı',
|
||||
files: 'Model hazırlığı',
|
||||
connectExternal: 'Harici hizmete bağlan',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: 'Recipe oluştur',
|
||||
recipeHelper:
|
||||
'Recipe = model + donanım + başlatma parametreleri. {link} üzerinden içe aktarın veya indirilen bir modelden oluşturun.',
|
||||
recipeFormHelper: '{link} üzerinden bir komut yapıştırın veya alanları elle doldurun.',
|
||||
recipeArguments: 'Başlatma parametreleri',
|
||||
recipeImportCommand: 'Komut içe aktar',
|
||||
recipeImportPlaceholder:
|
||||
'Recipe alanlarını otomatik doldurmak için Docker, pip/uv veya vllm serve komutlarını yapıştırın',
|
||||
recipeVariablesNotSupported:
|
||||
'Komutlarda değişken başvuruları desteklenmez. Açık değerler girin; ortam değişkenlerini ilgili alanda yapılandırın.',
|
||||
recipeImportError:
|
||||
'Dönüştürülemedi. Model içeren bir Docker veya vllm serve başlatma komutu ekleyin; pip/uv kurulum komutlarını başlatma komutuyla birlikte yapıştırın.',
|
||||
deploymentTemplates: 'vLLM başlatma komutu şablonları',
|
||||
engineProvider: 'Motor / Sağlayıcı',
|
||||
endpoint: 'Uç nokta',
|
||||
@@ -1114,8 +1130,6 @@ const message = {
|
||||
vllmDeployProfile: 'Dağıtım modeli',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
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 hizmetine senkronize et',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1123,6 +1137,8 @@ const message = {
|
||||
containerAddress: 'Konteyner adresi',
|
||||
customAddress: 'Özel adres',
|
||||
downloader: 'İndirme yönetimi',
|
||||
modelDirectoryPlaceholder: 'Model arayın veya tam yol girin',
|
||||
selectModelDirectory: 'Dizin seç',
|
||||
modelDir: 'Model dizini',
|
||||
modelSource: 'Model kaynağı',
|
||||
repoID: 'Depo ID',
|
||||
@@ -4664,6 +4680,7 @@ const message = {
|
||||
brotliCompLevelHelper: 'Brotli sıkıştırma oranı, 0 ile 11 arası',
|
||||
brotliManagedExternallyHelper:
|
||||
'Brotli, nginx.conf dosyasında elle yapılandırıldı; panel yalnızca geçerli değerleri gösterir ve üzerine yazmaz.',
|
||||
brotliManagedPendingHelper: 'Brotli ayarları henüz etkin değil; kaydettikten sonra etkinleşir.',
|
||||
brotliManagedUnavailableHelper:
|
||||
'Panel, yönetilen brotli yapılandırmasını nginx.conf dosyasına otomatik olarak ekleyemedi; aşağıdaki değerler etkin olmayacak.',
|
||||
brotliMinLengthHelper: 'brotli ile sıkıştırılacak minimum yanıt boyutu',
|
||||
|
||||
@@ -1002,6 +1002,7 @@ const message = {
|
||||
backboneParams: 'MoE 骨幹參數',
|
||||
activeParams: '啟用參數',
|
||||
engram: 'Engram',
|
||||
weightProvider: '權重提供商',
|
||||
weightFootprint: '權重體積',
|
||||
undisclosed: '未公開',
|
||||
featuredModels: '精選模型',
|
||||
@@ -1014,10 +1015,22 @@ const message = {
|
||||
instanceDeployment: '按執行個體部署',
|
||||
engines: '本機推理引擎',
|
||||
modelManagement: '模型管理',
|
||||
ollamaManagement: 'Ollama 管理',
|
||||
vllmManagement: 'vLLM 管理',
|
||||
serviceDetails: '服務詳情',
|
||||
deployments: '本機部署',
|
||||
files: '模型廣場',
|
||||
deployments: '模型部署',
|
||||
files: '模型準備',
|
||||
connectExternal: '接入外部服務',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: '建立 Recipe',
|
||||
recipeHelper: 'Recipe = 模型 + 硬體 + 啟動參數。支援從 {link} 匯入或從已下載模型建立。',
|
||||
recipeFormHelper: '從 {link} 複製命令匯入,或手動填寫。',
|
||||
recipeArguments: '啟動參數',
|
||||
recipeImportCommand: '匯入命令',
|
||||
recipeImportPlaceholder: '貼上 Docker、pip/uv 或 vllm serve 命令,自動填入 Recipe',
|
||||
recipeVariablesNotSupported: '命令中不支援變數引用,請填寫具體值;環境變數請在環境變數欄設定。',
|
||||
recipeImportError:
|
||||
'無法轉換,請提供包含模型的 Docker 或 vllm serve 啟動命令;pip/uv 安裝命令需要與啟動命令一起貼上。',
|
||||
deploymentTemplates: 'vLLM 啟動命令範本',
|
||||
engineProvider: '引擎 / 供應商',
|
||||
endpoint: '端點',
|
||||
@@ -1044,7 +1057,6 @@ const message = {
|
||||
vllmDeployProfile: '部署模型',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '啟動命令必須使用 {0} 連接埠,否則服務將無法存取。',
|
||||
ascendVisibleDevices: 'Ascend 可見裝置(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '同步到模型服務',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1052,6 +1064,8 @@ const message = {
|
||||
containerAddress: '容器地址',
|
||||
customAddress: '自訂地址',
|
||||
downloader: '下載管理',
|
||||
modelDirectoryPlaceholder: '搜尋模型或輸入完整路徑',
|
||||
selectModelDirectory: '選擇目錄',
|
||||
modelDir: '模型目錄',
|
||||
modelSource: '模型來源',
|
||||
repoID: '倉庫 ID',
|
||||
@@ -4292,6 +4306,7 @@ const message = {
|
||||
brotliHelper: '開啟 brotli 壓縮,通常比 gzip 體積更小',
|
||||
brotliCompLevelHelper: 'brotli 壓縮率,取值 0 到 11',
|
||||
brotliManagedExternallyHelper: 'brotli 已在 nginx.conf 中手動設定,面板僅顯示目前生效值,不會覆寫。',
|
||||
brotliManagedPendingHelper: 'brotli 設定尚未生效,點擊儲存後生效。',
|
||||
brotliManagedUnavailableHelper: '面板無法自動向 nginx.conf 寫入託管的 brotli 設定,以下設定不會生效。',
|
||||
brotliMinLengthHelper: 'brotli 壓縮的最小回應大小',
|
||||
brotliSaveFailed: 'brotli 設定儲存失敗,上方的 gzip 設定已套用',
|
||||
|
||||
@@ -1015,6 +1015,7 @@ const message = {
|
||||
backboneParams: 'MoE 骨干参数',
|
||||
activeParams: '激活参数',
|
||||
engram: 'Engram',
|
||||
weightProvider: '权重提供商',
|
||||
weightFootprint: '权重体积',
|
||||
undisclosed: '未公开',
|
||||
featuredModels: '精选模型',
|
||||
@@ -1027,10 +1028,22 @@ const message = {
|
||||
instanceDeployment: '按实例部署',
|
||||
engines: '本地推理引擎',
|
||||
modelManagement: '模型管理',
|
||||
ollamaManagement: 'Ollama 管理',
|
||||
vllmManagement: 'vLLM 管理',
|
||||
serviceDetails: '服务详情',
|
||||
deployments: '本地部署',
|
||||
files: '模型广场',
|
||||
deployments: '模型部署',
|
||||
files: '模型准备',
|
||||
connectExternal: '接入外部服务',
|
||||
recipes: 'vLLM Recipes',
|
||||
createRecipe: '创建 Recipe',
|
||||
recipeHelper: 'Recipe = 模型 + 硬件 + 启动参数。支持从 {link} 导入或从已下载模型创建。',
|
||||
recipeFormHelper: '从 {link} 复制命令导入,或手动填写。',
|
||||
recipeArguments: '启动参数',
|
||||
recipeImportCommand: '导入命令',
|
||||
recipeImportPlaceholder: '粘贴 Docker、pip/uv 或 vllm serve 命令,自动填充 Recipe',
|
||||
recipeVariablesNotSupported: '命令中不支持变量引用,请填写具体值;环境变量请在环境变量栏配置。',
|
||||
recipeImportError:
|
||||
'无法转换,请提供包含模型的 Docker 或 vllm serve 启动命令;pip/uv 安装命令需要与启动命令一起粘贴。',
|
||||
deploymentTemplates: 'vLLM 启动命令模板',
|
||||
engineProvider: '引擎 / 供应商',
|
||||
endpoint: '端点',
|
||||
@@ -1057,7 +1070,6 @@ const message = {
|
||||
vllmDeployProfile: '部署模型',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
|
||||
vllmCommandPortHelper: '启动命令必须使用 {0} 端口,否则服务将无法访问。',
|
||||
ascendVisibleDevices: 'Ascend 可见设备(ASCEND_RT_VISIBLE_DEVICES)',
|
||||
syncModelAccount: '同步到模型服务',
|
||||
modelAccountAddressHelper:
|
||||
@@ -1065,6 +1077,8 @@ const message = {
|
||||
containerAddress: '容器地址',
|
||||
customAddress: '自定义地址',
|
||||
downloader: '下载管理',
|
||||
modelDirectoryPlaceholder: '搜索模型或输入完整路径',
|
||||
selectModelDirectory: '选择目录',
|
||||
modelDir: '模型目录',
|
||||
modelSource: '模型来源',
|
||||
repoID: '仓库 ID',
|
||||
@@ -4332,6 +4346,7 @@ const message = {
|
||||
brotliHelper: '开启 brotli 压缩,通常比 gzip 体积更小',
|
||||
brotliCompLevelHelper: 'brotli 压缩率,取值 0 到 11',
|
||||
brotliManagedExternallyHelper: 'brotli 已在 nginx.conf 中手动配置,面板仅显示当前生效值,不会覆盖。',
|
||||
brotliManagedPendingHelper: 'brotli 配置尚未生效,点击保存后生效。',
|
||||
brotliManagedUnavailableHelper: '面板无法自动向 nginx.conf 写入托管的 brotli 配置,以下设置不会生效。',
|
||||
brotliMinLengthHelper: 'brotli 压缩的最小响应大小',
|
||||
brotliSaveFailed: 'brotli 设置保存失败,上方的 gzip 设置已应用',
|
||||
|
||||
@@ -12,13 +12,23 @@ const aiRouter = {
|
||||
title: 'menu.aiTools',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/ai/model/local/ollama',
|
||||
name: 'OllamaModels',
|
||||
component: () => import('@/views/ai/model/ollama/index.vue'),
|
||||
meta: {
|
||||
icon: 'p-moxing-menu',
|
||||
title: 'aiTools.model.ollamaManagement',
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/model',
|
||||
name: 'AIModel',
|
||||
redirect: '/ai/model/files',
|
||||
meta: {
|
||||
icon: 'p-moxing-menu',
|
||||
title: 'aiTools.model.model',
|
||||
title: 'aiTools.model.vllmManagement',
|
||||
activeMenu: '/ai/model',
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
@@ -56,18 +66,6 @@ const aiRouter = {
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/model/local/ollama',
|
||||
hidden: true,
|
||||
name: 'OllamaModels',
|
||||
component: () => import('@/views/ai/model/ollama/index.vue'),
|
||||
meta: {
|
||||
title: 'aiTools.model.engines',
|
||||
detail: 'aiTools.model.modelManagement',
|
||||
activeMenu: '/ai/model',
|
||||
permission: 'ai_model_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/agents/agent',
|
||||
name: 'Agents',
|
||||
|
||||
@@ -38,9 +38,6 @@ 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({
|
||||
|
||||
@@ -1,43 +1,9 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="visible"
|
||||
:header="$t(enginesOnly ? 'aiTools.model.addEngine' : 'aiTools.model.addService')"
|
||||
:header="$t(enginesOnly ? 'commons.button.add' : 'aiTools.model.addService')"
|
||||
:size="isMobile ? 'full' : '50%'"
|
||||
>
|
||||
<div v-if="selected !== 'account' && !vllmOnly" class="engine-options">
|
||||
<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>
|
||||
</div>
|
||||
<el-divider v-if="selected !== 'account' && !vllmOnly" />
|
||||
<DockerStatus
|
||||
v-if="visible && selected !== 'account'"
|
||||
v-model:is-active="dockerActive"
|
||||
@@ -61,76 +27,12 @@
|
||||
@close="visible = false"
|
||||
@task="openTask"
|
||||
/>
|
||||
<OllamaForm
|
||||
v-if="activeForm === 'ollama'"
|
||||
ref="ollamaRef"
|
||||
inline
|
||||
@search="emit('search')"
|
||||
@close="visible = false"
|
||||
@task="openTask"
|
||||
/>
|
||||
<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>
|
||||
<section v-if="selected === 'ollama' && ollama?.isExist" class="installed-engine">
|
||||
<div
|
||||
class="installed-engine-overview"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
:aria-label="$t('aiTools.model.modelManagement')"
|
||||
@click="openOllama"
|
||||
@keydown.enter.self.prevent="openOllama"
|
||||
@keydown.space.self.prevent="openOllama"
|
||||
>
|
||||
<div class="installed-engine-heading">
|
||||
<ProviderLogo provider="ollama" :size="40" />
|
||||
<div class="installed-engine-identity">
|
||||
<h3>{{ ollama.name }}</h3>
|
||||
<span>Ollama · {{ ollama.version }}</span>
|
||||
</div>
|
||||
<Status :status="ollama.status" />
|
||||
</div>
|
||||
<div class="installed-engine-actions" @click.stop>
|
||||
<el-button type="primary" plain @click="openOllamaConnection">
|
||||
{{ $t('database.databaseConnInfo') }}
|
||||
</el-button>
|
||||
<el-button :loading="openingWebUI" @click="openWebUI">OpenWebUI</el-button>
|
||||
<el-button @click="openOllama">
|
||||
{{ $t('aiTools.model.modelManagement') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="installed-engine-title">{{ $t('aiTools.model.serviceDetails') }}</h4>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item :label="$t('app.containerName')">
|
||||
<div class="installed-engine-value">
|
||||
<code>{{ ollama.containerName }}</code>
|
||||
<CopyButton :content="ollama.containerName" />
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.port')">
|
||||
{{ ollama.httpPort || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('aiTools.model.endpoint')">
|
||||
<div class="installed-engine-value">
|
||||
<code>{{ ollama.modelAccountBaseURL || '—' }}</code>
|
||||
<CopyButton v-if="ollama.modelAccountBaseURL" :content="ollama.modelAccountBaseURL" />
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('xpack.node.baseDir')">
|
||||
<div class="installed-engine-value">
|
||||
<code>{{ ollama.installPath || '—' }}</code>
|
||||
<CopyButton v-if="ollama.installPath" :content="ollama.installPath" />
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.createdAt')">
|
||||
{{ ollama.createdAt ? dateFormatSimple(ollama.createdAt) : '—' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
</div>
|
||||
<template v-if="activeForm" #footer>
|
||||
<el-button :disabled="currentForm?.loading" @click="visible = false">
|
||||
@@ -146,54 +48,34 @@
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else-if="selected === 'ollama' && ollama?.isExist" #footer>
|
||||
<el-button @click="visible = false">{{ $t('commons.button.close') }}</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<EngineConnection ref="connectionRef" />
|
||||
<PortJumpDialog ref="portRef" />
|
||||
<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 { ElMessageBox } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
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 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, getAppPort } from '@/api/modules/app';
|
||||
import { dateFormatSimple } from '@/utils/date';
|
||||
import i18n from '@/lang';
|
||||
import EngineConnection from '../connection/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import { vllmAvailable } from '../../deployment';
|
||||
|
||||
const props = defineProps({ enginesOnly: Boolean, vllmOnly: Boolean });
|
||||
const props = defineProps({ enginesOnly: Boolean });
|
||||
const emit = defineEmits(['search', 'manage']);
|
||||
const { isProductPro, isMobile, currentNode } = useGlobalStore();
|
||||
const router = useRouter();
|
||||
const { isProductPro, isMobile } = useGlobalStore();
|
||||
const visible = ref(false);
|
||||
const selected = ref<'account' | 'vllm' | 'ollama'>('account');
|
||||
const selected = ref<'account' | 'vllm'>('account');
|
||||
const modelDir = ref('');
|
||||
const recipe = ref<{ id?: number; imageType: string }>();
|
||||
const dockerActive = ref(false);
|
||||
const dockerExists = ref(true);
|
||||
const ollama = ref<App.CheckInstalled>();
|
||||
const accountRef = ref();
|
||||
const editAccountRef = ref();
|
||||
const ollamaRef = ref();
|
||||
const vllmRef = ref();
|
||||
const taskRef = ref();
|
||||
const connectionRef = ref();
|
||||
const portRef = ref();
|
||||
const openingWebUI = ref(false);
|
||||
const vllmForm = shallowRef();
|
||||
const initializing = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
@@ -202,14 +84,12 @@ const activeForm = computed(() => {
|
||||
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';
|
||||
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;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -230,8 +110,7 @@ watch(
|
||||
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 === 'vllm') await vllmRef.value.openCreate(modelDir.value, recipe.value);
|
||||
} catch {
|
||||
if (!cancelled) loadFailed.value = true;
|
||||
} finally {
|
||||
@@ -241,205 +120,31 @@ watch(
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
const open = async (engine?: 'ollama' | 'vllm', defaultModelDir = '') => {
|
||||
const open = (engine?: 'vllm', defaultModelDir = '', defaultRecipe?: { id?: number; imageType: string }) => {
|
||||
loadFailed.value = false;
|
||||
modelDir.value = defaultModelDir;
|
||||
selected.value = props.vllmOnly ? 'vllm' : engine || (props.enginesOnly ? 'vllm' : 'account');
|
||||
recipe.value = defaultRecipe;
|
||||
selected.value = engine || (props.enginesOnly ? 'vllm' : 'account');
|
||||
dockerActive.value = false;
|
||||
dockerExists.value = true;
|
||||
ollama.value = undefined;
|
||||
visible.value = true;
|
||||
if (selected.value === 'account' || 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 openOllamaConnection = () => {
|
||||
if (!ollama.value?.isExist) return;
|
||||
connectionRef.value.acceptParams({
|
||||
name: ollama.value.name,
|
||||
engine: 'ollama',
|
||||
port: ollama.value.httpPort,
|
||||
containerName: ollama.value.containerName,
|
||||
appinstallID: ollama.value.appInstallId,
|
||||
});
|
||||
};
|
||||
|
||||
const openWebUI = async () => {
|
||||
const node = currentNode.value;
|
||||
openingWebUI.value = true;
|
||||
try {
|
||||
const { data } = await getAppPort('ollama-webui', '');
|
||||
if (node !== currentNode.value || !visible.value || selected.value !== 'ollama') return;
|
||||
if (data) {
|
||||
await portRef.value.acceptParams({ port: data });
|
||||
return;
|
||||
}
|
||||
const confirmed = await ElMessageBox.confirm(
|
||||
i18n.global.t('app.checkInstalledWarn', ['OpenWebUI']),
|
||||
'OpenWebUI',
|
||||
{
|
||||
confirmButtonText: i18n.global.t('database.goInstall'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
).catch(() => false);
|
||||
if (!confirmed || node !== currentNode.value || !visible.value || selected.value !== 'ollama') return;
|
||||
visible.value = false;
|
||||
await router.push({ name: 'AppAll', query: { install: 'ollama-webui' } });
|
||||
} finally {
|
||||
openingWebUI.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openVllmLicense = () => {
|
||||
visible.value = false;
|
||||
emit('manage', { engine: 'vllm', id: 0, name: '' });
|
||||
emit('manage', { 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);
|
||||
}
|
||||
<style scoped>
|
||||
.service-form {
|
||||
min-height: 100px;
|
||||
}
|
||||
.installed-engine-overview {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--el-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
.installed-engine-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
}
|
||||
.installed-engine-identity {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 16px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
span {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.installed-engine-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
.el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.installed-engine-title {
|
||||
margin: 24px 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.installed-engine {
|
||||
:deep(.el-descriptions__table) {
|
||||
table-layout: fixed;
|
||||
}
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 110px;
|
||||
}
|
||||
}
|
||||
.installed-engine-value {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
:deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.engine-options {
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,14 +11,9 @@
|
||||
<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="service-card">
|
||||
<div class="flex flex-wrap gap-3 items-center mb-4">
|
||||
<strong>
|
||||
{{ engineLabels[instance.engine] }}
|
||||
{{
|
||||
instance.engine === 'vllm' ? formatVllmVersion(instance.version) : instance.version
|
||||
}}
|
||||
</strong>
|
||||
<strong>vLLM {{ formatVllmVersion(instance.appVersion) }}</strong>
|
||||
<Status :status="instance.status" />
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
@@ -36,7 +31,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions v-if="instance.engine !== 'ollama'" :column="1" border>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item :label="$t('commons.table.name')">
|
||||
{{ instance.name }}
|
||||
</el-descriptions-item>
|
||||
@@ -57,17 +52,15 @@
|
||||
v-if="visible && view && view !== VllmPlaceholder && !failed && !missing"
|
||||
ref="contentRef"
|
||||
v-bind="
|
||||
view === AppParams
|
||||
? { modelGPU: true }
|
||||
: view === EngineConnection
|
||||
? {}
|
||||
: view === ContainerLogs
|
||||
? { highlightDiff: 210 }
|
||||
: view === TaskLog
|
||||
? { width: '70%' }
|
||||
: ['config', 'monitor'].includes(actionName)
|
||||
? {}
|
||||
: { dialog: true }
|
||||
view === EngineConnection
|
||||
? {}
|
||||
: view === ContainerLogs
|
||||
? { highlightDiff: 210 }
|
||||
: view === TaskLog
|
||||
? { width: '70%' }
|
||||
: ['config', 'monitor'].includes(actionName)
|
||||
? {}
|
||||
: { dialog: true }
|
||||
"
|
||||
@close="returnToManagement"
|
||||
@search="emit('search')"
|
||||
@@ -79,7 +72,7 @@
|
||||
size="small"
|
||||
>
|
||||
<p>{{ $t('commons.msg.delete', [deleting?.name || '']) }}</p>
|
||||
<el-checkbox v-if="deleting?.engine === 'vllm'" v-model="forceDelete" :label="$t('website.forceDelete')" />
|
||||
<el-checkbox v-model="forceDelete" :label="$t('website.forceDelete')" />
|
||||
<template #footer>
|
||||
<el-button :disabled="deleteLoading" @click="deleteVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
@@ -96,14 +89,12 @@ 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 { searchAppInstalled } from '@/api/modules/app';
|
||||
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 { routerToName } from '@/utils/router';
|
||||
import AppParams from '@/views/app-store/installed/detail/index.vue';
|
||||
import EngineConnection from '../connection/index.vue';
|
||||
import VllmPlaceholder from '../../vllm/index.vue';
|
||||
import ContainerLogs from '@/components/log/container-drawer/index.vue';
|
||||
@@ -125,14 +116,14 @@ const loading = ref(false);
|
||||
const failed = ref(false);
|
||||
const missing = ref(false);
|
||||
const header = ref('');
|
||||
const instance = ref<Deployment>();
|
||||
type DeploymentDetails = Pick<Deployment, 'id' | 'name'> & Partial<Deployment>;
|
||||
const instance = ref<DeploymentDetails>();
|
||||
const actionName = ref('manage');
|
||||
const deleteVisible = ref(false);
|
||||
const deleting = ref<Deployment>();
|
||||
const deleting = ref<DeploymentDetails>();
|
||||
const forceDelete = ref(false);
|
||||
const deleteLoading = ref(false);
|
||||
const deleted = ref(false);
|
||||
const engineLabels = { vllm: 'vLLM', ollama: 'Ollama' };
|
||||
const view = shallowRef();
|
||||
const contentRef = ref();
|
||||
let request = 0;
|
||||
@@ -158,74 +149,26 @@ const openUpgrade = async (installId: number, version: number) => {
|
||||
if (standalone.value) visible.value = false;
|
||||
emit('upgrade', app);
|
||||
};
|
||||
const open = async (
|
||||
row: Pick<Deployment, 'engine' | 'id' | 'name'> & Partial<Deployment>,
|
||||
action = 'manage',
|
||||
outside = false,
|
||||
) => {
|
||||
if (row.engine === 'ollama' && action === 'manage') {
|
||||
visible.value = false;
|
||||
await routerToName('OllamaModels');
|
||||
return;
|
||||
}
|
||||
const open = async (row: DeploymentDetails, action = 'manage', outside = false) => {
|
||||
const version = ++request;
|
||||
standalone.value = outside;
|
||||
instance.value = { version: '', ports: [], containerName: '', status: '', modelDir: '', models: [], ...row };
|
||||
instance.value = { ...row };
|
||||
actionName.value = action;
|
||||
deleted.value = false;
|
||||
header.value = `${engineLabels[row.engine]} ${i18n.global.t('aiTools.model.manage')}`;
|
||||
header.value = `vLLM ${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)) {
|
||||
if (!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,
|
||||
taskID: data.taskID,
|
||||
containerName: data.containerName,
|
||||
};
|
||||
if (data.taskID && ['config', 'upgrade', 'log'].includes(action)) {
|
||||
await openTask(data.taskID);
|
||||
return;
|
||||
}
|
||||
if (action === 'upgrade') {
|
||||
await openUpgrade(data.appInstallId, version);
|
||||
return;
|
||||
}
|
||||
const component = { config: AppParams, connection: EngineConnection, log: ContainerLogs }[action];
|
||||
view.value = component ? markRaw(component) : undefined;
|
||||
await nextTick();
|
||||
if (version !== request || !visible.value) return;
|
||||
if (action === 'config') await contentRef.value?.acceptParams({ id: data.appInstallId, edit: true });
|
||||
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 = await readPages((page) => loadVllmAPI().then((api) => api.pageVllm({ ...page, info: row.name })));
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== request || !visible.value) return;
|
||||
const items = await readPages((page) => api.pageVllm({ ...page, info: row.name }));
|
||||
const detail = items.find((item) => item.id === row.id);
|
||||
if (version !== request || !visible.value) return;
|
||||
if (!detail) {
|
||||
@@ -233,15 +176,7 @@ const open = async (
|
||||
if (standalone.value) MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
instance.value = {
|
||||
...instance.value,
|
||||
status: detail.status,
|
||||
taskID: detail.taskID,
|
||||
containerName: detail.containerName,
|
||||
modelDir: detail.modelDir,
|
||||
version: detail.appVersion,
|
||||
upgradable: detail.upgradable,
|
||||
};
|
||||
instance.value = detail;
|
||||
if (detail.taskID && ['config', 'upgrade', 'delete'].includes(action)) {
|
||||
await openTask(detail.taskID);
|
||||
return;
|
||||
@@ -254,7 +189,7 @@ const open = async (
|
||||
port: detail.port,
|
||||
containerPort: '',
|
||||
endpoint: detail.modelAccountBaseURL,
|
||||
engine: row.engine,
|
||||
engine: 'vllm',
|
||||
name: row.name,
|
||||
modelDir: detail.modelDir,
|
||||
});
|
||||
@@ -306,7 +241,7 @@ watch([currentNode, () => route.fullPath, isProductPro], () => {
|
||||
deleteVisible.value = false;
|
||||
deleting.value = undefined;
|
||||
});
|
||||
const operationDisabled = (row: Deployment) => Boolean(deleteLoading.value || row.taskID);
|
||||
const operationDisabled = (row: DeploymentDetails) => Boolean(deleteLoading.value || row.taskID);
|
||||
const deleteDeployment = async () => {
|
||||
const version = request;
|
||||
const row = deleting.value;
|
||||
@@ -332,25 +267,24 @@ const deploymentButtons = [
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => open(row, 'config'),
|
||||
click: (row: DeploymentDetails) => open(row, 'config'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('menu.monitor'),
|
||||
show: (row: Deployment) => row.engine === 'vllm',
|
||||
click: (row: Deployment) => open(row, 'monitor'),
|
||||
click: (row: DeploymentDetails) => open(row, 'monitor'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.upgrade'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.upgradable && hasPermission('app_manage'),
|
||||
show: (row: DeploymentDetails) => row.upgradable && hasPermission('app_manage'),
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => open(row, 'upgrade'),
|
||||
click: (row: DeploymentDetails) => open(row, 'upgrade'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
disabled: (row: Deployment) => operationDisabled(row),
|
||||
click: (row: Deployment) => {
|
||||
disabled: (row: DeploymentDetails) => operationDisabled(row),
|
||||
click: (row: DeploymentDetails) => {
|
||||
deleting.value = row;
|
||||
forceDelete.value = false;
|
||||
deleteVisible.value = true;
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
import { onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import { syncOllamaModelAccount } from '@/api/modules/ai';
|
||||
import { checkAppInstalled } from '@/api/modules/app';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { useRoute } from 'vue-router';
|
||||
import i18n from '@/lang';
|
||||
@@ -50,7 +48,6 @@ const formRef = ref<FormInstance>();
|
||||
const addressRef = ref<InstanceType<typeof AccountAddress>>();
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
engine: 'ollama' as 'ollama' | 'vllm',
|
||||
name: '',
|
||||
containerName: '',
|
||||
hostPort: 0,
|
||||
@@ -68,55 +65,34 @@ const close = () => {
|
||||
};
|
||||
|
||||
const open = async (row: Deployment) => {
|
||||
if (row.engine !== 'ollama' && row.engine !== 'vllm') return;
|
||||
const version = ++request;
|
||||
form.id = 0;
|
||||
form.name = row.name;
|
||||
form.engine = row.engine;
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
if (row.engine === 'ollama') {
|
||||
const { data: item } = await checkAppInstalled('ollama', row.name);
|
||||
if (version !== request) return;
|
||||
if (!item.isExist || item.appInstallId !== row.id) {
|
||||
MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
Object.assign(form, {
|
||||
id: item.appInstallId,
|
||||
containerName: item.containerName,
|
||||
hostPort: item.httpPort,
|
||||
containerPort: '11434',
|
||||
hostMode: false,
|
||||
modelAccountBaseURLType: item.modelAccountBaseURLType || 'container',
|
||||
modelAccountBaseURL: item.modelAccountBaseURL || '',
|
||||
});
|
||||
} else {
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== request) return;
|
||||
const items = await readPages((page) => api.pageVllm({ ...page, info: row.name }));
|
||||
if (version !== request) return;
|
||||
const item = items.find((item) => item.id === row.id);
|
||||
if (!item) {
|
||||
MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
const hostMode = item.appVersion.trim().toLowerCase().startsWith('nvidia-gb10-dspark-');
|
||||
Object.assign(form, {
|
||||
id: item.id,
|
||||
containerName: item.containerName,
|
||||
hostPort: item.port,
|
||||
containerPort:
|
||||
item.command.match(/--port\s+(\d+)/)?.[1] || (item.imageType === 'intel' ? '8001' : '8000'),
|
||||
hostMode,
|
||||
modelAccountBaseURLType:
|
||||
hostMode && item.modelAccountBaseURLType === 'container'
|
||||
? 'systemIP'
|
||||
: item.modelAccountBaseURLType || (hostMode ? 'systemIP' : 'container'),
|
||||
modelAccountBaseURL: item.modelAccountBaseURL || '',
|
||||
});
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== request) return;
|
||||
const items = await readPages((page) => api.pageVllm({ ...page, info: row.name }));
|
||||
if (version !== request) return;
|
||||
const item = items.find((item) => item.id === row.id);
|
||||
if (!item) {
|
||||
MsgError(i18n.global.t('commons.res.notFound'));
|
||||
return;
|
||||
}
|
||||
const hostMode = item.appVersion.trim().toLowerCase().startsWith('nvidia-gb10-dspark-');
|
||||
Object.assign(form, {
|
||||
id: item.id,
|
||||
containerName: item.containerName,
|
||||
hostPort: item.port,
|
||||
containerPort: item.command.match(/--port\s+(\d+)/)?.[1] || (item.imageType === 'intel' ? '8001' : '8000'),
|
||||
hostMode,
|
||||
modelAccountBaseURLType:
|
||||
hostMode && item.modelAccountBaseURLType === 'container'
|
||||
? 'systemIP'
|
||||
: item.modelAccountBaseURLType || (hostMode ? 'systemIP' : 'container'),
|
||||
modelAccountBaseURL: item.modelAccountBaseURL || '',
|
||||
});
|
||||
} finally {
|
||||
if (version === request) loading.value = false;
|
||||
}
|
||||
@@ -134,13 +110,9 @@ const submit = async () => {
|
||||
modelAccountBaseURLType: form.modelAccountBaseURLType,
|
||||
modelAccountBaseURL: addressRef.value.baseURL,
|
||||
};
|
||||
if (form.engine === 'ollama') {
|
||||
await syncOllamaModelAccount(req);
|
||||
} else {
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== request || !visible.value) return;
|
||||
await api.operateVllm({ ...req, operate: 'sync', taskID: '' });
|
||||
}
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== request || !visible.value) return;
|
||||
await api.operateVllm({ ...req, operate: 'sync', taskID: '' });
|
||||
if (version !== request) return;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import type { ReqPage, ResPage, ResultData } from '@/api/interface';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import { searchOllamaModel } from '@/api/modules/ai';
|
||||
import { checkAppInstalled, searchAppInstalled } from '@/api/modules/app';
|
||||
|
||||
export interface Deployment {
|
||||
taskID?: string;
|
||||
id: number;
|
||||
engine: 'vllm' | 'ollama';
|
||||
name: string;
|
||||
version: string;
|
||||
ports: number[];
|
||||
containerName: string;
|
||||
modelDir: string;
|
||||
models: string[];
|
||||
status: string;
|
||||
upgradable?: boolean;
|
||||
}
|
||||
|
||||
interface VllmItem {
|
||||
taskID?: string;
|
||||
appInstallId: number;
|
||||
agentAccountId: number;
|
||||
@@ -38,7 +22,7 @@ interface VllmItem {
|
||||
}
|
||||
|
||||
interface VllmAPI {
|
||||
pageVllm: (req: ReqPage & { info: string }) => Promise<ResultData<ResPage<VllmItem>>>;
|
||||
pageVllm: (req: ReqPage & { info: string }) => Promise<ResultData<ResPage<Deployment>>>;
|
||||
operateVllm: (
|
||||
req: { id: number; operate: string; taskID: string; forceDelete?: boolean } & Partial<AI.ModelAccountSync>,
|
||||
) => Promise<unknown>;
|
||||
@@ -68,45 +52,3 @@ export async function readPages<T>(search: (page: ReqPage) => Promise<ResultData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadModelOverview(pro: boolean) {
|
||||
const [vllm, ollama] = await Promise.all([
|
||||
pro && vllmAvailable
|
||||
? loadVllmAPI().then((api) => readPages((page) => api.pageVllm({ ...page, info: '' })))
|
||||
: Promise.resolve([]),
|
||||
checkAppInstalled('ollama', ''),
|
||||
]);
|
||||
const deployments: Deployment[] = vllm.map((item) => ({
|
||||
id: item.id,
|
||||
engine: 'vllm',
|
||||
upgradable: item.upgradable,
|
||||
name: item.name,
|
||||
version: item.appVersion,
|
||||
ports: item.port > 0 ? [item.port] : [],
|
||||
containerName: item.containerName,
|
||||
modelDir: item.modelDir,
|
||||
models: item.servedModelName ? [item.servedModelName] : [],
|
||||
status: item.status,
|
||||
taskID: item.taskID,
|
||||
}));
|
||||
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,
|
||||
ports: ollama.data.httpPort > 0 ? [ollama.data.httpPort] : [],
|
||||
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,
|
||||
taskID: ollama.data.taskID,
|
||||
});
|
||||
}
|
||||
return deployments;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,7 @@
|
||||
<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="routerToName('VllmCommandTemplates')"
|
||||
>
|
||||
{{ $t('aiTools.model.deploymentTemplates') }}
|
||||
{{ $t(isLocal ? 'commons.button.add' : 'aiTools.model.addService') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
@@ -30,19 +23,7 @@
|
||||
<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-if="!isLocal"
|
||||
v-model="provider"
|
||||
clearable
|
||||
filterable
|
||||
@@ -179,12 +160,7 @@
|
||||
width="220"
|
||||
/>
|
||||
</ComplexTable>
|
||||
<ComplexTable
|
||||
v-else
|
||||
:data="deploymentPage"
|
||||
:pagination-config="pagination"
|
||||
@search="resetPageIfNeeded"
|
||||
>
|
||||
<ComplexTable v-else :data="deployments" :pagination-config="pagination" @search="search">
|
||||
<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)">
|
||||
@@ -195,12 +171,7 @@
|
||||
<el-table-column :label="$t('app.version')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>
|
||||
{{
|
||||
(row.engine === 'vllm' ? formatVllmVersion(row.version) : row.version) ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
<span>{{ formatVllmVersion(row.appVersion) || '—' }}</span>
|
||||
<el-button
|
||||
v-if="row.upgradable && hasPermission('app_manage')"
|
||||
v-permission
|
||||
@@ -217,57 +188,23 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.port')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.ports.length">—</span>
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="(port, index) in row.ports.slice(0, 2)"
|
||||
:key="index"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<el-button
|
||||
icon="Position"
|
||||
plain
|
||||
size="small"
|
||||
@click="portJumpRef.acceptParams({ port, ip: '', protocol: 'http' })"
|
||||
>
|
||||
{{ port }}
|
||||
</el-button>
|
||||
<el-popover
|
||||
v-if="index === 1 && row.ports.length > 2"
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
:title="$t('commons.table.port')"
|
||||
:width="160"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" class="shrink-0">
|
||||
+{{ row.ports.length - 2 }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="max-h-60 overflow-y-auto space-y-1">
|
||||
<div v-for="(item, portIndex) in row.ports" :key="portIndex">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="row.port > 0"
|
||||
icon="Position"
|
||||
plain
|
||||
size="small"
|
||||
@click="portJumpRef.acceptParams({ port: row.port, ip: '', protocol: 'http' })"
|
||||
>
|
||||
{{ row.port }}
|
||||
</el-button>
|
||||
<span v-else>—</span>
|
||||
</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"
|
||||
size="small"
|
||||
class="model-more"
|
||||
@click="manageDeployment(row)"
|
||||
>
|
||||
+{{ row.models.length - 2 }}
|
||||
</el-button>
|
||||
<div class="cell-ellipsis" :title="row.servedModelName">
|
||||
{{ row.servedModelName || '—' }}
|
||||
</div>
|
||||
<span v-if="!row.models.length">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" min-width="150">
|
||||
@@ -339,21 +276,13 @@
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
<CreateService
|
||||
ref="createRef"
|
||||
:engines-only="isLocal || isFiles"
|
||||
:vllm-only="isFiles"
|
||||
@search="search"
|
||||
@manage="manageDeployment"
|
||||
/>
|
||||
<CreateService ref="createRef" :engines-only="isLocal || 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" />
|
||||
<TerminalDialog :key="`terminal:${currentNode}:${route.fullPath}`" ref="terminalRef" />
|
||||
@@ -376,32 +305,20 @@ import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { getAgentProviderDisplayName, isAgentAccountVerificationSkipped } from '@/utils/agent';
|
||||
import i18n from '@/lang';
|
||||
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 { routerToName } from '@/utils/router';
|
||||
import TaskLog from '@/components/log/task/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 TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||
import SyncService from './components/sync/index.vue';
|
||||
import ProviderLabel from '@/components/agent-provider-label/index.vue';
|
||||
import {
|
||||
formatVllmVersion,
|
||||
loadModelOverview,
|
||||
loadVllmAPI,
|
||||
readPages,
|
||||
vllmAvailable,
|
||||
type Deployment,
|
||||
} from './deployment';
|
||||
import { formatVllmVersion, loadVllmAPI, vllmAvailable, type Deployment } from './deployment';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -412,15 +329,13 @@ const isFiles = pagePath === '/ai/model/files';
|
||||
const isLocal = pagePath === '/ai/model/local';
|
||||
const tabs = computed(() => [
|
||||
{ label: i18n.global.t('aiTools.model.files'), path: '/ai/model/files' },
|
||||
{ label: i18n.global.t('aiTools.model.deployments'), path: '/ai/model/local' },
|
||||
{ label: i18n.global.t('aiTools.model.services'), path: '/ai/model/account' },
|
||||
{ label: i18n.global.t('aiTools.model.engines'), path: '/ai/model/local' },
|
||||
]);
|
||||
const downloaderView = defineAsyncComponent(() => import('@/views/model-downloader/index.vue'));
|
||||
const createRef = ref();
|
||||
const manageRef = ref();
|
||||
const upgradeRef = ref();
|
||||
const appDeleteRef = ref();
|
||||
const appResourcesRef = ref();
|
||||
const poolRef = ref();
|
||||
const terminalRef = ref<InstanceType<typeof TerminalDialog>>();
|
||||
const syncRef = ref<InstanceType<typeof SyncService>>();
|
||||
@@ -447,7 +362,6 @@ watch(
|
||||
);
|
||||
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 });
|
||||
@@ -483,23 +397,9 @@ const providerOptions = computed(() =>
|
||||
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, currentNode],
|
||||
[deployments, isCurrentPage, currentNode],
|
||||
async ([rows, active, node], _, onCleanup) => {
|
||||
resourceStats.value = {};
|
||||
const controller = new AbortController();
|
||||
@@ -540,12 +440,6 @@ const resetPageIfNeeded = () => {
|
||||
Math.max(1, Math.ceil(pagination.total / pagination.pageSize)),
|
||||
);
|
||||
};
|
||||
watch(filteredDeployments, () => {
|
||||
if (!isLocal) return;
|
||||
pagination.total = filteredDeployments.value.length;
|
||||
resetPageIfNeeded();
|
||||
});
|
||||
|
||||
let searchVersion = 0;
|
||||
const search = async () => {
|
||||
const version = ++searchVersion;
|
||||
@@ -553,9 +447,21 @@ const search = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isLocal) {
|
||||
const data = await loadModelOverview(isProductPro.value);
|
||||
if (!isProductPro.value || !vllmAvailable) {
|
||||
deployments.value = [];
|
||||
pagination.total = 0;
|
||||
return;
|
||||
}
|
||||
const api = await loadVllmAPI();
|
||||
if (version !== searchVersion) return;
|
||||
deployments.value = data;
|
||||
const { data } = await api.pageVllm({
|
||||
page: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
info: searchName.value,
|
||||
});
|
||||
if (version !== searchVersion) return;
|
||||
deployments.value = data.items || [];
|
||||
pagination.total = data.total;
|
||||
} else {
|
||||
const result = await pageAgentAccounts({
|
||||
page: pagination.currentPage,
|
||||
@@ -567,10 +473,10 @@ const search = async () => {
|
||||
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();
|
||||
}
|
||||
const previousPage = pagination.currentPage;
|
||||
resetPageIfNeeded();
|
||||
if (previousPage !== pagination.currentPage) await search();
|
||||
} catch {
|
||||
if (version !== searchVersion) return;
|
||||
connections.value = [];
|
||||
@@ -617,10 +523,9 @@ const accountButtons = [
|
||||
},
|
||||
];
|
||||
const taskRef = ref();
|
||||
const pending = ref('');
|
||||
const pending = ref(false);
|
||||
const operationDisabled = (row: Deployment) => Boolean(pending.value || row.taskID);
|
||||
const statusDisabled = (row: Deployment) =>
|
||||
operationDisabled(row) || !hasManagePermissionAccess() || (row.engine === 'ollama' && !hasPermission('app_manage'));
|
||||
const statusDisabled = (row: Deployment) => operationDisabled(row) || !hasManagePermissionAccess();
|
||||
const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart') => {
|
||||
if (statusDisabled(row)) return;
|
||||
const node = currentNode.value;
|
||||
@@ -630,46 +535,30 @@ const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart')
|
||||
{ type: 'warning' },
|
||||
);
|
||||
if (node !== currentNode.value || statusDisabled(row)) return;
|
||||
pending.value = `${row.engine}:${row.id}`;
|
||||
pending.value = true;
|
||||
try {
|
||||
if (row.engine === 'ollama') {
|
||||
const taskID = newUUID();
|
||||
await installedOp({ installId: row.id, operate: operation, taskID });
|
||||
if (node !== currentNode.value) return;
|
||||
taskRef.value.openWithTaskID(taskID);
|
||||
} else {
|
||||
const taskID = newUUID();
|
||||
await (await loadVllmAPI()).operateVllm({ id: row.id, operate: operation, taskID });
|
||||
if (node !== currentNode.value) return;
|
||||
taskRef.value.openWithTaskID(taskID);
|
||||
}
|
||||
const taskID = newUUID();
|
||||
await (await loadVllmAPI()).operateVllm({ id: row.id, operate: operation, taskID });
|
||||
if (node !== currentNode.value) return;
|
||||
taskRef.value.openWithTaskID(taskID);
|
||||
await search();
|
||||
} finally {
|
||||
pending.value = '';
|
||||
pending.value = false;
|
||||
}
|
||||
};
|
||||
const deploymentButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'vllm' || (row.engine === 'ollama' && hasPermission('app_manage')),
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'config', true),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('aiTools.model.manage'),
|
||||
show: (row: Deployment) => row.engine !== 'vllm',
|
||||
click: manageDeployment,
|
||||
},
|
||||
{
|
||||
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('menu.terminal'),
|
||||
show: (row: Deployment) => row.engine === 'ollama' || row.engine === 'vllm',
|
||||
disabled: (row: Deployment) =>
|
||||
operationDisabled(row) || row.status !== 'Running' || !row.containerName || !isAdminOrNodeAdmin.value,
|
||||
click: (row: Deployment) =>
|
||||
@@ -682,44 +571,12 @@ const deploymentButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.sync'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'ollama' || row.engine === 'vllm',
|
||||
disabled: operationDisabled,
|
||||
click: (row: Deployment) => syncRef.value.open(row),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
permission: true,
|
||||
show: (row: Deployment) => row.engine === 'ollama' && hasPermission('app_manage'),
|
||||
disabled: (row: Deployment) => operationDisabled(row),
|
||||
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),
|
||||
click: (row: Deployment) => manageRef.value.open(row, 'delete', true),
|
||||
},
|
||||
@@ -746,9 +603,17 @@ watch(
|
||||
await router.replace('/ai/model/files');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isLocal &&
|
||||
route.query.open === 'create' &&
|
||||
route.query.engine !== 'vllm' &&
|
||||
(route.query.engine === 'ollama' || route.query.tab === 'ollama')
|
||||
) {
|
||||
await router.replace({ name: 'AppAll', query: { install: 'ollama' } });
|
||||
return;
|
||||
}
|
||||
if (isLocal || isFiles) {
|
||||
apiType.value = '';
|
||||
engine.value = '';
|
||||
provider.value = '';
|
||||
searchName.value = '';
|
||||
resetPage();
|
||||
@@ -761,15 +626,10 @@ watch(
|
||||
}
|
||||
await search();
|
||||
if (cancelled) return;
|
||||
if (
|
||||
isCurrentPage.value &&
|
||||
isLocal &&
|
||||
route.query.open === 'create' &&
|
||||
(route.query.engine === 'ollama' || route.query.engine === 'vllm' || route.query.tab === 'ollama')
|
||||
) {
|
||||
if (isCurrentPage.value && isLocal && route.query.open === 'create' && route.query.engine === 'vllm') {
|
||||
await nextTick();
|
||||
if (cancelled) return;
|
||||
await createRef.value.open(route.query.engine === 'vllm' ? 'vllm' : 'ollama');
|
||||
await createRef.value.open('vllm');
|
||||
if (cancelled) return;
|
||||
const query = { ...route.query };
|
||||
delete query.open;
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton :buttons="tabs" />
|
||||
<RouterButton :buttons="[{ label: $t('aiTools.model.ollamaManagement'), path: '/ai/model/local/ollama' }]" />
|
||||
<OllamaModels :key="currentNode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import i18n from '@/lang';
|
||||
import OllamaModels from './models/index.vue';
|
||||
|
||||
const { currentNode } = useGlobalStore();
|
||||
const tabs = computed(() => [
|
||||
{ label: i18n.global.t('aiTools.model.files'), path: '/ai/model/files' },
|
||||
{ label: i18n.global.t('aiTools.model.services'), path: '/ai/model/account' },
|
||||
{ label: i18n.global.t('aiTools.model.engines'), path: '/ai/model/local' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
<template>
|
||||
<div v-loading="loading || initializing">
|
||||
<LayoutContent back-name="LocalModel" :title="'Ollama ' + $t('aiTools.model.modelManagement')">
|
||||
<LayoutContent :title="$t('aiTools.model.ollamaManagement')">
|
||||
<template #app>
|
||||
<AppStatus
|
||||
ref="appStatusRef"
|
||||
app-key="ollama"
|
||||
:hide-setting="true"
|
||||
v-model:loading="loading"
|
||||
@is-exist="checkExist"
|
||||
/>
|
||||
</template>
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false" :title="$t('runtime.systemRestartHelper')" />
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" type="primary" @click="onCreate">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" @click="search">
|
||||
{{ $t('database.loadFromRemote') }}
|
||||
</el-button>
|
||||
<el-button v-permission :disabled="modelInfo.status !== 'Running'" @click="bindDomain">
|
||||
{{ $t('aiTools.proxy.proxy') }}
|
||||
</el-button>
|
||||
<el-button :disabled="modelInfo.status !== 'Running'" @click="onLoadConn">
|
||||
{{ $t('database.databaseConnInfo') }}
|
||||
</el-button>
|
||||
<el-button :disabled="modelInfo.status !== 'Running'" icon="Position" @click="openWebUI">
|
||||
OpenWebUI
|
||||
</el-button>
|
||||
<el-button v-permission :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
@@ -29,7 +44,7 @@
|
||||
<el-button
|
||||
v-permission
|
||||
type="primary"
|
||||
@click="routerToNameWithQuery('LocalModel', { open: 'create', engine: 'ollama' })"
|
||||
@click="routerToNameWithQuery('AppAll', { install: 'ollama' })"
|
||||
>
|
||||
{{ $t('commons.button.install') }}
|
||||
</el-button>
|
||||
@@ -126,6 +141,8 @@
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<BindDomain ref="bindDomainRef" />
|
||||
<Connection ref="connectionRef" />
|
||||
<PortJumpDialog ref="portRef" />
|
||||
<TaskLog ref="taskLogRef" width="50%" @close="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -135,11 +152,16 @@ import { useSearchState } from '@/utils/search-reset';
|
||||
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import Terminal from '@/views/ai/model/ollama/terminal/index.vue';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import { toRef, reactive, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import Connection from '@/views/ai/model/components/connection/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import { App } from '@/api/interface/app';
|
||||
import i18n from '@/lang';
|
||||
import { deleteOllamaModel, loadOllamaModel, recreateOllamaModel, searchOllamaModel } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { checkAppInstalled } from '@/api/modules/app';
|
||||
import { getAppPort } from '@/api/modules/app';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { MsgInfo, MsgSuccess } from '@/utils/message';
|
||||
@@ -147,7 +169,10 @@ import BindDomain from '@/views/ai/model/ollama/domain/index.vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { routerToNameWithQuery } from '@/utils/router';
|
||||
|
||||
const { isMobile } = useGlobalStore();
|
||||
const { isMobile, currentNode } = useGlobalStore();
|
||||
const appStatusRef = ref();
|
||||
const connectionRef = ref();
|
||||
const portRef = ref();
|
||||
const loading = ref(false);
|
||||
const initializing = ref(true);
|
||||
const loadFailed = ref(false);
|
||||
@@ -177,7 +202,7 @@ const opRef = ref();
|
||||
const operateIDs = ref();
|
||||
const forceDelete = ref();
|
||||
|
||||
const modelInfo = reactive({ status: '' });
|
||||
const modelInfo = reactive({ status: '', name: '', containerName: '', httpPort: 0 });
|
||||
|
||||
function selectable(row) {
|
||||
return row.status !== 'Waiting';
|
||||
@@ -327,27 +352,57 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
|
||||
const onLoadConn = () => {
|
||||
connectionRef.value.acceptParams({
|
||||
name: modelInfo.name,
|
||||
engine: 'ollama',
|
||||
port: modelInfo.httpPort,
|
||||
containerName: modelInfo.containerName,
|
||||
appinstallID: appInstallID.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openWebUI = async () => {
|
||||
const node = currentNode.value;
|
||||
const { data: port } = await getAppPort('ollama-webui', '');
|
||||
if (node !== currentNode.value) return;
|
||||
if (port) {
|
||||
portRef.value.acceptParams({ port });
|
||||
return;
|
||||
}
|
||||
const confirmed = await ElMessageBox.confirm(i18n.global.t('app.checkInstalledWarn', ['OpenWebUI']), 'OpenWebUI', {
|
||||
confirmButtonText: i18n.global.t('database.goInstall'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).catch(() => false);
|
||||
if (!confirmed || node !== currentNode.value) return;
|
||||
await routerToNameWithQuery('AppAll', { install: 'ollama-webui' });
|
||||
};
|
||||
|
||||
const checkExist = async (installed: App.CheckInstalled | false) => {
|
||||
loadFailed.value = !installed;
|
||||
selects.value = [];
|
||||
if (!installed) {
|
||||
modelInfo.status = '';
|
||||
initializing.value = false;
|
||||
return;
|
||||
}
|
||||
appInstallID.value = installed.appInstallId;
|
||||
modelInstalled.value = installed.isExist;
|
||||
modelInfo.status = installed.status;
|
||||
modelInfo.name = installed.name;
|
||||
modelInfo.containerName = installed.containerName;
|
||||
modelInfo.httpPort = installed.httpPort;
|
||||
if (installed.isExist) {
|
||||
await search();
|
||||
} else {
|
||||
data.value = [];
|
||||
paginationConfig.total = 0;
|
||||
}
|
||||
initializing.value = false;
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
initializing.value = true;
|
||||
loadFailed.value = false;
|
||||
selects.value = [];
|
||||
try {
|
||||
const { data: installed } = await checkAppInstalled('ollama', '');
|
||||
appInstallID.value = installed.appInstallId;
|
||||
modelInstalled.value = installed.isExist;
|
||||
modelInfo.status = installed.status;
|
||||
if (installed.isExist) {
|
||||
await search();
|
||||
} else {
|
||||
data.value = [];
|
||||
paginationConfig.total = 0;
|
||||
}
|
||||
} catch {
|
||||
modelInfo.status = '';
|
||||
loadFailed.value = true;
|
||||
} finally {
|
||||
initializing.value = false;
|
||||
}
|
||||
await appStatusRef.value?.onCheck('ollama', '');
|
||||
};
|
||||
onMounted(refresh);
|
||||
</script>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
<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" sectioned hide-gpu-config>
|
||||
<template #after-params>
|
||||
<GPUConfig ref="gpuRef" v-model="form.gpu" />
|
||||
<details ref="syncSectionRef" open class="form-section">
|
||||
<summary class="form-section-header">
|
||||
<span>{{ $t('aiTools.model.syncModelAccount') }}</span>
|
||||
<el-switch
|
||||
@click.stop
|
||||
@keydown.stop
|
||||
v-model="form.syncModelAccount"
|
||||
:aria-label="$t('aiTools.model.syncModelAccount')"
|
||||
@change="(enabled) => enabled && syncSectionRef && (syncSectionRef.open = true)"
|
||||
/>
|
||||
</summary>
|
||||
<div v-show="form.syncModelAccount" class="form-section-body">
|
||||
<AccountAddress
|
||||
ref="accountRef"
|
||||
show-sync
|
||||
hide-sync-toggle
|
||||
:form="form"
|
||||
:host-port="Number(form.params.PANEL_APP_PORT_HTTP) || 11434"
|
||||
container-port="11434"
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</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 || gpuRef?.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';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import GPUConfig from '../../components/gpu-config/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 syncSectionRef = ref<HTMLDetailsElement>();
|
||||
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
|
||||
const form = ref({
|
||||
gpu: {
|
||||
enabled: true,
|
||||
mode: 'all',
|
||||
deviceIDs: [],
|
||||
capabilities: ['gpu', 'utility', 'compute'],
|
||||
} as AI.ModelGPUConfig,
|
||||
appDetailId: 0,
|
||||
name: '',
|
||||
params: {} as Record<string, any>,
|
||||
containerName: '',
|
||||
syncModelAccount: true,
|
||||
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({
|
||||
gpu: { enabled: true, mode: 'all', deviceIDs: [], capabilities: ['gpu', 'utility', 'compute'] },
|
||||
restartPolicy: 'unless-stopped',
|
||||
syncModelAccount: true,
|
||||
modelAccountBaseURLType: 'container',
|
||||
modelAccountBaseURL: '',
|
||||
});
|
||||
await formRef.value.initForm('ollama', true);
|
||||
if (!open.value) return;
|
||||
await gpuRef.value?.init(true);
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const submit = async () => {
|
||||
if (loading.value || gpuRef.value?.loading || !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 || gpuRef.value?.loading),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/views/ai/model/components/form-section.scss';
|
||||
</style>
|
||||
@@ -258,7 +258,7 @@ onMounted(async () => {
|
||||
key: installKey.value,
|
||||
},
|
||||
};
|
||||
if (!['ollama', 'vllm'].includes(installKey.value) || !jumpToInstall('', installKey.value)) {
|
||||
if (installKey.value !== 'vllm' || !jumpToInstall('', installKey.value)) {
|
||||
installRef.value.acceptParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutContent :title="currentTitle">
|
||||
<LayoutContent v-if="currentTab !== 'recipes'" :title="currentTitle">
|
||||
<template #search>
|
||||
<el-button
|
||||
v-for="button in buttons"
|
||||
@@ -58,6 +58,23 @@
|
||||
<ProviderLabel :provider="familyProviders[family] || family" :display-name="family" />
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="featuredWeightProvider"
|
||||
clearable
|
||||
filterable
|
||||
placeholder=""
|
||||
class="model-filter"
|
||||
:aria-label="$t('aiTools.model.weightProvider')"
|
||||
@change="handleFeaturedSearch"
|
||||
>
|
||||
<template #prefix>{{ $t('aiTools.model.weightProvider') }}</template>
|
||||
<el-option
|
||||
v-for="provider in featuredWeightProviders"
|
||||
:key="provider"
|
||||
:label="provider"
|
||||
:value="provider"
|
||||
/>
|
||||
</el-select>
|
||||
<TableSearch
|
||||
v-model:searchName="featuredSearchName"
|
||||
class="model-search"
|
||||
@@ -116,20 +133,40 @@
|
||||
:prop="field.prop"
|
||||
:label="$t('aiTools.model.' + field.label)"
|
||||
:min-width="field.width"
|
||||
:card-type="field.prop === 'repoID' ? 'name' : 'content'"
|
||||
:card-type="
|
||||
field.prop === 'repoID' ? 'name' : field.prop === 'weightProvider' ? 'status' : 'content'
|
||||
"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template v-if="field.prop === 'repoID'">
|
||||
<span v-if="featuredViewMode === 'card'" class="featured-name-label">
|
||||
{{ $t('aiTools.model.modelName') }}
|
||||
</span>
|
||||
<el-button link type="primary" class="featured-name" @click="openFeaturedModel(row)">
|
||||
{{ row.repoID }}
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="featured-name"
|
||||
:title="row.repoID"
|
||||
@click="openFeaturedModel(row)"
|
||||
>
|
||||
{{ row.repoID?.slice(row.repoID.indexOf('/') + 1) || '-' }}
|
||||
</el-button>
|
||||
<div v-if="featuredViewMode === 'card' && row.description" class="featured-description">
|
||||
{{ row.description[language.toLowerCase()] || row.description.en }}
|
||||
</div>
|
||||
</template>
|
||||
<el-tag
|
||||
v-else-if="
|
||||
field.prop === 'weightProvider' && featuredViewMode === 'card' && row.weightProvider
|
||||
"
|
||||
class="featured-provider"
|
||||
type="info"
|
||||
effect="plain"
|
||||
size="small"
|
||||
:title="$t('aiTools.model.weightProvider') + ': ' + row.weightProvider"
|
||||
>
|
||||
{{ row.weightProvider }}
|
||||
</el-tag>
|
||||
<ProviderLabel
|
||||
v-else-if="field.prop === 'family'"
|
||||
:provider="familyProviders[row.family]"
|
||||
@@ -254,7 +291,7 @@
|
||||
<template #default="{ row }">{{ row.localModel?.sizeFormatted || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" :label="$t('commons.table.date')" width="180" />
|
||||
<el-table-column :label="$t('commons.table.operate')" width="140" fixed="right">
|
||||
<el-table-column :label="$t('commons.table.operate')" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-permission v-if="row.canCancel" link type="primary" @click="cancelTask(row)">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
@@ -271,6 +308,24 @@
|
||||
>
|
||||
{{ $t('aiTools.model.deploy') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
recipesView &&
|
||||
isProductPro &&
|
||||
row.downloadType === 'all' &&
|
||||
row.status === 'Success' &&
|
||||
row.localModel
|
||||
"
|
||||
v-permission
|
||||
link
|
||||
type="primary"
|
||||
@click="
|
||||
recipeModelPath = row.localModel.path;
|
||||
currentTab = 'recipes';
|
||||
"
|
||||
>
|
||||
{{ $t('aiTools.model.createRecipe') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
v-if="row.canRemoveRecord || (row.localModel && !row.canCancel)"
|
||||
@@ -420,13 +475,29 @@
|
||||
</ComplexTable>
|
||||
</div>
|
||||
</DrawerPro>
|
||||
<component :is="recipesView" v-if="currentTab === 'recipes' && recipesView" :model-path="recipeModelPath">
|
||||
<template #search>
|
||||
<el-button
|
||||
v-for="button in buttons"
|
||||
:key="button.value"
|
||||
class="tag-button"
|
||||
:class="{ 'no-active': currentTab !== button.value }"
|
||||
:type="currentTab === button.value ? 'primary' : undefined"
|
||||
:aria-pressed="currentTab === button.value"
|
||||
@click="currentTab = button.value"
|
||||
>
|
||||
{{ button.label }}
|
||||
</el-button>
|
||||
</template>
|
||||
</component>
|
||||
<Download ref="downloadRef" @search="handleDownloadQueued" />
|
||||
<DownloadDelete ref="downloadDeleteRef" @search="loadDownloads" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch, type Component } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessageBox, FormInstance } from 'element-plus';
|
||||
import { checkFile } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
@@ -455,10 +526,10 @@ import {
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
const emit = defineEmits<{ deploy: [modelDir: string] }>();
|
||||
const { currentNode, isMobile, isOffline, language } = useGlobalStore();
|
||||
const { currentNode, isMobile, isOffline, language, isProductPro } = useGlobalStore();
|
||||
const downloadTypeFilter = ref<ModelDownloader.DownloadType | ''>('');
|
||||
|
||||
type DownloaderTab = 'featured' | 'downloader' | 'settings';
|
||||
type DownloaderTab = 'featured' | 'downloader' | 'recipes' | 'settings';
|
||||
type DownloaderSource = ModelDownloader.Source;
|
||||
type SortValue = ModelDownloader.ModelSearch['sort'];
|
||||
|
||||
@@ -470,9 +541,17 @@ interface SortOption {
|
||||
const searchCardLoading = ref(false);
|
||||
let searchRequest = 0;
|
||||
const savingSettings = ref(false);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const pagePath = route.path;
|
||||
const recipeModules = import.meta.glob<{ default: Component }>('@/xpack/views/vllm/template/index.vue');
|
||||
const recipeLoader = Object.values(recipeModules)[0];
|
||||
const recipesView = recipeLoader ? defineAsyncComponent(recipeLoader) : undefined;
|
||||
const recipeModelPath = ref('');
|
||||
const currentTab = ref<DownloaderTab>('featured');
|
||||
const featuredViewMode = ref<'table' | 'card'>('table');
|
||||
const featuredFamily = ref('');
|
||||
const featuredWeightProvider = ref('');
|
||||
const featuredSearchName = ref('');
|
||||
const familyProviders: Record<string, string> = {
|
||||
Qwen: 'bailian',
|
||||
@@ -484,12 +563,14 @@ const autoSync = ref<'Enable' | 'Disable'>();
|
||||
const featuredModels = ref<ModelDownloader.FeaturedModel[]>([]);
|
||||
const featuredColumns = ref<FuTableColumnConfig[]>([]);
|
||||
const featuredFamilies = ref<string[]>([]);
|
||||
const featuredWeightProviders = ref<string[]>([]);
|
||||
const featuredPagination = reactive({ currentPage: 1, pageSize: 20, total: 0 });
|
||||
const featuredLoading = ref(false);
|
||||
let featuredRequest = 0;
|
||||
const featuredFields = [
|
||||
{ prop: 'repoID', label: 'modelName', width: 400 },
|
||||
{ prop: 'family', label: 'modelCategory', width: 140 },
|
||||
{ prop: 'weightProvider', label: 'weightProvider', width: 180 },
|
||||
{ prop: 'architecture', label: 'modelArchitecture', width: 140 },
|
||||
{ prop: 'backboneParams', label: 'backboneParams', width: 170 },
|
||||
{ prop: 'activeParams', label: 'activeParams', width: 170 },
|
||||
@@ -520,11 +601,14 @@ const openFeaturedModel = (row: ModelDownloader.FeaturedModel) => {
|
||||
);
|
||||
};
|
||||
|
||||
const buttons: Array<{ label: string; value: DownloaderTab }> = [
|
||||
const buttons = computed<Array<{ label: string; value: DownloaderTab }>>(() => [
|
||||
{ label: i18n.global.t('aiTools.model.featuredModels'), value: 'featured' },
|
||||
{ label: i18n.global.t('aiTools.model.downloader'), value: 'downloader' },
|
||||
...(recipesView && isProductPro.value
|
||||
? [{ label: i18n.global.t('aiTools.model.recipes'), value: 'recipes' as const }]
|
||||
: []),
|
||||
{ label: i18n.global.t('commons.button.set'), value: 'settings' },
|
||||
];
|
||||
]);
|
||||
|
||||
const settings = reactive<ModelDownloader.Setting>({
|
||||
modelDir: '/opt/1panel/ai/models',
|
||||
@@ -556,7 +640,10 @@ const downloadPagination = reactive({
|
||||
});
|
||||
|
||||
const currentTitle = computed(() => {
|
||||
return buttons.find((item) => item.value === currentTab.value)?.label || i18n.global.t('aiTools.model.downloader');
|
||||
return (
|
||||
buttons.value.find((item) => item.value === currentTab.value)?.label ||
|
||||
i18n.global.t('aiTools.model.downloader')
|
||||
);
|
||||
});
|
||||
|
||||
const huggingFaceSortOptions = computed<SortOption[]>(() => [
|
||||
@@ -747,12 +834,14 @@ const loadFeaturedModels = async () => {
|
||||
page: featuredPagination.currentPage,
|
||||
pageSize: featuredPagination.pageSize,
|
||||
family: featuredFamily.value,
|
||||
weightProvider: featuredWeightProvider.value,
|
||||
info: featuredSearchName.value,
|
||||
});
|
||||
if (node !== currentNode.value || request !== featuredRequest) return;
|
||||
featuredModels.value = res.data.items;
|
||||
featuredPagination.total = res.data.total;
|
||||
featuredFamilies.value = res.data.families;
|
||||
featuredWeightProviders.value = res.data.weightProviders;
|
||||
} finally {
|
||||
if (request === featuredRequest) featuredLoading.value = false;
|
||||
}
|
||||
@@ -839,15 +928,29 @@ const formatSizePair = (downloaded: number, total: number) => {
|
||||
return `${formatBytes(downloaded)} / ${formatBytes(total)}`;
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => route.query.tab, () => route.path, buttons],
|
||||
([tab, path]) => {
|
||||
if (path !== pagePath) return;
|
||||
currentTab.value = buttons.value.some((button) => button.value === tab) ? (tab as DownloaderTab) : 'featured';
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(currentTab, async (value) => {
|
||||
if (value !== 'recipes') recipeModelPath.value = '';
|
||||
if (route.path === pagePath && route.query.tab !== value)
|
||||
await router.replace({ query: { ...route.query, tab: value } });
|
||||
if (value !== 'settings') await refreshCurrent();
|
||||
});
|
||||
|
||||
watch(currentNode, async () => {
|
||||
recipeModelPath.value = '';
|
||||
featuredRequest++;
|
||||
featuredLoading.value = false;
|
||||
featuredModels.value = [];
|
||||
featuredFamilies.value = [];
|
||||
featuredWeightProviders.value = [];
|
||||
featuredPagination.currentPage = 1;
|
||||
featuredPagination.total = 0;
|
||||
autoSync.value = undefined;
|
||||
@@ -904,6 +1007,14 @@ onMounted(() => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.complex-table__card-header) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.complex-table__card-status) {
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
:deep(.complex-table__card-value) {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -915,6 +1026,14 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.featured-provider {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
min-height: 20px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.featured-name-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
{{ $t('nginx.brotliManagedExternallyHelper') }}
|
||||
</el-alert>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="brotliManagedPending">
|
||||
<el-alert type="info" :closable="false" class="mb-2">
|
||||
{{ $t('nginx.brotliManagedPendingHelper') }}
|
||||
</el-alert>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="brotliManagedUnavailable">
|
||||
<el-alert type="warning" :closable="false" class="mb-2">
|
||||
{{ $t('nginx.brotliManagedUnavailableHelper') }}
|
||||
@@ -127,6 +132,8 @@ let loading = ref(false);
|
||||
// disappear together with the module.
|
||||
const brotliAvailable = ref(false);
|
||||
const brotliManagedExternally = ref(false);
|
||||
// Pending: nginx does not load the managed file yet, and saving wires it in.
|
||||
const brotliManagedPending = ref(false);
|
||||
const brotliManagedUnavailable = ref(false);
|
||||
|
||||
const variablesRules = reactive({
|
||||
@@ -201,6 +208,7 @@ const getBrotliParams = async () => {
|
||||
}
|
||||
const res = await getNginxConfigByScope<Nginx.NginxBrotliRes>({ scope: 'brotli' });
|
||||
brotliManagedExternally.value = !!res.data?.managedExternally;
|
||||
brotliManagedPending.value = !!res.data?.managedPending;
|
||||
brotliManagedUnavailable.value = !!res.data?.managedUnavailable;
|
||||
for (const param of res.data?.params ?? []) {
|
||||
if (param.params.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user