feat: ai model support tensorRT LLM (#10688)

This commit is contained in:
CityFun 2025-10-18 22:49:48 +08:00 committed by GitHub
parent 05d125c762
commit 17224b8920
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1525 additions and 463 deletions

View file

@ -17,8 +17,9 @@ var (
appInstallService = service.NewIAppInstalledService()
appIgnoreUpgradeService = service.NewIAppIgnoreUpgradeService()
aiToolService = service.NewIAIToolService()
mcpServerService = service.NewIMcpServerService()
aiToolService = service.NewIAIToolService()
mcpServerService = service.NewIMcpServerService()
tensorrtLLMService = service.NewITensorRTLLMService()
containerService = service.NewIContainerService()
composeTemplateService = service.NewIComposeTemplateService()

View file

@ -0,0 +1,68 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
var req request.TensorRTLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list := tensorrtLLMService.Page(req)
helper.SuccessWithData(c, list)
}
func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := tensorrtLLMService.Create(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := tensorrtLLMService.Update(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := tensorrtLLMService.Delete(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func (b *BaseApi) OperateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := tensorrtLLMService.Operate(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}

View file

@ -0,0 +1,32 @@
package request
import "github.com/1Panel-dev/1Panel/agent/app/dto"
type TensorRTLLMSearch struct {
dto.PageInfo
Name string `json:"name"`
}
type TensorRTLLMCreate struct {
Name string `json:"name" validate:"required"`
ContainerName string `json:"containerName"`
Port int `json:"port" validate:"required"`
Version string `json:"version" validate:"required"`
ModelDir string `json:"modelDir" validate:"required"`
Model string `json:"model" validate:"required"`
HostIP string `json:"hostIP"`
}
type TensorRTLLMUpdate struct {
ID uint `json:"id" validate:"required"`
TensorRTLLMCreate
}
type TensorRTLLMDelete struct {
ID uint `json:"id" validate:"required"`
}
type TensorRTLLMOperate struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
}

View file

@ -0,0 +1,16 @@
package response
import "github.com/1Panel-dev/1Panel/agent/app/model"
type TensorRTLLMsRes struct {
Items []TensorRTLLMDTO `json:"items"`
Total int64 `json:"total"`
}
type TensorRTLLMDTO struct {
model.TensorRTLLM
Version string `json:"version"`
Model string `json:"model"`
Dir string `json:"dir"`
ModelDir string `json:"modelDir"`
}

View file

@ -0,0 +1,13 @@
package model
type TensorRTLLM struct {
BaseModel
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
TaskID string `json:"taskID"`
}

View file

@ -0,0 +1,56 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
type TensorRTLLMRepo struct {
}
type ITensorRTLLMRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.TensorRTLLM, error)
GetFirst(opts ...DBOption) (*model.TensorRTLLM, error)
Create(tensorrtLLM *model.TensorRTLLM) error
Save(tensorrtLLM *model.TensorRTLLM) error
DeleteBy(opts ...DBOption) error
List(opts ...DBOption) ([]model.TensorRTLLM, error)
}
func NewITensorRTLLMRepo() ITensorRTLLMRepo {
return &TensorRTLLMRepo{}
}
func (t TensorRTLLMRepo) Page(page, size int, opts ...DBOption) (int64, []model.TensorRTLLM, error) {
var servers []model.TensorRTLLM
db := getDb(opts...).Model(&model.TensorRTLLM{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
return count, servers, err
}
func (t TensorRTLLMRepo) GetFirst(opts ...DBOption) (*model.TensorRTLLM, error) {
var tensorrtLLM model.TensorRTLLM
if err := getDb(opts...).First(&tensorrtLLM).Error; err != nil {
return nil, err
}
return &tensorrtLLM, nil
}
func (t TensorRTLLMRepo) List(opts ...DBOption) ([]model.TensorRTLLM, error) {
var tensorrtLLMs []model.TensorRTLLM
if err := getDb(opts...).Find(&tensorrtLLMs).Error; err != nil {
return nil, err
}
return tensorrtLLMs, nil
}
func (t TensorRTLLMRepo) Create(tensorrtLLM *model.TensorRTLLM) error {
return getDb().Create(tensorrtLLM).Error
}
func (t TensorRTLLMRepo) Save(tensorrtLLM *model.TensorRTLLM) error {
return getDb().Save(tensorrtLLM).Error
}
func (t TensorRTLLMRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.TensorRTLLM{}).Error
}

View file

@ -12,8 +12,9 @@ var (
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()

View file

@ -10,7 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/mcp"
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@ -565,7 +565,7 @@ func handleEnv(mcpServer *model.McpServer) gotenv.Env {
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
var composeContent []byte
if mcpServer.ID == 0 {
composeContent = mcp.DefaultMcpCompose
composeContent = ai.DefaultMcpCompose
} else {
composeContent = []byte(mcpServer.DockerCompose)
}

View file

@ -0,0 +1,305 @@
package service
import (
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
"path"
"strconv"
)
type TensorRTLLMService struct{}
type ITensorRTLLMService interface {
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes
Create(create request.TensorRTLLMCreate) error
Update(req request.TensorRTLLMUpdate) error
Delete(id uint) error
Operate(req request.TensorRTLLMOperate) error
}
func NewITensorRTLLMService() ITensorRTLLMService {
return &TensorRTLLMService{}
}
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes {
var (
res response.TensorRTLLMsRes
items []response.TensorRTLLMDTO
)
total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data {
_ = syncTensorRTLLMContainerStatus(&item)
serverDTO := response.TensorRTLLMDTO{
TensorRTLLM: item,
}
env, _ := gotenv.Unmarshal(item.Env)
serverDTO.Version = env["VERSION"]
serverDTO.Model = env["MODEL_NAME"]
serverDTO.ModelDir = env["MODEL_PATH"]
serverDTO.Dir = path.Join(global.Dir.TensorRTLLMDir, item.Name)
items = append(items, serverDTO)
}
res.Total = total
res.Items = items
return res
}
func handleLLMParams(llm *model.TensorRTLLM) error {
var composeContent []byte
if llm.ID == 0 {
composeContent = ai.DefaultTensorrtLLMCompose
} else {
composeContent = []byte(llm.DockerCompose)
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
return err
}
services, serviceValid := composeMap["services"].(map[string]interface{})
if !serviceValid {
return buserr.New("ErrFileParse")
}
serviceName := ""
serviceValue := make(map[string]interface{})
if llm.ID > 0 {
serviceName = llm.Name
serviceValue = services[serviceName].(map[string]interface{})
} else {
for name, service := range services {
serviceName = name
serviceValue = service.(map[string]interface{})
break
}
delete(services, serviceName)
}
services[llm.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
llm.DockerCompose = string(composeByte)
return nil
}
func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gotenv.Env {
env := make(gotenv.Env)
env["CONTAINER_NAME"] = create.ContainerName
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(llm.Port)
env["MODEL_PATH"] = create.ModelDir
env["MODEL_NAME"] = create.Model
env["VERSION"] = create.Version
if create.HostIP != "" {
env["HOST_IP"] = create.HostIP
} else {
env["HOST_IP"] = ""
}
envStr, _ := gotenv.Marshal(env)
llm.Env = envStr
return env
}
func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
servers, _ := tensorrtLLMRepo.List()
for _, server := range servers {
if server.Port == create.Port {
return buserr.New("ErrPortInUsed")
}
if server.ContainerName == create.ContainerName {
return buserr.New("ErrContainerName")
}
if server.Name == create.Name {
return buserr.New("ErrNameIsExist")
}
}
if err := checkPortExist(create.Port); err != nil {
return err
}
if err := checkContainerName(create.ContainerName); err != nil {
return err
}
tensorrtLLMDir := path.Join(global.Dir.TensorRTLLMDir, create.Name)
filesOP := files.NewFileOp()
if !filesOP.Stat(tensorrtLLMDir) {
_ = filesOP.CreateDir(tensorrtLLMDir, 0644)
}
tensorrtLLM := &model.TensorRTLLM{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Status: constant.StatusStarting,
}
if err := handleLLMParams(tensorrtLLM); err != nil {
return err
}
env := handleLLMEnv(tensorrtLLM, create)
llmDir := path.Join(global.Dir.TensorRTLLMDir, create.Name)
envPath := path.Join(llmDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(llmDir, "docker-compose.yml")
if err := filesOP.SaveFile(dockerComposePath, tensorrtLLM.DockerCompose, 0644); err != nil {
return err
}
tensorrtLLM.Status = constant.StatusStarting
if err := tensorrtLLMRepo.Create(tensorrtLLM); err != nil {
return err
}
go startTensorRTLLM(tensorrtLLM)
return nil
}
func (t TensorRTLLMService) Update(req request.TensorRTLLMUpdate) error {
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if tensorrtLLM.Port != req.Port {
if err := checkPortExist(req.Port); err != nil {
return err
}
}
if tensorrtLLM.ContainerName != req.ContainerName {
if err := checkContainerName(req.ContainerName); err != nil {
return err
}
}
tensorrtLLM.ContainerName = req.ContainerName
tensorrtLLM.Port = req.Port
if err := handleLLMParams(tensorrtLLM); err != nil {
return err
}
newEnv, err := gotenv.Unmarshal(tensorrtLLM.Env)
if err != nil {
return err
}
newEnv["CONTAINER_NAME"] = req.ContainerName
newEnv["PANEL_APP_PORT_HTTP"] = strconv.Itoa(tensorrtLLM.Port)
newEnv["MODEL_PATH"] = req.ModelDir
newEnv["MODEL_NAME"] = req.Model
newEnv["VERSION"] = req.Version
if req.HostIP != "" {
newEnv["HOST_IP"] = req.HostIP
} else {
newEnv["HOST_IP"] = ""
}
envStr, _ := gotenv.Marshal(newEnv)
tensorrtLLM.Env = envStr
llmDir := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name)
envPath := path.Join(llmDir, ".env")
if err := gotenv.Write(newEnv, envPath); err != nil {
return err
}
tensorrtLLM.Status = constant.StatusStarting
if err := tensorrtLLMRepo.Save(tensorrtLLM); err != nil {
return err
}
go startTensorRTLLM(tensorrtLLM)
return nil
}
func (t TensorRTLLMService) Delete(id uint) error {
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(id))
if err != nil {
return err
}
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name))
return tensorrtLLMRepo.DeleteBy(repo.WithByID(id))
}
func (t TensorRTLLMService) Operate(req request.TensorRTLLMOperate) error {
tensorrtLLM, err := tensorrtLLMRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
var out string
switch req.Operate {
case "start":
out, err = compose.Up(composePath)
tensorrtLLM.Status = constant.StatusRunning
case "stop":
out, err = compose.Down(composePath)
tensorrtLLM.Status = constant.StatusStopped
case "restart":
out, err = compose.Restart(composePath)
tensorrtLLM.Status = constant.StatusRunning
}
if err != nil {
tensorrtLLM.Status = constant.StatusError
tensorrtLLM.Message = out
}
return tensorrtLLMRepo.Save(tensorrtLLM)
}
func startTensorRTLLM(tensorrtLLM *model.TensorRTLLM) {
composePath := path.Join(global.Dir.TensorRTLLMDir, tensorrtLLM.Name, "docker-compose.yml")
if tensorrtLLM.Status != constant.StatusNormal {
_, _ = compose.Down(composePath)
}
if out, err := compose.Up(composePath); err != nil {
tensorrtLLM.Status = constant.StatusError
tensorrtLLM.Message = out
} else {
tensorrtLLM.Status = constant.StatusRunning
tensorrtLLM.Message = ""
}
_ = syncTensorRTLLMContainerStatus(tensorrtLLM)
}
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
containerNames := []string{tensorrtLLM.ContainerName}
cli, err := docker.NewClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
if len(containers) == 0 {
if tensorrtLLM.Status == constant.StatusStarting {
return nil
}
tensorrtLLM.Status = constant.StatusStopped
return tensorrtLLMRepo.Save(tensorrtLLM)
}
container := containers[0]
switch container.State {
case "exited":
tensorrtLLM.Status = constant.StatusError
case "running":
tensorrtLLM.Status = constant.StatusRunning
case "paused":
tensorrtLLM.Status = constant.StatusStopped
case "restarting":
tensorrtLLM.Status = constant.StatusRestarting
default:
if tensorrtLLM.Status != constant.StatusStarting {
tensorrtLLM.Status = constant.StatusStopped
}
}
return tensorrtLLMRepo.Save(tensorrtLLM)
}

