fix: Adjust the synchronization method for the Ollama model (#7895)

This commit is contained in:
ssongliu 2025-02-18 14:09:10 +08:00 committed by GitHub
parent e492c6573c
commit 39385ea0a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 958 additions and 173 deletions

View file

@ -32,6 +32,44 @@ func (b *BaseApi) CreateOllamaModel(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags AI
// @Summary Rereate Ollama model
// @Accept json
// @Param request body dto.OllamaModelName true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/ollama/model/recreate [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加模型重试 [name]","formatEN":"re-add Ollama model [name]"}
func (b *BaseApi) RecreateOllamaModel(c *gin.Context) {
var req dto.OllamaModelName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := AIToolService.Recreate(req.Name); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags AI
// @Summary Sync Ollama model list
// @Success 200 {array} dto.OllamaModelDropList
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/ollama/model/sync [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
list, err := AIToolService.Sync()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Page Ollama models
// @Accept json
@ -84,19 +122,19 @@ func (b *BaseApi) LoadOllamaModelDetail(c *gin.Context) {
// @Tags AI
// @Summary Delete Ollama model
// @Accept json
// @Param request body dto.OllamaModelName true "request"
// @Param request body dto.ForceDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/ollama/model/del [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除模型 [name]","formatEN":"remove Ollama model [name]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"ollama_models","output_column":"name","output_value":"name"}],"formatZH":"删除 ollama 模型 [name]","formatEN":"remove ollama model [name]"}
func (b *BaseApi) DeleteOllamaModel(c *gin.Context) {
var req dto.OllamaModelName
var req dto.ForceDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := AIToolService.Delete(req.Name); err != nil {
if err := AIToolService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}

View file

@ -1,9 +1,22 @@
package dto
import "time"
type OllamaModelInfo struct {
Name string `json:"name"`
Size string `json:"size"`
Modified string `json:"modified"`
ID uint `json:"id"`
Name string `json:"name"`
Size string `json:"size"`
From string `json:"from"`
LogFileExist bool `json:"logFileExist"`
Status string `json:"status"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
}
type OllamaModelDropList struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type OllamaModelName struct {

View file

@ -52,3 +52,8 @@ type OperationWithNameAndType struct {
Name string `json:"name"`
Type string `json:"type" validate:"required"`
}
type ForceDelete struct {
IDs []uint `json:"ids"`
ForceDelete bool `json:"forceDelete"`
}

11
backend/app/model/ai.go Normal file
View file

@ -0,0 +1,11 @@
package model
type OllamaModel struct {
BaseModel
Name string `json:"name"`
Size string `json:"size"`
From string `json:"from"`
Status string `json:"status"`
Message string `json:"message"`
}

69
backend/app/repo/ai.go Normal file
View file

@ -0,0 +1,69 @@
package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
)
type AiRepo struct{}
type IAiRepo interface {
Get(opts ...DBOption) (model.OllamaModel, error)
List(opts ...DBOption) ([]model.OllamaModel, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.OllamaModel, error)
Create(cronjob *model.OllamaModel) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
}
func NewIAiRepo() IAiRepo {
return &AiRepo{}
}
func (u *AiRepo) Get(opts ...DBOption) (model.OllamaModel, error) {
var item model.OllamaModel
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&item).Error
return item, err
}
func (u *AiRepo) List(opts ...DBOption) ([]model.OllamaModel, error) {
var list []model.OllamaModel
db := global.DB.Model(&model.OllamaModel{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&list).Error
return list, err
}
func (u *AiRepo) Page(page, size int, opts ...DBOption) (int64, []model.OllamaModel, error) {
var list []model.OllamaModel
db := global.DB.Model(&model.OllamaModel{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&list).Error
return count, list, err
}
func (u *AiRepo) Create(item *model.OllamaModel) error {
return global.DB.Create(item).Error
}
func (u *AiRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.OllamaModel{}).Where("id = ?", id).Updates(vars).Error
}
func (u *AiRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.OllamaModel{}).Error
}

View file

@ -12,10 +12,14 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type AIToolService struct{}
@ -23,7 +27,9 @@ type AIToolService struct{}
type IAIToolService interface {
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
Create(name string) error
Delete(name string) error
Recreate(name string) error
Delete(req dto.ForceDelete) error
Sync() ([]dto.OllamaModelDropList, error)
LoadDetail(name string) (string, error)
BindDomain(req dto.OllamaBindDomain) error
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
@ -35,78 +41,38 @@ func NewIAIToolService() IAIToolService {
}
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
return 0, nil, err
}
if ollamaBaseInfo.Status != constant.Running {
return 0, nil, nil
}
stdout, err := cmd.Execf("docker exec %s ollama list", ollamaBaseInfo.ContainerName)
if err != nil {
return 0, nil, err
}
var list []dto.OllamaModelInfo
modelMaps := make(map[string]struct{})
lines := strings.Split(stdout, "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 5 {
continue
}
if parts[0] == "NAME" {
continue
}
modelMaps[strings.ReplaceAll(parts[0], ":", "-")] = struct{}{}
list = append(list, dto.OllamaModelInfo{Name: parts[0], Size: parts[2] + " " + parts[3], Modified: strings.Join(parts[4:], " ")})
}
entries, _ := os.ReadDir(path.Join(global.CONF.System.DataDir, "log", "AITools"))
for _, item := range entries {
if _, ok := modelMaps[item.Name()]; ok {
continue
}
if _, ok := modelMaps[item.Name()+":latest"]; ok {
continue
}
list = append(list, dto.OllamaModelInfo{Name: item.Name(), Size: "-", Modified: "-"})
}
var options []repo.DBOption
if len(req.Info) != 0 {
length, count := len(list), 0
for count < length {
if !strings.Contains(list[count].Name, req.Info) {
list = append(list[:count], list[(count+1):]...)
length--
} else {
count++
}
}
options = append(options, commonRepo.WithLikeName(req.Info))
}
var records []dto.OllamaModelInfo
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]dto.OllamaModelInfo, 0)
} else {
if end >= total {
end = total
}
records = list[start:end]
total, list, err := aiRepo.Page(req.Page, req.PageSize, options...)
if err != nil {
return 0, nil, err
}
return int64(total), records, err
var dtoLists []dto.OllamaModelInfo
for _, itemModel := range list {
var item dto.OllamaModelInfo
if err := copier.Copy(&item, &itemModel); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
logPath := path.Join(global.CONF.System.DataDir, "log", "AITools", itemModel.Name)
if _, err := os.Stat(logPath); err == nil {
item.LogFileExist = true
}
dtoLists = append(dtoLists, item)
}
return int64(total), dtoLists, err
}
func (u *AIToolService) LoadDetail(name string) (string, error) {
if cmd.CheckIllegal(name) {
return "", buserr.New(constant.ErrCmdIllegal)
}
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
containerName, err := loadContainerName()
if err != nil {
return "", err
}
if ollamaBaseInfo.Status != constant.Running {
return "", nil
}
stdout, err := cmd.Execf("docker exec %s ollama show %s", ollamaBaseInfo.ContainerName, name)
stdout, err := cmd.Execf("docker exec %s ollama show %s", containerName, name)
if err != nil {
return "", err
}
@ -117,15 +83,52 @@ func (u *AIToolService) Create(name string) error {
if cmd.CheckIllegal(name) {
return buserr.New(constant.ErrCmdIllegal)
}
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
modelInfo, _ := aiRepo.Get(commonRepo.WithByName(name))
if modelInfo.ID != 0 {
return constant.ErrRecordExist
}
containerName, err := loadContainerName()
if err != nil {
return err
}
if ollamaBaseInfo.Status != constant.Running {
return nil
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", name)
if _, err := os.Stat(path.Dir(logItem)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(logItem), os.ModePerm); err != nil {
return err
}
}
fileName := strings.ReplaceAll(name, ":", "-")
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", fileName)
info := model.OllamaModel{
Name: name,
From: "local",
Status: constant.StatusWaiting,
}
if err := aiRepo.Create(&info); err != nil {
return err
}
file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
go pullOllamaModel(file, containerName, info)
return nil
}
func (u *AIToolService) Recreate(name string) error {
if cmd.CheckIllegal(name) {
return buserr.New(constant.ErrCmdIllegal)
}
modelInfo, _ := aiRepo.Get(commonRepo.WithByName(name))
if modelInfo.ID == 0 {
return constant.ErrRecordNotFound
}
containerName, err := loadContainerName()
if err != nil {
return err
}
if err := aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusWaiting, "from": "local"}); err != nil {
return err
}
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", name)
if _, err := os.Stat(path.Dir(logItem)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(logItem), os.ModePerm); err != nil {
return err
@ -135,40 +138,41 @@ func (u *AIToolService) Create(name string) error {
if err != nil {
return err
}
go func() {
defer file.Close()
cmd := exec.Command("docker", "exec", ollamaBaseInfo.ContainerName, "ollama", "run", name)
multiWriter := io.MultiWriter(os.Stdout, file)
cmd.Stdout = multiWriter
cmd.Stderr = multiWriter
if err := cmd.Run(); err != nil {
global.LOG.Errorf("ollama pull %s failed, err: %v", name, err)
_, _ = file.WriteString("ollama pull failed!")
return
}
global.LOG.Infof("ollama pull %s successful!", name)
_, _ = file.WriteString("ollama pull successful!")
}()
go pullOllamaModel(file, containerName, modelInfo)
return nil
}
func (u *AIToolService) Delete(name string) error {
if cmd.CheckIllegal(name) {
return buserr.New(constant.ErrCmdIllegal)
func (u *AIToolService) Delete(req dto.ForceDelete) error {
ollamaList, _ := aiRepo.List(commonRepo.WithIdsIn(req.IDs))
if len(ollamaList) == 0 {
return constant.ErrRecordNotFound
}
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
containerName, err := loadContainerName()
if err != nil && !req.ForceDelete {
return err
}
if ollamaBaseInfo.Status != constant.Running {
return nil
for _, item := range ollamaList {
stdout, err := cmd.Execf("docker exec %s ollama rm %s", containerName, item.Name)
if err != nil && !req.ForceDelete {
return fmt.Errorf("handle ollama rm %s failed, stdout: %s, err: %v", item.Name, stdout, err)
}
_ = aiRepo.Delete(commonRepo.WithByID(item.ID))
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", item.Name)
_ = os.Remove(logItem)
}
stdout, err := cmd.Execf("docker exec %s ollama list", ollamaBaseInfo.ContainerName)
return nil
}
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
containerName, err := loadContainerName()
if err != nil {
return err
return nil, err
}
isExist := false
stdout, err := cmd.Execf("docker exec %s ollama list", containerName)
if err != nil {
return nil, err
}
var list []model.OllamaModel
lines := strings.Split(stdout, "\n")
for _, line := range lines {
parts := strings.Fields(line)
@ -178,25 +182,33 @@ func (u *AIToolService) Delete(name string) error {
if parts[0] == "NAME" {
continue
}
if parts[0] == name {
isExist = true
break
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
}
listInDB, _ := aiRepo.List()
var dropList []dto.OllamaModelDropList
for _, itemModel := range listInDB {
isExit := false
for i := 0; i < len(list); i++ {
if list[i].Name == itemModel.Name {
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
list = append(list[:i], list[(i+1):]...)
isExit = true
break
}
}
if !isExit && itemModel.Status != constant.StatusWaiting {
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
continue
}
}
for _, item := range list {
item.Status = constant.StatusSuccess
item.From = "remote"
_ = aiRepo.Create(&item)
}
if isExist {
stdout, err := cmd.Execf("docker exec %s ollama rm %s", ollamaBaseInfo.ContainerName, name)
if err != nil {
return fmt.Errorf("handle ollama rm %s failed, stdout: %s, err: %v", name, stdout, err)
}
}
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", name)
_ = os.Remove(logItem)
logItem2 := path.Join(global.CONF.System.DataDir, "log", "AITools", strings.TrimSuffix(name, ":latest"))
if logItem2 != logItem {
_ = os.Remove(logItem2)
}
return nil
return dropList, nil
}
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
@ -318,3 +330,46 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
}
return nil
}
func loadContainerName() (string, error) {
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
return "", fmt.Errorf("ollama service is not found, err: %v", err)
}
if ollamaBaseInfo.Status != constant.Running {
return "", fmt.Errorf("container %s of ollama is not running, please check and retry!", ollamaBaseInfo.ContainerName)
}
return ollamaBaseInfo.ContainerName, nil
}
func pullOllamaModel(file *os.File, containerName string, info model.OllamaModel) {
defer file.Close()
cmd := exec.Command("docker", "exec", containerName, "ollama", "pull", info.Name)
multiWriter := io.MultiWriter(os.Stdout, file)
cmd.Stdout = multiWriter
cmd.Stderr = multiWriter
_ = cmd.Run()
itemSize, err := loadModelSize(info.Name, containerName)
if len(itemSize) != 0 {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusSuccess, "size": itemSize})
} else {
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
}
_, _ = file.WriteString("ollama pull completed!")
}
func loadModelSize(name string, containerName string) (string, error) {
stdout, err := cmd.Execf("docker exec %s ollama list | grep %s", containerName, name)
if err != nil {
return "", err
}
lines := strings.Split(string(stdout), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 5 {
continue
}
return parts[2] + " " + parts[3], nil
}
return "", fmt.Errorf("no such model %s in ollama list, std: %s", name, string(stdout))
}

View file

@ -12,6 +12,8 @@ var (
appInstallRepo = repo.NewIAppInstallRepo()
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
aiRepo = repo.NewIAiRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
databaseRepo = repo.NewIDatabaseRepo()

View file

@ -480,13 +480,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
case "image-pull", "image-push", "image-build", "compose-create":
logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
case "ollama-model":
fileName := strings.ReplaceAll(req.Name, ":", "-")
if _, err := os.Stat(fileName); err != nil {
if strings.HasSuffix(req.Name, ":latest") {
fileName = strings.TrimSuffix(req.Name, ":latest")
}
}
logFilePath = path.Join(global.CONF.System.DataDir, "log", "AITools", fileName)
logFilePath = path.Join(global.CONF.System.DataDir, "log", "AITools", req.Name)
}
lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest)

View file

@ -6,6 +6,7 @@ const (
StatusWaiting = "Waiting"
StatusSuccess = "Success"
StatusFailed = "Failed"
StatusDeleted = "Deleted"
StatusUploading = "Uploading"
StatusEnable = "Enable"
StatusDisable = "Disable"

View file

@ -102,6 +102,7 @@ func Init() {
migrations.UpdateAppTag,
migrations.UpdateApp,
migrations.AddOllamaModel,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -380,3 +380,13 @@ var UpdateApp = &gormigrate.Migration{
return nil
},
}
var AddOllamaModel = &gormigrate.Migration{
ID: "20250218-add-ollama-model",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.OllamaModel{}); err != nil {
return err
}
return nil
},
}

View file

@ -16,7 +16,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
aiToolsRouter.POST("/ollama/model/search", baseApi.SearchOllamaModel)
aiToolsRouter.POST("/ollama/model/sync", baseApi.SyncOllamaModel)
aiToolsRouter.POST("/ollama/model/load", baseApi.LoadOllamaModelDetail)
aiToolsRouter.POST("/ollama/model/del", baseApi.DeleteOllamaModel)
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)

View file

@ -185,7 +185,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OllamaModelName"
"$ref": "#/definitions/dto.ForceDelete"
}
}
],
@ -195,12 +195,21 @@ const docTemplate = `{
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
"BeforeFunctions": [
{
"db": "ollama_models",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"formatEN": "remove Ollama model [name]",
"formatZH": "删除模型 [name]",
"bodyKeys": [
"id"
],
"formatEN": "remove ollama model [name]",
"formatZH": "删除 ollama 模型 [name]",
"paramKeys": []
}
}
@ -243,6 +252,50 @@ const docTemplate = `{
}
}
},
"/ai/ollama/model/recreate": {
"post": {
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"consumes": [
"application/json"
],
"tags": [
"AI"
],
"summary": "Rereate Ollama model",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OllamaModelName"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "re-add Ollama model [name]",
"formatZH": "添加模型重试 [name]",
"paramKeys": []
}
}
},
"/ai/ollama/model/search": {
"post": {
"security": [
@ -281,6 +334,40 @@ const docTemplate = `{
}
}
},
"/ai/ollama/model/sync": {
"post": {
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"tags": [
"AI"
],
"summary": "Sync Ollama model list",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.OllamaModelDropList"
}
}
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "sync Ollama model list",
"formatZH": "同步 Ollama 模型列表",
"paramKeys": []
}
}
},
"/apps/checkupdate": {
"get": {
"security": [
@ -19234,6 +19321,20 @@ const docTemplate = `{
}
}
},
"dto.ForceDelete": {
"type": "object",
"properties": {
"forceDelete": {
"type": "boolean"
},
"ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"dto.ForwardRuleOperate": {
"type": "object",
"properties": {
@ -20583,6 +20684,17 @@ const docTemplate = `{
}
}
},
"dto.OllamaModelDropList": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"dto.OllamaModelName": {
"type": "object",
"properties": {

View file

@ -182,7 +182,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OllamaModelName"
"$ref": "#/definitions/dto.ForceDelete"
}
}
],
@ -192,12 +192,21 @@
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
"BeforeFunctions": [
{
"db": "ollama_models",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"formatEN": "remove Ollama model [name]",
"formatZH": "删除模型 [name]",
"bodyKeys": [
"id"
],
"formatEN": "remove ollama model [name]",
"formatZH": "删除 ollama 模型 [name]",
"paramKeys": []
}
}
@ -240,6 +249,50 @@
}
}
},
"/ai/ollama/model/recreate": {
"post": {
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"consumes": [
"application/json"
],
"tags": [
"AI"
],
"summary": "Rereate Ollama model",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OllamaModelName"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "re-add Ollama model [name]",
"formatZH": "添加模型重试 [name]",
"paramKeys": []
}
}
},
"/ai/ollama/model/search": {
"post": {
"security": [
@ -278,6 +331,40 @@
}
}
},
"/ai/ollama/model/sync": {
"post": {
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"tags": [
"AI"
],
"summary": "Sync Ollama model list",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.OllamaModelDropList"
}
}
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "sync Ollama model list",
"formatZH": "同步 Ollama 模型列表",
"paramKeys": []
}
}
},
"/apps/checkupdate": {
"get": {
"security": [
@ -19231,6 +19318,20 @@
}
}
},
"dto.ForceDelete": {
"type": "object",
"properties": {
"forceDelete": {
"type": "boolean"
},
"ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"dto.ForwardRuleOperate": {
"type": "object",
"properties": {
@ -20580,6 +20681,17 @@
}
}
},
"dto.OllamaModelDropList": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"dto.OllamaModelName": {
"type": "object",
"properties": {

View file

@ -1567,6 +1567,15 @@ definitions:
- type
- vars
type: object
dto.ForceDelete:
properties:
forceDelete:
type: boolean
ids:
items:
type: integer
type: array
type: object
dto.ForwardRuleOperate:
properties:
rules:
@ -2484,6 +2493,13 @@ definitions:
websiteID:
type: integer
type: object
dto.OllamaModelDropList:
properties:
id:
type: integer
name:
type: string
type: object
dto.OllamaModelName:
properties:
name:
@ -6579,7 +6595,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/dto.OllamaModelName'
$ref: '#/definitions/dto.ForceDelete'
responses:
"200":
description: OK
@ -6590,11 +6606,17 @@ paths:
tags:
- AI
x-panel-log:
BeforeFunctions: []
BeforeFunctions:
- db: ollama_models
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- name
formatEN: remove Ollama model [name]
formatZH: 删除模型 [name]
- id
formatEN: remove ollama model [name]
formatZH: 删除 ollama 模型 [name]
paramKeys: []
/ai/ollama/model/load:
post:
@ -6618,6 +6640,33 @@ paths:
summary: Page Ollama models
tags:
- AI
/ai/ollama/model/recreate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OllamaModelName'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Rereate Ollama model
tags:
- AI
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
formatEN: re-add Ollama model [name]
formatZH: 添加模型重试 [name]
paramKeys: []
/ai/ollama/model/search:
post:
consumes:
@ -6640,6 +6689,27 @@ paths:
summary: Page Ollama models
tags:
- AI
/ai/ollama/model/sync:
post:
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.OllamaModelDropList'
type: array
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Sync Ollama model list
tags:
- AI
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: sync Ollama model list
formatZH: 同步 Ollama 模型列表
paramKeys: []
/apps/{key}:
get:
consumes:

View file

@ -2,9 +2,18 @@ import { ReqPage } from '.';
export namespace AI {
export interface OllamaModelInfo {
id: number;
name: string;
size: string;
modified: string;
from: string;
logFileExist: boolean;
status: string;
message: string;
createdAt: Date;
}
export interface OllamaModelDropInfo {
id: number;
name: string;
}
export interface OllamaModelSearch extends ReqPage {
info: string;

View file

@ -5,8 +5,11 @@ import { ResPage } from '../interface';
export const createOllamaModel = (name: string) => {
return http.post(`/ai/ollama/model`, { name: name });
};
export const deleteOllamaModel = (name: string) => {
return http.post(`/ai/ollama/model/del`, { name: name });
export const recreateOllamaModel = (name: string) => {
return http.post(`/ai/ollama/model/recreate`, { name: name });
};
export const deleteOllamaModel = (ids: Array<number>, force: boolean) => {
return http.post(`/ai/ollama/model/del`, { ids: ids, forceDelete: force });
};
export const searchOllamaModel = (params: AI.OllamaModelSearch) => {
return http.post<ResPage<AI.OllamaModelInfo>>(`/ai/ollama/model/search`, params);
@ -14,6 +17,9 @@ export const searchOllamaModel = (params: AI.OllamaModelSearch) => {
export const loadOllamaModel = (name: string) => {
return http.post<string>(`/ai/ollama/model/load`, { name: name });
};
export const syncOllamaModel = () => {
return http.post<Array<AI.OllamaModelDropInfo>>(`/ai/ollama/model/sync`);
};
export const loadGPUInfo = () => {
return http.get<any>(`/ai/gpu/load`);

View file

@ -72,8 +72,7 @@ const stopSignals = [
'image pull successful!',
'image push failed!',
'image push successful!',
'ollama pull failed!',
'ollama pull successful!',
'ollama pull completed!',
];
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
const tailLog = ref(false);

View file

@ -600,6 +600,9 @@ const message = {
create_helper: 'Pull "{0}" from Ollama.com',
ollama_doc: 'You can visit the Ollama official website to search and find more models.',
container_conn_helper: 'Use this address for inter-container access or connection',
ollama_sync: 'Syncing Ollama model found the following models do not exist, do you want to delete them?',
from_remote: 'This model was not downloaded via 1Panel, no related pull logs.',
no_logs: 'The pull logs for this model have been deleted and cannot be viewed.',
},
gpu: {
gpu: 'GPU Monitor',

View file

@ -600,6 +600,9 @@ const message = {
create_helper: 'Ollama.com から "{0}" を取得',
ollama_doc: 'Ollama の公式ウェブサイトを訪れてさらに多くのモデルを検索して見つけることができます',
container_conn_helper: 'コンテナ間のアクセスまたは接続にこのアドレスを使用',
ollama_sync: 'Ollamaモデルの同期中に以下のモデルが存在しないことが判明しました削除しますか',
from_remote: 'このモデルは1Panelを介してダウンロードされておらず関連するプルログはありません',
no_logs: 'このモデルのプルログは削除されており関連するログを表示できません',
},
gpu: {
gpu: 'GPUモニター',

View file

@ -596,6 +596,9 @@ const message = {
create_helper: 'Ollama.com에서 "{0}" 가져오기',
ollama_doc: 'Ollama 공식 웹사이트를 방문하여 많은 모델을 검색하고 찾을 있습니다.',
container_conn_helper: '컨테이너 접근 또는 연결에 주소를 사용',
ollama_sync: 'Ollama 모델 동기화 다음 모델이 존재하지 않음을 발견했습니다. 삭제하시겠습니까?',
from_remote: ' 모델은 1Panel을 통해 다운로드되지 않았으며 관련 로그가 없습니다.',
no_logs: ' 모델의 로그가 삭제되어 관련 로그를 없습니다.',
},
gpu: {
gpu: 'GPU 모니터',

View file

@ -611,6 +611,10 @@ const message = {
create_helper: 'Tarik "{0}" dari Ollama.com',
ollama_doc: 'Anda boleh melawat laman web rasmi Ollama untuk mencari dan menemui lebih banyak model.',
container_conn_helper: 'Gunakan alamat ini untuk akses atau sambungan antara kontena',
ollama_sync:
'Sincronizando o modelo Ollama, encontrou que os seguintes modelos não existem, deseja excluí-los?',
from_remote: 'Este modelo não foi baixado via 1Panel, sem logs de pull relacionados.',
no_logs: 'Os logs de pull deste modelo foram excluídos e não podem ser visualizados.',
},
gpu: {
gpu: 'Monitor GPU',

View file

@ -608,6 +608,10 @@ const message = {
create_helper: 'Puxar "{0}" do Ollama.com',
ollama_doc: 'Você pode visitar o site oficial da Ollama para pesquisar e encontrar mais modelos.',
container_conn_helper: 'Use este endereço para acesso ou conexão entre contêineres',
ollama_sync:
'Menyelaraskan model Ollama mendapati model berikut tidak wujud, adakah anda ingin memadamnya?',
from_remote: 'Model ini tidak dimuat turun melalui 1Panel, tiada log pengambilan berkaitan.',
no_logs: 'Log pengambilan untuk model ini telah dipadam dan tidak dapat dilihat.',
},
gpu: {
gpu: 'Monitor de GPU',

View file

@ -606,6 +606,10 @@ const message = {
create_helper: 'Загрузить "{0}" с Ollama.com',
ollama_doc: 'Вы можете посетить официальный сайт Ollama, чтобы искать и находить больше моделей.',
container_conn_helper: 'Используйте этот адрес для доступа или подключения между контейнерами',
ollama_sync:
'Синхронизация модели Ollama обнаружила, что следующие модели не существуют, хотите удалить их?',
from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.',
no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.',
},
gpu: {
gpu: 'Мониторинг GPU',

View file

@ -581,6 +581,9 @@ const message = {
create_helper: ' Ollama.com 拉取 "{0}"',
ollama_doc: '您可以訪問 Ollama 官方網站搜索並查找更多模型',
container_conn_helper: '容器間訪問或連接使用此地址',
ollama_sync: '同步 Ollama 模型發現下列模型不存在是否刪除',
from_remote: '該模型並非通過 1Panel 下載無相關拉取日誌',
no_logs: '該模型的拉取日誌已被刪除無法查看相關日誌',
},
gpu: {
gpu: 'GPU 监控',

View file

@ -582,6 +582,9 @@ const message = {
create_helper: ' Ollama.com 拉取 "{0}"',
ollama_doc: '您可以访问 Ollama 官网搜索并查找更多模型',
container_conn_helper: '容器间访问或连接使用此地址',
ollama_sync: '同步 Ollama 模型发现下列模型不存在是否删除',
from_remote: '该模型并非通过 1Panel 下载无相关拉取日志',
no_logs: '该模型的拉取日志已被删除无法查看相关日志',
},
gpu: {
gpu: 'GPU 监控',

View file

@ -0,0 +1,118 @@
<template>
<div>
<el-dialog
v-model="open"
:title="$t('commons.button.sync')"
width="30%"
:close-on-click-modal="false"
@close="handleClose"
>
<div v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-alert class="mt-2" :show-icon="true" type="warning" :closable="false">
{{ $t('ai_tools.model.ollama_sync') }}
</el-alert>
<el-checkbox
class="mt-2"
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('setting.all') }}
</el-checkbox>
<el-checkbox-group v-model="checkedItems" @change="handleCheckedChange">
<el-checkbox
v-for="(item, index) in list"
:key="index"
:label="item.name"
:value="item.id"
/>
</el-checkbox-group>
</el-col>
</el-row>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose()" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onConfirm" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { AI } from '@/api/interface/ai';
import { deleteOllamaModel } from '@/api/modules/ai';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { CheckboxValueType } from 'element-plus';
import { onMounted, ref } from 'vue';
defineOptions({ name: 'OpDialog' });
const checkAll = ref(false);
const isIndeterminate = ref(true);
const checkedItems = ref([]);
const list = ref([]);
const loading = ref();
const open = ref();
interface DialogProps {
list: Array<AI.OllamaModelDropInfo>;
}
const acceptParams = (props: DialogProps): void => {
list.value = props.list;
checkAll.value = true;
handleCheckAllChange(true);
open.value = true;
};
const emit = defineEmits(['search']);
const handleCheckAllChange = (val: CheckboxValueType) => {
checkedItems.value = [];
if (val) {
for (const item of list.value) {
checkedItems.value.push(item.id);
}
}
isIndeterminate.value = false;
};
const handleCheckedChange = (value: CheckboxValueType[]) => {
const checkedCount = value.length;
checkAll.value = checkedCount === list.value.length;
isIndeterminate.value = checkedCount > 0 && checkedCount < list.value.length;
};
const onConfirm = async () => {
loading.value = true;
await deleteOllamaModel(checkedItems.value, true)
.then(() => {
emit('search');
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
open.value = false;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
};
const handleClose = () => {
emit('search');
open.value = false;
};
onMounted(() => {});
defineExpose({
acceptParams,
});
</script>

View file

@ -33,6 +33,9 @@
<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"
@ -42,6 +45,9 @@
>
OpenWebUI
</el-button>
<el-button plain :disabled="selects.length === 0" type="primary" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</div>
<div>
<TableSearch @search="search()" v-model:searchName="searchName" />
@ -51,36 +57,61 @@
<template #main v-if="modelInfo.isExist">
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:class="{ mask: maskShow }"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="90">
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column :label="$t('ai_tools.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)"
>
<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" />
<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 === '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.name)" link type="primary">
<el-button @click="onLoadLog(row)" link type="primary">
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.createdAt')" prop="modified" />
<el-table-column
min-width="100"
:label="$t('commons.table.createdAt')"
prop="createdAt"
:formatter="dateFormat"
/>
<fu-table-operations
:ellipsis="mobile ? 0 : 10"
:min-width="mobile ? 'auto' : 400"
:min-width="mobile ? 'auto' : 100"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
@ -116,8 +147,21 @@
</template>
</el-dialog>
<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" />
<Log ref="logRef" @close="search" />
<Del ref="delRef" @search="search" />
<Conn ref="connRef" />
<CodemirrorDialog ref="detailRef" />
<PortJumpDialog ref="dialogPortJumpRef" />
@ -129,6 +173,7 @@
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 Del from '@/views/ai/model/del/index.vue';
import Log from '@/components/log-dialog/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import CodemirrorDialog from '@/components/codemirror-dialog/index.vue';
@ -136,19 +181,28 @@ import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { App } from '@/api/interface/app';
import { GlobalStore } from '@/store';
import { deleteOllamaModel, loadOllamaModel, searchOllamaModel } from '@/api/modules/ai';
import {
deleteOllamaModel,
loadOllamaModel,
recreateOllamaModel,
searchOllamaModel,
syncOllamaModel,
} from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import { GetAppPort } from '@/api/modules/app';
import { dateFormat } from '@/utils/util';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
import { MsgInfo, MsgSuccess } from '@/utils/message';
import BindDomain from '@/views/ai/model/domain/index.vue';
const globalStore = GlobalStore();
const loading = ref(false);
const selects = ref<any>([]);
const maskShow = ref(true);
const addRef = ref();
const logRef = ref();
const detailRef = ref();
const delRef = ref();
const connRef = ref();
const openWebUIPort = ref();
const dashboardVisible = ref(false);
@ -165,6 +219,10 @@ const paginationConfig = reactive({
const searchName = ref();
const appInstallID = ref(0);
const opRef = ref();
const operateIDs = ref();
const forceDelete = ref();
const modelInfo = reactive({
status: '',
container: '',
@ -177,6 +235,10 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
function selectable(row) {
return row.status !== 'Waiting';
}
const search = async () => {
let params = {
page: paginationConfig.currentPage,
@ -199,6 +261,23 @@ 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 });
};
@ -246,35 +325,87 @@ const checkExist = (data: App.CheckInstalled) => {
}
};
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;
await recreateOllamaModel(name)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const onDelete = async (row: AI.OllamaModelInfo) => {
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.button.delete'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await deleteOllamaModel(row.name)
.then(() => {
loading.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
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('cronjob.cronTask'),
i18n.global.t('commons.button.delete'),
]),
api: null,
params: null,
});
};
const onLoadLog = (name: string) => {
logRef.value.acceptParams({ id: 0, type: 'ollama-model', name: name, tail: true });
const onLoadLog = (row: AI.OllamaModelInfo) => {
if (row.from === 'remote') {
MsgInfo(i18n.global.t('ai_tools.model.from_remote'));
return;
}
if (!row.logFileExist) {
MsgInfo(i18n.global.t('ai_tools.model.no_logs'));
return;
}
logRef.value.acceptParams({ id: 0, type: 'ollama-model', name: row.name, tail: true });
};
const buttons = [
{
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 !== 'Success';
},
},
];