View file

@ -1,4 +1,4 @@
package mcp
package ai
import (
_ "embed"
@ -6,3 +6,6 @@ import (
//go:embed compose.yml
var DefaultMcpCompose []byte
//go:embed llm-compose.yml
var DefaultTensorrtLLMCompose []byte

View file

@ -0,0 +1,32 @@
services:
tensorrt-llm:
image: nvcr.io/nvidia/tensorrt-llm/release:${VERSION}
container_name: ${CONTAINER_NAME}
restart: always
runtime: nvidia
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
networks:
- 1panel-network
volumes:
- ${MODEL_PATH}:/models
ports:
- ${PANEL_APP_PORT_HTTP}:8000
ipc: host
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65535
hard: 65535
stack: 67108864
command: bach -c "trtllm-serve /models/${MODEL_NAME} --host 0.0.0.0 --port 8000"
networks:
1panel-network:
external: true

View file

@ -46,6 +46,7 @@ type SystemDir struct {
SSLLogDir string
McpDir string
ConvertLogDir string
TensorRTLLMDir string
}
type LogConfig struct {

View file

@ -33,4 +33,5 @@ func Init() {
global.Dir.SSLLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/ssl"))
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
global.Dir.ConvertLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/convert"))
global.Dir.TensorRTLLMDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/ai/tensorrt_llm"))
}

View file

@ -46,6 +46,7 @@ func InitAgentDB() {
migrations.AddTimeoutForClam,
migrations.UpdateCronjobSpec,
migrations.UpdateWebsiteSSLAddColumn,
migrations.AddTensorRTLLMModel,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -627,3 +627,10 @@ var UpdateWebsiteSSLAddColumn = &gormigrate.Migration{
return nil
},
}
var AddTensorRTLLMModel = &gormigrate.Migration{
ID: "20251018-add-tensorrt-llm-model",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.TensorRTLLM{})
},
}

View file

@ -33,5 +33,11 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain)
aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain)
aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain)
aiToolsRouter.POST("/tensorrt/search", baseApi.PageTensorRTLLMs)
aiToolsRouter.POST("/tensorrt/create", baseApi.CreateTensorRTLLM)
aiToolsRouter.POST("/tensorrt/update", baseApi.UpdateTensorRTLLM)
aiToolsRouter.POST("/tensorrt/delete", baseApi.DeleteTensorRTLLM)
aiToolsRouter.POST("/tensorrt/operate", baseApi.OperateTensorRTLLM)
}
}

View file

@ -183,4 +183,35 @@ export namespace AI {
containerName: string;
environments: Environment[];
}
export interface TensorRTLLM {
id?: number;
name: string;
containerName: string;
port: number;
version: string;
modelDir: string;
model: string;
hostIP: string;
status?: string;
message?: string;
createdAt?: string;
}
export interface TensorRTLLMDTO extends TensorRTLLM {
dir?: string;
}
export interface TensorRTLLMSearch extends ReqPage {
name: string;
}
export interface TensorRTLLMDelete {
id: number;
}
export interface TensorRTLLMOperate {
id: number;
operate: string;
}
}

View file

@ -71,3 +71,23 @@ export const getMcpDomain = () => {
export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => {
return http.post(`/ai/mcp/domain/update`, req);
};
export const pageTensorRTLLM = (req: AI.TensorRTLLMSearch) => {
return http.post<ResPage<AI.TensorRTLLMDTO>>(`/ai/tensorrt/search`, req);
};
export const createTensorRTLLM = (req: AI.TensorRTLLM) => {
return http.post(`/ai/tensorrt/create`, req);
};
export const updateTensorRTLLM = (req: AI.TensorRTLLM) => {
return http.post(`/ai/tensorrt/update`, req);
};
export const deleteTensorRTLLM = (req: AI.TensorRTLLMDelete) => {
return http.post(`/ai/tensorrt/delete`, req);
};
export const operateTensorRTLLM = (req: AI.TensorRTLLMOperate) => {
return http.post(`/ai/tensorrt/operate`, req);
};

View file

@ -724,6 +724,10 @@ const message = {
npxHelper: 'Suitable for mcp started with npx or binary',
uvxHelper: 'Suitable for mcp started with uvx',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Model Directory',
},
},
container: {
create: 'Create',

View file

@ -725,6 +725,10 @@ const message = {
npxHelper: 'Adecuado para mcp iniciado con npx o binario',
uvxHelper: 'Adecuado para mcp iniciado con uvx',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Directorio del Modelo',
},
},
container: {
create: 'Crear',

View file

@ -712,6 +712,10 @@ const message = {
npxHelper: 'npx またはバイナリで起動する mcp に適しています',
uvxHelper: 'uvx で起動する mcp に適しています',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'モデルディレクトリ',
},
},
container: {
create: 'コンテナを作成します',

View file

@ -708,6 +708,10 @@ const message = {
npxHelper: 'npx 또는 바이너리로 시작하는 mcp에 적합',
uvxHelper: 'uvx로 시작하는 mcp에 적합',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: '모델 디렉토리',
},
},
container: {
create: '컨테이너 만들기',

View file

@ -725,6 +725,10 @@ const message = {
npxHelper: 'Sesuai untuk mcp yang dimulakan dengan npx atau binari',
uvxHelper: 'Sesuai untuk mcp yang dimulakan dengan uvx',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Direktori Model',
},
},
container: {
create: 'Cipta kontena',

View file

@ -721,6 +721,10 @@ const message = {
npxHelper: 'Adequado para mcp iniciado com npx ou binário',
uvxHelper: 'Adequado para mcp iniciado com uvx',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Diretório do Modelo',
},
},
container: {
create: 'Criar contêiner',

View file

@ -719,6 +719,10 @@ const message = {
npxHelper: 'Подходит для mcp, запущенного с помощью npx или бинарного файла',
uvxHelper: 'Подходит для mcp, запущенного с помощью uvx',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Каталог модели',
},
},
container: {
create: 'Создать контейнер',

View file

@ -733,6 +733,10 @@ const message = {
npxHelper: 'npx veya ikili dosya ile başlatılan mcp için uygundur',
uvxHelper: 'uvx ile başlatılan mcp için uygundur',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: 'Model Dizini',
},
},
container: {
create: 'Oluştur',

View file

@ -698,6 +698,10 @@ const message = {
npxHelper: '適合 npx 或者 二進制啟動的 mcp',
uvxHelper: '適合 uvx 啟動的 mcp',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: '模型目錄',
},
},
container: {
create: '建立容器',

View file

@ -697,6 +697,10 @@ const message = {
npxHelper: '适合 npx 或者 二进制启动的 mcp',
uvxHelper: '适合 uvx 启动的 mcp',
},
tensorRT: {
llm: 'TensorRT LLM',
modelDir: '模型目录',
},
},
container: {
create: '创建容器',

View file

@ -12,9 +12,9 @@ const databaseRouter = {
},
children: [
{
path: '/ai/model',
path: '/ai/model/ollama',
name: 'OllamaModel',
component: () => import('@/views/ai/model/index.vue'),
component: () => import('@/views/ai/model/ollama/index.vue'),
meta: {
icon: 'p-moxing-menu',
title: 'aiTools.model.model',
@ -41,6 +41,17 @@ const databaseRouter = {
requiresAuth: true,
},
},
{
path: '/ai/model/tensorrt',
hidden: true,
name: 'TensorRTLLm',
component: () => import('@/views/ai/model/tensorrt/index.vue'),
meta: {
title: 'aiTools.tensorRT.llm',
activeMenu: '/ai/model/ollama',
requiresAuth: true,
},
},
],
};

View file

@ -1,467 +1,21 @@
<template>
<div v-loading="loading">
<RouterButton
:buttons="[
{
label: i18n.global.t('aiTools.model.model'),
path: '/ai-tools/model',
},
]"
/>
<LayoutContent title="Ollama">
<template #app>
<AppStatus
app-key="ollama"
v-model:loading="loading"
:hide-setting="true"
v-model:mask-show="maskShow"
v-model:appInstallID="appInstallID"
@is-exist="checkExist"
ref="appStatusRef"
></AppStatus>
</template>
<template #prompt>
<el-alert type="info" :closable="false">
<template #title>
<span>{{ $t('runtime.systemRestartHelper') }}</span>
</template>
</el-alert>
</template>
<template #leftToolBar>
<el-button :disabled="modelInfo.status !== 'Running'" type="primary" @click="onCreate()">
{{ $t('aiTools.model.create') }}
</el-button>
<el-button plain type="primary" :disabled="modelInfo.status !== 'Running'" @click="bindDomain">
{{ $t('aiTools.proxy.proxy') }}
</el-button>
<el-button :disabled="modelInfo.status !== 'Running'" @click="onLoadConn" type="primary" plain>
{{ $t('database.databaseConnInfo') }}
</el-button>
<el-button :disabled="modelInfo.status !== 'Running'" type="primary" plain @click="onSync()">
{{ $t('database.loadFromRemote') }}
</el-button>
<el-button
:disabled="modelInfo.status !== 'Running'"
icon="Position"
@click="goDashboard()"
type="primary"
plain
>
OpenWebUI
</el-button>
<el-button plain :disabled="selects.length === 0" type="primary" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<template #rightToolBar>
<TableSearch @search="search()" v-model:searchName="searchName" />
<TableRefresh @search="search()" />
<TableSetting title="model-refresh" @search="search()" />
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:class="{ mask: maskShow }"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column :label="$t('aiTools.model.model')" prop="name" min-width="90">
<template #default="{ row }">
<el-text v-if="row.size" type="primary" class="cursor-pointer" @click="onLoad(row.name)">
{{ row.name }}
</el-text>
<span v-else>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" prop="size">
<template #default="{ row }">
<span>{{ row.size || '-' }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === 'Success'" type="success">
{{ $t('commons.status.success') }}
</el-tag>
<el-tag v-if="row.status === 'Deleted'" type="info">
{{ $t('database.isDelete') }}
</el-tag>
<el-tag v-if="row.status === 'Canceled'" type="danger">
{{ $t('commons.status.systemrestart') }}
</el-tag>
<el-tag v-if="row.status === 'Failed'" type="danger">
{{ $t('commons.status.failed') }}
</el-tag>
<el-tag v-if="row.status === 'Waiting'">
<el-icon v-if="row.status === 'Waiting'" class="is-loading">
<Loading />
</el-icon>
{{ $t('commons.status.waiting') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('commons.button.log')">
<template #default="{ row }">
<el-button @click="onLoadLog(row)" link type="primary">
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column
min-width="80"
:label="$t('commons.table.date')"
prop="createdAt"
:formatter="dateFormat"
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 10"
:min-width="mobile ? 'auto' : 200"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
<el-card v-if="modelInfo.status != 'Running' && !loading && maskShow" class="mask-prompt">
<span v-if="modelInfo.isExist">
{{ $t('commons.service.serviceNotStarted', ['Ollama']) }}
</span>
<span v-else>
{{ $t('app.checkInstalledWarn', ['Ollama']) }}
<el-button @click="goInstall('ollama')" link icon="Position" type="primary">
{{ $t('database.goInstall') }}
</el-button>
</span>
</el-card>
</template>
<div>
<RouterButton :buttons="buttons" />
<LayoutContent>
<router-view></router-view>
</LayoutContent>
<DialogPro v-model="dashboardVisible" :title="$t('app.checkTitle')" size="mini">
<div class="flex justify-center items-center gap-2 flex-wrap">
{{ $t('app.checkInstalledWarn', ['OpenWebUI']) }}
<el-link icon="Position" @click="goInstall('ollama-webui')" type="primary">
{{ $t('database.goInstall') }}
</el-link>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dashboardVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</DialogPro>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
<template #content>
<el-form class="mt-4 mb-1" ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="forceDelete" :label="$t('website.forceDelete')" />
<span class="input-help">
{{ $t('website.forceDeleteHelper') }}
</span>
</el-form-item>
</el-form>
</template>
</OpDialog>
<AddDialog ref="addRef" @search="search" @log="onLoadLog" />
<Del ref="delRef" @search="search" />
<Terminal ref="terminalRef" />
<Conn ref="connRef" />
<CodemirrorDrawer ref="detailRef" />
<PortJumpDialog ref="dialogPortJumpRef" />
<BindDomain ref="bindDomainRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div>
</template>
<script lang="ts" setup>
import AppStatus from '@/components/app-status/index.vue';
import AddDialog from '@/views/ai/model/add/index.vue';
import Conn from '@/views/ai/model/conn/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import Terminal from '@/views/ai/model/terminal/index.vue';
import Del from '@/views/ai/model/del/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { App } from '@/api/interface/app';
import { GlobalStore } from '@/store';
import {
deleteOllamaModel,
loadOllamaModel,
recreateOllamaModel,
searchOllamaModel,
syncOllamaModel,
} from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import { getAppPort } from '@/api/modules/app';
import { dateFormat, newUUID } from '@/utils/util';
import { MsgInfo, MsgSuccess } from '@/utils/message';
import BindDomain from '@/views/ai/model/domain/index.vue';
import { routerToNameWithQuery } from '@/utils/router';
const globalStore = GlobalStore();
const loading = ref(false);
const selects = ref<any>([]);
const maskShow = ref(false);
const addRef = ref();
const detailRef = ref();
const delRef = ref();
const connRef = ref();
const terminalRef = ref();
const openWebUIPort = ref();
const dashboardVisible = ref(false);
const dialogPortJumpRef = ref();
const appStatusRef = ref();
const bindDomainRef = ref();
const taskLogRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'model-page-size',
currentPage: 1,
pageSize: Number(localStorage.getItem('page-size')) || 20,
total: 0,
});
const searchName = ref();
const appInstallID = ref(0);
const opRef = ref();
const operateIDs = ref();
const forceDelete = ref();
const modelInfo = reactive({
status: '',
container: '',
isExist: null,
version: '',
port: 11434,
});
const mobile = computed(() => {
return globalStore.isMobile();
});
function selectable(row) {
return row.status !== 'Waiting';
}
const search = async () => {
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
info: searchName.value,
};
loading.value = true;
await searchOllamaModel(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const onCreate = async () => {
addRef.value.acceptParams();
};
const onSync = async () => {
loading.value = true;
await syncOllamaModel()
.then((res) => {
loading.value = false;
if (res.data) {
delRef.value.acceptParams({ list: res.data });
} else {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
}
})
.catch(() => {
loading.value = false;
});
};
const onLoadConn = async () => {
connRef.value.acceptParams({
port: modelInfo.port,
containerName: modelInfo.container,
appinstallID: appInstallID.value,
});
};
const onLoad = async (name: string) => {
const res = await loadOllamaModel(name);
let detailInfo = res.data;
let param = {
header: i18n.global.t('commons.button.view'),
detailInfo: detailInfo,
mode: 'json',
};
detailRef.value!.acceptParams(param);
};
const goDashboard = async () => {
if (openWebUIPort.value === 0) {
dashboardVisible.value = true;
return;
}
dialogPortJumpRef.value.acceptParams({ port: openWebUIPort.value });
};
const bindDomain = () => {
bindDomainRef.value.acceptParams(appInstallID.value);
};
const goInstall = (name: string) => {
routerToNameWithQuery('AppAll', { install: name });
};
const loadWebUIPort = async () => {
const res = await getAppPort('ollama-webui', '');
openWebUIPort.value = res.data;
};
const checkExist = (data: App.CheckInstalled) => {
modelInfo.isExist = data.isExist;
modelInfo.status = data.status;
modelInfo.version = data.version;
modelInfo.container = data.containerName;
modelInfo.port = data.httpPort;
if (modelInfo.isExist && modelInfo.status === 'Running') {
search();
}
};
const onSubmitDelete = async () => {
loading.value = true;
await deleteOllamaModel(operateIDs.value, forceDelete.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const onReCreate = async (name: string) => {
loading.value = true;
let taskID = newUUID();
await recreateOllamaModel(name, taskID)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
openTaskLog(taskID);
search();
})
.catch(() => {
loading.value = false;
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onDelete = async (row: AI.OllamaModelInfo) => {
let names = [];
let ids = [];
if (row) {
ids = [row.id];
names = [row.name];
} else {
for (const item of selects.value) {
names.push(item.name);
ids.push(item.id);
}
}
operateIDs.value = ids;
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('aiTools.model.model'),
i18n.global.t('commons.button.delete'),
]),
api: null,
params: null,
});
};
const onLoadLog = (row: any) => {
if (row.taskID) {
openTaskLog(row.taskID);
}
if (row.from === 'remote') {
MsgInfo(i18n.global.t('aiTools.model.from_remote'));
return;
}
if (!row.logFileExist) {
MsgInfo(i18n.global.t('aiTools.model.no_logs'));
return;
}
taskLogRef.value.openWithResourceID('AI', 'TaskPull', row.id);
};
const buttons = [
{
label: i18n.global.t('commons.button.run'),
click: (row: AI.OllamaModelInfo) => {
terminalRef.value.acceptParams({ name: row.name });
},
disabled: (row: any) => {
return row.status !== 'Success';
},
label: 'Ollama',
path: '/ai/model/ollama',
},
{
label: i18n.global.t('commons.button.retry'),
click: (row: AI.OllamaModelInfo) => {
onReCreate(row.name);
},
disabled: (row: any) => {
return row.status === 'Success' || row.status === 'Waiting';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.OllamaModelInfo) => {
onDelete(row);
},
disabled: (row: any) => {
return row.status === 'Waiting';
},
label: 'TensorRT LLM',
path: '/ai/model/tensorrt',
},
];
onMounted(() => {
loadWebUIPort();
});
</script>
<style lang="scss" scoped>
.iconInTable {
margin-left: 5px;
margin-top: 3px;
}
.jumpAdd {
margin-top: 10px;
margin-left: 15px;
margin-bottom: 5px;
font-size: 12px;
}
.tagClass {
float: right;
font-size: 12px;
margin-top: 5px;
}
</style>

View file

@ -0,0 +1,461 @@
<template>
<div v-loading="loading">
<RouterMenu />
<LayoutContent title="Ollama">
<template #app>
<AppStatus
app-key="ollama"
v-model:loading="loading"
:hide-setting="true"
v-model:mask-show="maskShow"
v-model:appInstallID="appInstallID"
@is-exist="checkExist"
ref="appStatusRef"
></AppStatus>
</template>
<template #prompt>
<el-alert type="info" :closable="false">
<template #title>
<span>{{ $t('runtime.systemRestartHelper') }}</span>
</template>
</el-alert>
</template>
<template #leftToolBar>
<el-button :disabled="modelInfo.status !== 'Running'" type="primary" @click="onCreate()">
{{ $t('aiTools.model.create') }}
</el-button>
<el-button plain type="primary" :disabled="modelInfo.status !== 'Running'" @click="bindDomain">
{{ $t('aiTools.proxy.proxy') }}
</el-button>
<el-button :disabled="modelInfo.status !== 'Running'" @click="onLoadConn" type="primary" plain>
{{ $t('database.databaseConnInfo') }}
</el-button>
<el-button :disabled="modelInfo.status !== 'Running'" type="primary" plain @click="onSync()">
{{ $t('database.loadFromRemote') }}
</el-button>
<el-button
:disabled="modelInfo.status !== 'Running'"
icon="Position"
@click="goDashboard()"
type="primary"
plain
>
OpenWebUI
</el-button>
<el-button plain :disabled="selects.length === 0" type="primary" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<template #rightToolBar>
<TableSearch @search="search()" v-model:searchName="searchName" />
<TableRefresh @search="search()" />
<TableSetting title="model-refresh" @search="search()" />
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:class="{ mask: maskShow }"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column :label="$t('aiTools.model.model')" prop="name" min-width="90">
<template #default="{ row }">
<el-text v-if="row.size" type="primary" class="cursor-pointer" @click="onLoad(row.name)">
{{ row.name }}
</el-text>
<span v-else>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" prop="size">
<template #default="{ row }">
<span>{{ row.size || '-' }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === 'Success'" type="success">
{{ $t('commons.status.success') }}
</el-tag>
<el-tag v-if="row.status === 'Deleted'" type="info">
{{ $t('database.isDelete') }}
</el-tag>
<el-tag v-if="row.status === 'Canceled'" type="danger">
{{ $t('commons.status.systemrestart') }}
</el-tag>
<el-tag v-if="row.status === 'Failed'" type="danger">
{{ $t('commons.status.failed') }}
</el-tag>
<el-tag v-if="row.status === 'Waiting'">
<el-icon v-if="row.status === 'Waiting'" class="is-loading">
<Loading />
</el-icon>
{{ $t('commons.status.waiting') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('commons.button.log')">
<template #default="{ row }">
<el-button @click="onLoadLog(row)" link type="primary">
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column
min-width="80"
:label="$t('commons.table.date')"
prop="createdAt"
:formatter="dateFormat"
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 10"
:min-width="mobile ? 'auto' : 200"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
<el-card v-if="modelInfo.status != 'Running' && !loading && maskShow" class="mask-prompt">
<span v-if="modelInfo.isExist">
{{ $t('commons.service.serviceNotStarted', ['Ollama']) }}
</span>
<span v-else>
{{ $t('app.checkInstalledWarn', ['Ollama']) }}
<el-button @click="goInstall('ollama')" link icon="Position" type="primary">
{{ $t('database.goInstall') }}
</el-button>
</span>
</el-card>
</template>
</LayoutContent>
<DialogPro v-model="dashboardVisible" :title="$t('app.checkTitle')" size="mini">
<div class="flex justify-center items-center gap-2 flex-wrap">
{{ $t('app.checkInstalledWarn', ['OpenWebUI']) }}
<el-link icon="Position" @click="goInstall('ollama-webui')" type="primary">
{{ $t('database.goInstall') }}
</el-link>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dashboardVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</DialogPro>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
<template #content>
<el-form class="mt-4 mb-1" ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="forceDelete" :label="$t('website.forceDelete')" />
<span class="input-help">
{{ $t('website.forceDeleteHelper') }}
</span>
</el-form-item>
</el-form>
</template>
</OpDialog>
<AddDialog ref="addRef" @search="search" @log="onLoadLog" />
<Del ref="delRef" @search="search" />
<Terminal ref="terminalRef" />
<Conn ref="connRef" />
<CodemirrorDrawer ref="detailRef" />
<PortJumpDialog ref="dialogPortJumpRef" />
<BindDomain ref="bindDomainRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div>
</template>
<script lang="ts" setup>
import AppStatus from '@/components/app-status/index.vue';
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
import Conn from '@/views/ai/model/ollama/conn/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import Terminal from '@/views/ai/model/ollama/terminal/index.vue';
import Del from '@/views/ai/model/ollama/del/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import RouterMenu from '@/views/ai/model/index.vue';
import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { App } from '@/api/interface/app';
import { GlobalStore } from '@/store';
import {
deleteOllamaModel,
loadOllamaModel,
recreateOllamaModel,
searchOllamaModel,
syncOllamaModel,
} from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import { getAppPort } from '@/api/modules/app';
import { dateFormat, newUUID } from '@/utils/util';
import { MsgInfo, MsgSuccess } from '@/utils/message';
import BindDomain from '@/views/ai/model/ollama/domain/index.vue';
import { routerToNameWithQuery } from '@/utils/router';
const globalStore = GlobalStore();
const loading = ref(false);
const selects = ref<any>([]);
const maskShow = ref(false);
const addRef = ref();
const detailRef = ref();
const delRef = ref();
const connRef = ref();
const terminalRef = ref();
const openWebUIPort = ref();
const dashboardVisible = ref(false);
const dialogPortJumpRef = ref();
const appStatusRef = ref();
const bindDomainRef = ref();
const taskLogRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'model-page-size',
currentPage: 1,
pageSize: Number(localStorage.getItem('page-size')) || 20,
total: 0,
});
const searchName = ref();
const appInstallID = ref(0);
const opRef = ref();
const operateIDs = ref();
const forceDelete = ref();
const modelInfo = reactive({
status: '',
container: '',
isExist: null,
version: '',
port: 11434,
});
const mobile = computed(() => {
return globalStore.isMobile();
});
function selectable(row) {
return row.status !== 'Waiting';
}
const search = async () => {
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
info: searchName.value,
};
loading.value = true;
await searchOllamaModel(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const onCreate = async () => {
addRef.value.acceptParams();
};
const onSync = async () => {
loading.value = true;
await syncOllamaModel()
.then((res) => {
loading.value = false;
if (res.data) {
delRef.value.acceptParams({ list: res.data });
} else {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
}
})
.catch(() => {
loading.value = false;
});
};
const onLoadConn = async () => {
connRef.value.acceptParams({
port: modelInfo.port,
containerName: modelInfo.container,
appinstallID: appInstallID.value,
});
};
const onLoad = async (name: string) => {
const res = await loadOllamaModel(name);
let detailInfo = res.data;
let param = {
header: i18n.global.t('commons.button.view'),
detailInfo: detailInfo,
mode: 'json',
};
detailRef.value!.acceptParams(param);
};
const goDashboard = async () => {
if (openWebUIPort.value === 0) {
dashboardVisible.value = true;
return;
}
dialogPortJumpRef.value.acceptParams({ port: openWebUIPort.value });
};
const bindDomain = () => {
bindDomainRef.value.acceptParams(appInstallID.value);
};
const goInstall = (name: string) => {
routerToNameWithQuery('AppAll', { install: name });
};
const loadWebUIPort = async () => {
const res = await getAppPort('ollama-webui', '');
openWebUIPort.value = res.data;
};
const checkExist = (data: App.CheckInstalled) => {
modelInfo.isExist = data.isExist;
modelInfo.status = data.status;
modelInfo.version = data.version;
modelInfo.container = data.containerName;
modelInfo.port = data.httpPort;
if (modelInfo.isExist && modelInfo.status === 'Running') {
search();
}
};
const onSubmitDelete = async () => {
loading.value = true;
await deleteOllamaModel(operateIDs.value, forceDelete.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const onReCreate = async (name: string) => {
loading.value = true;
let taskID = newUUID();
await recreateOllamaModel(name, taskID)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
openTaskLog(taskID);
search();
})
.catch(() => {
loading.value = false;
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onDelete = async (row: AI.OllamaModelInfo) => {
let names = [];
let ids = [];
if (row) {
ids = [row.id];
names = [row.name];
} else {
for (const item of selects.value) {
names.push(item.name);
ids.push(item.id);
}
}
operateIDs.value = ids;
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('aiTools.model.model'),
i18n.global.t('commons.button.delete'),
]),
api: null,
params: null,
});
};
const onLoadLog = (row: any) => {
if (row.taskID) {
openTaskLog(row.taskID);
}
if (row.from === 'remote') {
MsgInfo(i18n.global.t('aiTools.model.from_remote'));
return;
}
if (!row.logFileExist) {
MsgInfo(i18n.global.t('aiTools.model.no_logs'));
return;
}
taskLogRef.value.openWithResourceID('AI', 'TaskPull', row.id);
};
const buttons = [
{
label: i18n.global.t('commons.button.run'),
click: (row: AI.OllamaModelInfo) => {
terminalRef.value.acceptParams({ name: row.name });
},
disabled: (row: any) => {
return row.status !== 'Success';
},
},
{
label: i18n.global.t('commons.button.retry'),
click: (row: AI.OllamaModelInfo) => {
onReCreate(row.name);
},
disabled: (row: any) => {
return row.status === 'Success' || row.status === 'Waiting';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.OllamaModelInfo) => {
onDelete(row);
},
disabled: (row: any) => {
return row.status === 'Waiting';
},
},
];
onMounted(() => {
loadWebUIPort();
});
</script>
<style lang="scss" scoped>
.iconInTable {
margin-left: 5px;
margin-top: 3px;
}
.jumpAdd {
margin-top: 10px;
margin-left: 15px;
margin-bottom: 5px;
font-size: 12px;
}
.tagClass {
float: right;
font-size: 12px;
margin-top: 5px;
}
</style>

View file

@ -0,0 +1,224 @@
<template>
<div>
<RouterMenu />
<LayoutContent>
<template #leftToolBar>
<div class="flex flex-wrap gap-3">
<el-button type="primary" @click="openCreate">
{{ $t('commons.button.create') }}
</el-button>
</div>
</template>
<template #rightToolBar>
<TableRefresh @search="search()" />
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@search="search"
v-loading="loading"
>
<el-table-column
:label="$t('commons.table.name')"
min-width="120"
prop="name"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.table.port')" min-width="80" prop="port" />
<el-table-column :label="$t('app.version')" min-width="100" prop="version" show-overflow-tooltip />
<el-table-column
:label="$t('aiTools.model.model')"
min-width="120"
prop="model"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="status">
<template #default="{ row }">
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-table-column>
<el-table-column :label="$t('commons.button.log')" width="120px">
<template #default="{ row }">
<el-button
@click="openLog(row)"
link
type="primary"
:disabled="
row.status !== 'Running' && row.status !== 'Rrror' && row.status !== 'Restarting'
"
>
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
width="180"
fix
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 2"
:min-width="mobile ? 'auto' : 200"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OpDialog ref="opRef" @search="search" />
<OperateDialog @search="search" ref="dialogRef" />
<ComposeLogs ref="composeLogRef" />
</div>
</template>
<script lang="ts" setup>
import ComposeLogs from '@/components/log/compose/index.vue';
import OperateDialog from './operate/index.vue';
import RouterMenu from '@/views/ai/model/index.vue';
import { reactive, onMounted, ref } from 'vue';
import { dateFormat } from '@/utils/util';
import { AI } from '@/api/interface/ai';
import { deleteTensorRTLLM, operateTensorRTLLM, pageTensorRTLLM } from '@/api/modules/ai';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const mobile = computed(() => {
return globalStore.isMobile();
});
const loading = ref();
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
const searchName = ref();
const opRef = ref();
const dialogRef = ref();
const composeLogRef = ref();
const search = async () => {
const params = {
name: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
loading.value = true;
await pageTensorRTLLM(params)
.then((res) => {
data.value = res.data.items || [];
paginationConfig.total = res.data.total || 0;
})
.finally(() => {
loading.value = false;
});
};
const openCreate = () => {
dialogRef.value.openCreate();
};
const openEdit = (row: AI.TensorRTLLM) => {
dialogRef.value.openEdit(row);
};
const openLog = (row: AI.McpServer) => {
composeLogRef.value.acceptParams({
compose: row.dir + '/docker-compose.yml',
resource: row.name,
container: row.containerName,
});
};
const operate = async (row: AI.TensorRTLLM, operation: string) => {
ElMessageBox.confirm(
i18n.global.t('commons.msg.operatorHelper', ['LLM', i18n.global.t('commons.operate.' + operation)]),
i18n.global.t('commons.operate.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
loading.value = true;
await operateTensorRTLLM({ id: row.id, operate: operation })
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
});
};
const deleteLLM = async (row: AI.TensorRTLLM) => {
try {
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: [row.name],
msg: i18n.global.t('commons.msg.operatorHelper', ['LLM', i18n.global.t('commons.button.delete')]),
api: deleteTensorRTLLM,
params: { id: row.id },
});
} catch (error) {}
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: AI.TensorRTLLM) => {
openEdit(row);
},
},
{
label: i18n.global.t('commons.button.start'),
disabled: (row: AI.TensorRTLLM) => {
return row.status === 'running';
},
click: (row: AI.TensorRTLLM) => {
operate(row, 'start');
},
},
{
label: i18n.global.t('commons.button.stop'),
disabled: (row: AI.TensorRTLLM) => {
return row.status !== 'running';
},
click: (row: AI.TensorRTLLM) => {
operate(row, 'stop');
},
},
{
label: i18n.global.t('commons.button.restart'),
disabled: (row: AI.TensorRTLLM) => {
return row.status !== 'running';
},
click: (row: AI.TensorRTLLM) => {
operate(row, 'restart');
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.TensorRTLLM) => {
deleteLLM(row);
},
},
];
onMounted(() => {
search();
});
</script>

View file

@ -0,0 +1,178 @@
<template>
<DrawerPro :header="$t('commons.button.' + mode)" v-model="drawerVisiable" size="large" @close="handleClose">
<el-form ref="formRef" label-position="top" :model="tensorRTLLM" :rules="rules" v-loading="loading">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model.trim="tensorRTLLM.name" :disabled="mode == 'edit'" />
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="tensorRTLLM.containerName"></el-input>
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-input v-model.trim="tensorRTLLM.version" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input v-model.number="tensorRTLLM.port" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="$t('app.allowPort')" prop="hostIP">
<el-switch
v-model="tensorRTLLM.hostIP"
:active-value="'0.0.0.0'"
:inactive-value="'127.0.0.1'"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('aiTools.tensorRT.modelDir')" prop="modelDir">
<el-input v-model="tensorRTLLM.modelDir">
<template #prepend>
<el-button icon="Folder" @click="modelDirRef.acceptParams({ dir: true })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('aiTools.model.model')" prop="model">
<el-input v-model="tensorRTLLM.model">
<template #prepend>
<el-button
icon="Folder"
@click="modelRef.acceptParams({ path: tensorRTLLM.modelDir, dir: true })"
/>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
<FileList ref="modelDirRef" @choose="getModelDir" />
<FileList ref="modelRef" @choose="getModelPath" />
</DrawerPro>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, FormInstance } from 'element-plus';
import DrawerPro from '@/components/drawer-pro/index.vue';
import FileList from '@/components/file-list/index.vue';
// import { AI } from '@/api/interface/ai';
import { createTensorRTLLM, updateTensorRTLLM } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
// interface DialogProps {
// rowData?: AI.TensorRTLLM;
// }
const mode = ref('create');
const drawerVisiable = ref(false);
const newTensorRTLLM = () => {
return {
name: '',
containerName: '',
port: 8000,
version: 'latest',
modelDir: '',
model: '',
hostIP: '',
};
};
const modelDirRef = ref();
const modelRef = ref();
const tensorRTLLM = ref(newTensorRTLLM());
const emit = defineEmits(['search']);
const openCreate = (): void => {
mode.value = 'create';
drawerVisiable.value = true;
};
const openEdit = (rowData: any): void => {
mode.value = 'edit';
tensorRTLLM.value = { ...rowData };
drawerVisiable.value = true;
};
const handleClose = () => {
drawerVisiable.value = false;
};
const getModelDir = (path: string) => {
tensorRTLLM.value.modelDir = path;
};
const getModelPath = (path: string) => {
const modelDir = tensorRTLLM.value.modelDir;
if (modelDir && path.startsWith(modelDir)) {
tensorRTLLM.value.model = path.replace(modelDir, '').replace(/^[\/\\]+/, '');
} else {
tensorRTLLM.value.model = path;
}
};
const rules = reactive({
name: [Rules.requiredInput],
port: [Rules.requiredInput],
version: [Rules.requiredInput],
modelDir: [Rules.requiredInput],
model: [Rules.requiredInput],
containerName: [Rules.requiredInput],
});
const formRef = ref<FormInstance>();
const onSubmit = async () => {
formRef.value?.validate(async (valid) => {
if (!valid) return;
loading.value = true;
if (mode.value === 'edit') {
await updateTensorRTLLM(tensorRTLLM.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
return;
}
await createTensorRTLLM(tensorRTLLM.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
});
};
watch(
() => tensorRTLLM.value.name,
(newVal) => {
if (newVal && mode.value == 'create') {
tensorRTLLM.value.containerName = newVal;
}
},
{ deep: true },
);
defineExpose({
openCreate,
openEdit,
});
</script>