fix: align AI menu order and labels (#14192)

This commit is contained in:
ssongliu
2026-10-26 14:03:40 +08:00
committed by GitHub
parent d7355f3ccc
commit fbf66766c2
34 changed files with 505 additions and 73 deletions
+12
View File
@@ -114,6 +114,18 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
})
}
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
var req dto.OllamaModelAccountSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := aiToolService.SyncAccount(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Page Ollama models
// @Accept json
+6
View File
@@ -24,6 +24,12 @@ type OllamaModelName struct {
TaskID string `json:"taskID"`
}
type OllamaModelAccountSync struct {
ID uint `json:"id" validate:"required"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
}
type OllamaBindDomain struct {
Domain string `json:"domain" validate:"required"`
AppInstallID uint `json:"appInstallID" validate:"required"`
+7 -3
View File
@@ -23,6 +23,7 @@ type AppInstallCreate struct {
Services map[string]string `json:"services"`
TaskID string `json:"taskID"`
SyncModelAccount *bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
@@ -127,9 +128,12 @@ type AppInstallDelete struct {
}
type AppInstalledUpdate struct {
InstallId uint `json:"installId" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"`
TaskID string `json:"-"`
SyncModelAccount *bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
InstallId uint `json:"installId" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"`
TaskID string `json:"-"`
AppContainerConfig
}
+23 -15
View File
@@ -50,19 +50,21 @@ type TagDTO struct {
}
type AppInstalledCheck struct {
IsExist bool `json:"isExist"`
Name string `json:"name"`
App string `json:"app"`
Version string `json:"version"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
LastBackupAt string `json:"lastBackupAt"`
AppInstallID uint `json:"appInstallId"`
ContainerName string `json:"containerName"`
InstallPath string `json:"installPath"`
HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"`
WebsiteDir string `json:"websiteDir"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
IsExist bool `json:"isExist"`
Name string `json:"name"`
App string `json:"app"`
Version string `json:"version"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
LastBackupAt string `json:"lastBackupAt"`
AppInstallID uint `json:"appInstallId"`
ContainerName string `json:"containerName"`
InstallPath string `json:"installPath"`
HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"`
WebsiteDir string `json:"websiteDir"`
}
type AppDetailDTO struct {
@@ -184,7 +186,13 @@ type AppParam struct {
}
type AppConfig struct {
Params []AppParam `json:"params"`
RawCompose string `json:"rawCompose"`
AppKey string `json:"appKey"`
Name string `json:"name"`
HttpPort int `json:"httpPort"`
SyncModelAccount bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
Params []AppParam `json:"params"`
RawCompose string `json:"rawCompose"`
request.AppContainerConfig
}
+4
View File
@@ -28,6 +28,10 @@ type AppInstall struct {
Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder" gorm:"default:0"`
SyncModelAccount bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
App App `json:"app" gorm:"-:migration"`
}
+32 -1
View File
@@ -117,6 +117,13 @@ func SyncOllamaModelAccount(createIfMissing bool) error {
if err != nil {
return err
}
if !install.SyncModelAccount {
return nil
}
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
}
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
if err != nil {
return err
@@ -125,10 +132,34 @@ func SyncOllamaModelAccount(createIfMissing bool) error {
for _, item := range models {
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
}
_, err = SyncLocalModelAccount(install, 0, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), items, false, createIfMissing)
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
return err
}
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
addressType := install.ModelAccountBaseURLType
if addressType == "" {
addressType = "container"
}
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
if errors.Is(err, gorm.ErrRecordNotFound) {
return addressType, install.ModelAccountBaseURL, nil
}
if err != nil {
return "", "", err
}
if addressType != "custom" {
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
if err != nil {
return "", "", err
}
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
addressType = "custom"
}
}
return addressType, account.BaseURL, nil
}
func CleanupLocalModelAccount(installID uint) {
if installID == 0 {
return
+22
View File
@@ -35,6 +35,7 @@ type IAIToolService interface {
Recreate(req dto.OllamaModelName) error
Delete(req dto.ForceDelete) error
Sync() ([]dto.OllamaModelDropList, error)
SyncAccount(req dto.OllamaModelAccountSync) error
LoadDetail(name string) (string, error)
BindDomain(req dto.OllamaBindDomain) error
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
@@ -259,6 +260,27 @@ func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
return dropList, SyncOllamaModelAccount(false)
}
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if install.App.Key != "ollama" {
return buserr.New("ErrInvalidParams")
}
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
if err != nil {
return err
}
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
return err
}
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
"model_account_base_url_type": req.ModelAccountBaseURLType,
"model_account_base_url": baseURL,
}, repo.WithByID(install.ID))
}
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
+9 -1
View File
@@ -501,6 +501,14 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
modelAccountBaseURL := ""
if app.Key == "ollama" {
appInstall.SyncModelAccount = req.SyncModelAccount == nil || *req.SyncModelAccount
appInstall.ModelAccountBaseURLType = req.ModelAccountBaseURLType
if appInstall.ModelAccountBaseURLType == "" {
appInstall.ModelAccountBaseURLType = "container"
}
appInstall.ModelAccountBaseURL = req.ModelAccountBaseURL
}
if app.Key == "ollama" && appInstall.SyncModelAccount {
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
if err != nil {
return
@@ -617,7 +625,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err
}
updateToolApp(appInstall)
if app.Key == "ollama" {
if app.Key == "ollama" && appInstall.SyncModelAccount {
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
return err
}
+49
View File
@@ -159,6 +159,14 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
}
res.ContainerName = appInstall.ContainerName
res.ModelAccountBaseURLType = appInstall.ModelAccountBaseURLType
res.ModelAccountBaseURL = appInstall.ModelAccountBaseURL
if app.Key == "ollama" {
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(appInstall)
if err != nil {
return nil, err
}
}
res.Name = appInstall.Name
res.Version = appInstall.Version
res.CreatedAt = appInstall.CreatedAt
@@ -438,6 +446,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
}
}
modelAccountBaseURL := ""
if installed.App.Key == "ollama" {
if req.SyncModelAccount != nil {
installed.SyncModelAccount = *req.SyncModelAccount
}
if req.ModelAccountBaseURLType != "" {
installed.ModelAccountBaseURLType = req.ModelAccountBaseURLType
installed.ModelAccountBaseURL = req.ModelAccountBaseURL
} else if installed.SyncModelAccount {
installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(oldInstalled)
if err != nil {
return err
}
}
if installed.SyncModelAccount {
containerName := installed.ContainerName
if req.Advanced && req.ContainerName != "" {
containerName = req.ContainerName
}
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, containerName, "11434", installed.HttpPort)
if err != nil {
return err
}
}
}
backupDockerCompose := installed.DockerCompose
if req.Advanced {
composeMap := make(map[string]interface{})
@@ -563,6 +597,9 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
}
if installed.App.Key == "ollama" && installed.SyncModelAccount {
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
}
return nil
}
@@ -934,6 +971,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
config := getAppCommonConfig(envs)
config.DockerCompose = install.DockerCompose
res.Params = params
res.AppKey = install.App.Key
res.Name = install.Name
res.HttpPort = install.HttpPort
res.SyncModelAccount = install.SyncModelAccount
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
res.ModelAccountBaseURL = install.ModelAccountBaseURL
if install.App.Key == "ollama" {
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
if err != nil {
return nil, err
}
}
if config.ContainerName == "" {
config.ContainerName = install.ContainerName
}
+1
View File
@@ -118,6 +118,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddNativeAgentInstalls,
migrations.AddQuickDirectory,
migrations.AddLocalModelAccountLink,
migrations.AddOllamaModelAccountSync,
}
}
+36 -2
View File
@@ -1899,9 +1899,43 @@ var AddQuickDirectory = &gormigrate.Migration{
var AddLocalModelAccountLink = &gormigrate.Migration{
ID: "20261023-add-local-model-account-link",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.AgentAccount{}); err != nil {
return tx.AutoMigrate(&model.AgentAccount{})
},
}
var AddOllamaModelAccountSync = &gormigrate.Migration{
ID: "20261026-add-ollama-model-account-sync",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.AppInstall{}); err != nil {
return err
}
return service.SyncOllamaModelAccount(true)
var accounts []model.AgentAccount
if err := tx.Where("provider = ? AND app_install_id > 0", "ollama").Find(&accounts).Error; err != nil {
return err
}
for _, account := range accounts {
var install model.AppInstall
if err := tx.Preload("App").First(&install, account.AppInstallID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
if install.App.Key != "ollama" {
continue
}
addressType := "custom"
if strings.TrimRight(account.BaseURL, "/") == fmt.Sprintf("http://%s:11434/v1", install.ContainerName) {
addressType = "container"
}
if err := tx.Model(&model.AppInstall{}).Where("id = ?", account.AppInstallID).Updates(map[string]interface{}{
"sync_model_account": true,
"model_account_base_url_type": addressType,
"model_account_base_url": account.BaseURL,
}).Error; err != nil {
return err
}
}
return nil
},
}
+1
View File
@@ -19,6 +19,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
aiToolsRouter.POST("/ollama/model/search", baseApi.SearchOllamaModel)
aiToolsRouter.POST("/ollama/model/sync", baseApi.SyncOllamaModel)
aiToolsRouter.POST("/ollama/account/sync", baseApi.SyncOllamaModelAccount)
aiToolsRouter.POST("/ollama/model/load", baseApi.LoadOllamaModelDetail)
aiToolsRouter.POST("/ollama/model/del", baseApi.DeleteOllamaModel)
aiToolsRouter.POST("/domain/bind", baseApi.BindDomain)
+5
View File
@@ -1,6 +1,11 @@
import { ReqPage } from '.';
export namespace AI {
export interface ModelAccountSync {
id: number;
modelAccountBaseURLType: 'container' | 'localhost' | 'systemIP' | 'custom';
modelAccountBaseURL: string;
}
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent' | 'deepseek-harness' | 'octop';
export interface OllamaModelInfo {
+9
View File
@@ -136,6 +136,7 @@ export namespace App {
}
export interface AppInstall {
syncModelAccount?: boolean;
modelAccountBaseURLType?: string;
modelAccountBaseURL?: string;
appDetailId: number;
@@ -229,6 +230,8 @@ export namespace App {
name: string;
version: string;
isExist: boolean;
modelAccountBaseURLType: string;
modelAccountBaseURL: string;
app: string;
status: string;
createdAt: string;
@@ -303,6 +306,12 @@ export namespace App {
}
export interface AppConfig {
appKey: string;
name: string;
httpPort: number;
syncModelAccount: boolean;
modelAccountBaseURLType: string;
modelAccountBaseURL: string;
params: InstallParams[];
rawCompose?: string;
cpuQuota: number;
+4
View File
@@ -22,6 +22,10 @@ export const loadOllamaModel = (name: string) => {
export const syncOllamaModel = () => {
return http.post<Array<AI.OllamaModelDropInfo>>(`/ai/ollama/model/sync`);
};
export const syncOllamaModelAccount = (req: AI.ModelAccountSync) => {
return http.post('/ai/ollama/account/sync', req);
};
export const closeOllamaModel = (name: string, currentNode?: string) => {
return http.post(
`/ai/ollama/close`,
+1
View File
@@ -1063,6 +1063,7 @@ const message = {
vllmCommandPortHelper:
'The startup command must use port {0}; otherwise, the service will be inaccessible.',
ascendVisibleDevices: 'Ascend visible devices (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sync to model account',
modelAccountAddressHelper:
'Container address is for agent containers; 127.0.0.1, system IP, or custom address is for AI Gateway or external services.',
containerAddress: 'Container address',
+1
View File
@@ -1066,6 +1066,7 @@ const message = {
vllmCommandPortHelper:
'El comando de inicio debe usar el puerto {0}; de lo contrario, no se podrá acceder al servicio.',
ascendVisibleDevices: 'Dispositivos Ascend visibles (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sincronizar con cuenta de modelo',
modelAccountAddressHelper:
'La dirección del contenedor es para contenedores de agentes; 127.0.0.1, IP del sistema o dirección personalizada es para AI Gateway o servicios externos.',
containerAddress: 'Dirección del contenedor',
+1
View File
@@ -1048,6 +1048,7 @@ const message = {
vllmCommandPortHelper:
'فرمان راه‌اندازی باید از پورت {0} استفاده کند؛ در غیر این صورت سرویس قابل دسترسی نخواهد بود.',
ascendVisibleDevices: 'دستگاه‌های قابل مشاهده Ascend (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'همگام‌سازی با حساب مدل',
modelAccountAddressHelper:
'آدرس کانتینر برای کانتینرهای عامل است؛ 127.0.0.1، IP سیستم یا آدرس سفارشی برای دروازه AI یا خدمات خارجی است.',
containerAddress: 'آدرس کانتینر',
+1
View File
@@ -1052,6 +1052,7 @@ const message = {
vllmCommandPortHelper:
'起動コマンドではポート {0} を使用する必要があります。使用しない場合、サービスにアクセスできません。',
ascendVisibleDevices: 'Ascend 可視デバイス(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'モデルアカウントに同期',
modelAccountAddressHelper:
'コンテナアドレスはエージェントコンテナ向けです。127.0.0.1、システム IP、カスタムアドレスは AI Gateway または外部サービス向けです。',
containerAddress: 'コンテナアドレス',
+1
View File
@@ -1041,6 +1041,7 @@ const message = {
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '시작 명령은 {0} 포트를 사용해야 하며, 그렇지 않으면 서비스에 접근할 수 없습니다.',
ascendVisibleDevices: 'Ascend 표시 장치 (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '모델 계정에 동기화',
modelAccountAddressHelper:
'컨테이너 주소는 에이전트 컨테이너 호출에 적합합니다. 127.0.0.1, 시스템 IP 또는 사용자 지정 주소는 AI Gateway 또는 외부 서비스 호출에 적합합니다.',
containerAddress: '컨테이너 주소',
+1
View File
@@ -1038,6 +1038,7 @@ const message = {
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: 'ຄຳສັ່ງເລີ່ມຕົ້ນຕ້ອງໃຊ້ພອດ {0}; ບໍ່ດັ່ງນັ້ນຈະບໍ່ສາມາດເຂົ້າເຖິງບໍລິການໄດ້.',
ascendVisibleDevices: 'ອຸປະກອນ Ascend ທີ່ເຫັນໄດ້ (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'ຊິ້ງຄ໌ໄປຍັງບັນຊີໂມເດວ',
modelAccountAddressHelper:
'ທີ່ຢູ່ຄອນເທນເນີແມ່ນສຳລັບຄອນເທນເນີຕົວແທນ; 127.0.0.1, IP ລະບົບ, ຫຼື ທີ່ຢູ່ກຳນົດເອງແມ່ນສຳລັບ AI Gateway ຫຼື ບໍລິການພາຍນອກ.',
containerAddress: 'ທີ່ຢູ່ຄອນເທນເນີ',
+1
View File
@@ -1075,6 +1075,7 @@ const message = {
vllmCommandPortHelper:
'Perintah permulaan mesti menggunakan port {0}; jika tidak, perkhidmatan tidak dapat diakses.',
ascendVisibleDevices: 'Peranti Ascend boleh dilihat (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Segerakkan ke akaun model',
modelAccountAddressHelper:
'Alamat kontena sesuai untuk kontena agen; 127.0.0.1, IP sistem, atau alamat tersuai sesuai untuk AI Gateway atau perkhidmatan luaran.',
containerAddress: 'Alamat kontena',
+1
View File
@@ -1070,6 +1070,7 @@ const message = {
vllmCommandPortHelper:
'O comando de inicialização deve usar a porta {0}; caso contrário, o serviço ficará inacessível.',
ascendVisibleDevices: 'Dispositivos Ascend visíveis (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Sincronizar com conta de modelo',
modelAccountAddressHelper:
'Use o endereço do contêiner para agentes; 127.0.0.1, IP do sistema ou endereço personalizado para AI Gateway ou serviços externos.',
containerAddress: 'Endereço do contêiner',
+1
View File
@@ -1063,6 +1063,7 @@ const message = {
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: 'Команда запуска должна использовать порт {0}, иначе сервис будет недоступен.',
ascendVisibleDevices: 'Видимые устройства Ascend (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Синхронизировать с аккаунтом модели',
modelAccountAddressHelper:
'Адрес контейнера подходит для контейнеров агентов; 127.0.0.1, системный IP или пользовательский адрес подходит для AI Gateway или внешних сервисов.',
containerAddress: 'Адрес контейнера',
+1
View File
@@ -1074,6 +1074,7 @@ const message = {
vllmCommandPortHelper:
'Başlatma komutu {0} numaralı bağlantı noktasını kullanmalıdır; aksi halde hizmete erişilemez.',
ascendVisibleDevices: 'Görünür Ascend cihazları (ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: 'Model hesabına senkronize et',
modelAccountAddressHelper:
'Konteyner adresi ajan konteynerleri içindir; 127.0.0.1, sistem IP veya özel adres AI Gateway ya da harici servisler içindir.',
containerAddress: 'Konteyner adresi',
+2 -1
View File
@@ -798,7 +798,7 @@ const message = {
contextWindow: '上下文視窗',
useModelDefault: '使用模型預設值',
reasoning: '推理模型',
verified: '驗證狀態',
verified: '驗證成功',
verifySkipped: '略過驗證',
skillsTab: 'Skill',
pluginsTab: '外掛',
@@ -1004,6 +1004,7 @@ const message = {
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '啟動命令必須使用 {0} 連接埠,否則服務將無法存取。',
ascendVisibleDevices: 'Ascend 可見裝置(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '同步到模型帳號',
modelAccountAddressHelper:
'容器地址適合智能體容器呼叫;127.0.0.1、本機 IP 或自訂地址適合 AI 閘道或外部服務呼叫。',
containerAddress: '容器地址',
+2 -1
View File
@@ -815,7 +815,7 @@ const message = {
contextWindow: '上下文窗口',
useModelDefault: '使用模型默认值',
reasoning: '推理模型',
verified: '验证状态',
verified: '验证成功',
verifySkipped: '不验证',
skillsTab: '技能',
pluginsTab: '插件',
@@ -1017,6 +1017,7 @@ const message = {
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
vllmCommandPortHelper: '启动命令必须使用 {0} 端口,否则服务将无法访问。',
ascendVisibleDevices: 'Ascend 可见设备(ASCEND_RT_VISIBLE_DEVICES)',
syncModelAccount: '同步到模型账号',
modelAccountAddressHelper:
'容器地址适合智能体容器调用;127.0.0.1、本机 IP 或自定义地址适合 AI 网关或外部服务调用。',
containerAddress: '容器地址',
@@ -1,33 +1,38 @@
<template>
<div class="form-section-title">{{ $t('aiTools.agents.account') }}</div>
<span class="input-help model-account-helper">{{ $t('aiTools.model.modelAccountAddressHelper') }}</span>
<el-form-item
:label="$t('aiTools.agents.allowedOrigins')"
prop="modelAccountBaseURLType"
:rules="Rules.requiredSelect"
>
<el-radio-group v-model="form.modelAccountBaseURLType">
<el-radio-button
v-for="item in addressTypes"
:key="item.value"
:value="item.value"
:disabled="hostMode && item.value === 'container'"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="form.modelAccountBaseURLType === 'custom'"
label="Base URL"
prop="modelAccountBaseURL"
:rules="customURLRules"
>
<el-input v-model.trim="form.modelAccountBaseURL" placeholder="http://192.168.1.100:8000/v1" />
</el-form-item>
<el-form-item v-else label="Base URL">
<el-input :model-value="baseURL" readonly />
<el-form-item v-if="showSync">
<el-checkbox v-model="form.syncModelAccount">{{ $t('aiTools.model.syncModelAccount') }}</el-checkbox>
</el-form-item>
<template v-if="!showSync || form.syncModelAccount">
<span class="input-help model-account-helper">{{ $t('aiTools.model.modelAccountAddressHelper') }}</span>
<el-form-item
:label="$t('aiTools.agents.allowedOrigins')"
prop="modelAccountBaseURLType"
:rules="Rules.requiredSelect"
>
<el-radio-group v-model="form.modelAccountBaseURLType">
<el-radio-button
v-for="item in addressTypes"
:key="item.value"
:value="item.value"
:disabled="hostMode && item.value === 'container'"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="form.modelAccountBaseURLType === 'custom'"
label="Base URL"
prop="modelAccountBaseURL"
:rules="customURLRules"
>
<el-input v-model.trim="form.modelAccountBaseURL" placeholder="http://192.168.1.100:8000/v1" />
</el-form-item>
<el-form-item v-else label="Base URL">
<el-input :model-value="baseURL" readonly />
</el-form-item>
</template>
</template>
<script setup lang="ts">
@@ -39,6 +44,7 @@ import i18n from '@/lang';
const props = defineProps<{
form: {
name: string;
syncModelAccount?: boolean;
containerName: string;
modelAccountBaseURLType: string;
modelAccountBaseURL: string;
@@ -46,6 +52,7 @@ const props = defineProps<{
hostPort: number;
containerPort: string;
hostMode?: boolean;
showSync?: boolean;
}>();
const systemIP = ref('');
const addressTypes = computed(() => [
@@ -57,7 +57,7 @@
v-if="visible && view && view !== VllmPlaceholder && !failed && !missing"
ref="contentRef"
v-bind="
view === EngineConnection
view === EngineConnection || view === AppParams
? {}
: view === ContainerLogs
? { highlightDiff: 210 }
@@ -100,6 +100,7 @@ import { hasPermission } from '@/utils/rbac';
import { hasManagePermissionAccess } from '@/utils/permission';
import { newUUID } from '@/utils/id';
import OllamaModels from '../../ollama/models/index.vue';
import AppParams from '@/views/app-store/installed/detail/index.vue';
import EngineConnection from '../connection/index.vue';
import TensorForm from '../../tensorrt/operate/index.vue';
import VllmPlaceholder from '../../vllm/index.vue';
@@ -196,9 +197,11 @@ const open = async (
await openUpgrade(data.appInstallId, version);
return;
}
const component = action === 'connection' ? EngineConnection : action === 'log' ? ContainerLogs : undefined;
const component = { config: AppParams, connection: EngineConnection, log: ContainerLogs }[action];
view.value = component ? markRaw(component) : undefined;
await nextTick();
if (version !== request || !visible.value) return;
if (action === 'config') await contentRef.value?.acceptParams({ id: data.appInstallId, edit: true });
if (action === 'connection')
contentRef.value?.acceptParams({
name: data.name,
@@ -304,7 +307,9 @@ watch([currentNode, () => route.fullPath, isProductPro, isFxplay], () => {
});
const operationDisabled = (row: Deployment) =>
deleteLoading.value ||
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Waiting', 'Uninstalling'].includes(row.status);
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Rebuilding', 'Waiting', 'Uninstalling'].includes(
row.status,
);
const deleteDeployment = async () => {
const version = request;
const row = deleting.value;
@@ -367,7 +372,7 @@ const returnToManagement = async () => {
const previousView = view.value;
await nextTick();
if (view.value !== previousView) return;
if (actionName.value === 'task' || actionName.value === 'upgrade') emit('search');
if (['task', 'upgrade', 'config'].includes(actionName.value)) emit('search');
if (deleted.value || standalone.value) visible.value = false;
else if (instance.value) open(instance.value);
};
@@ -0,0 +1,156 @@
<template>
<DialogPro
v-model="visible"
:title="$t('commons.button.sync') + ' - ' + form.name"
:size="isMobile ? 'full' : 'normal'"
:show-close="!saving"
@close="close"
>
<div v-loading="loading">
<el-form v-if="form.id" ref="formRef" :model="form" label-position="top" :disabled="saving">
<AccountAddress
ref="addressRef"
:form="form"
:host-port="form.hostPort"
:container-port="form.containerPort"
:host-mode="form.hostMode"
/>
</el-form>
<el-alert v-else-if="!loading" type="error" :closable="false" :title="$t('aiTools.model.loadFailed')" />
</div>
<template #footer>
<el-button :disabled="saving" @click="close">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission type="primary" :loading="saving" :disabled="loading || !form.id" @click="submit">
{{ $t('commons.button.sync') }}
</el-button>
</template>
</DialogPro>
</template>
<script setup lang="ts">
import { onBeforeUnmount, reactive, ref, watch } from 'vue';
import type { FormInstance } from 'element-plus';
import type { AI } from '@/api/interface/ai';
import { syncOllamaModelAccount } from '@/api/modules/ai';
import { checkAppInstalled } from '@/api/modules/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { useRoute } from 'vue-router';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import AccountAddress from '../account-address/index.vue';
import { loadVllmAPI, readPages, type Deployment } from '../../deployment';
const emit = defineEmits(['search']);
const { currentNode, isMobile } = useGlobalStore();
const route = useRoute();
const visible = ref(false);
const loading = ref(false);
const saving = ref(false);
const formRef = ref<FormInstance>();
const addressRef = ref<InstanceType<typeof AccountAddress>>();
const form = reactive({
id: 0,
engine: 'ollama' as 'ollama' | 'vllm',
name: '',
containerName: '',
hostPort: 0,
containerPort: '',
hostMode: false,
modelAccountBaseURLType: 'container' as AI.ModelAccountSync['modelAccountBaseURLType'],
modelAccountBaseURL: '',
});
let request = 0;
const close = () => {
request++;
visible.value = false;
saving.value = false;
};
const open = async (row: Deployment) => {
if (row.engine !== 'ollama' && row.engine !== 'vllm') return;
const version = ++request;
form.id = 0;
form.name = row.name;
form.engine = row.engine;
visible.value = true;
loading.value = true;
try {
if (row.engine === 'ollama') {
const { data: item } = await checkAppInstalled('ollama', row.name);
if (version !== request) return;
if (!item.isExist || item.appInstallId !== row.id) {
MsgError(i18n.global.t('commons.res.notFound'));
return;
}
Object.assign(form, {
id: item.appInstallId,
containerName: item.containerName,
hostPort: item.httpPort,
containerPort: '11434',
hostMode: false,
modelAccountBaseURLType: item.modelAccountBaseURLType || 'container',
modelAccountBaseURL: item.modelAccountBaseURL || '',
});
} else {
const api = await loadVllmAPI();
if (version !== request) return;
const items = await readPages((page) => api.pageVllm({ ...page, info: row.name }));
if (version !== request) return;
const item = items.find((item) => item.id === row.id);
if (!item) {
MsgError(i18n.global.t('commons.res.notFound'));
return;
}
const hostMode = item.appVersion.toLowerCase().startsWith('nvidia-gb10-dspark-');
Object.assign(form, {
id: item.id,
containerName: item.containerName,
hostPort: item.port,
containerPort:
item.command.match(/--port\s+(\d+)/)?.[1] || (item.imageType === 'intel' ? '8001' : '8000'),
hostMode,
modelAccountBaseURLType:
hostMode && item.modelAccountBaseURLType === 'container'
? 'systemIP'
: item.modelAccountBaseURLType || (hostMode ? 'systemIP' : 'container'),
modelAccountBaseURL: item.modelAccountBaseURL || '',
});
}
} finally {
if (version === request) loading.value = false;
}
};
const submit = async () => {
if (loading.value || saving.value || !form.id || !formRef.value) return;
const version = request;
saving.value = true;
try {
const valid = await formRef.value.validate().catch(() => false);
if (!valid || version !== request || !visible.value) return;
const req: AI.ModelAccountSync = {
id: form.id,
modelAccountBaseURLType: form.modelAccountBaseURLType,
modelAccountBaseURL: addressRef.value.baseURL,
};
if (form.engine === 'ollama') {
await syncOllamaModelAccount(req);
} else {
const api = await loadVllmAPI();
if (version !== request || !visible.value) return;
await api.operateVllm({ ...req, operate: 'sync', taskID: '' });
}
if (version !== request) return;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
close();
} finally {
if (version === request) saving.value = false;
}
};
watch([currentNode, () => route.fullPath], close);
onBeforeUnmount(close);
defineExpose({ open });
</script>
+7 -1
View File
@@ -17,6 +17,7 @@ export interface Deployment {
interface VllmItem {
appInstallId: number;
agentAccountId: number;
path: string;
upgradable: boolean;
id: number;
@@ -25,6 +26,9 @@ interface VllmItem {
appVersion: string;
port: number;
modelAccountBaseURL: string;
modelAccountBaseURLType: AI.ModelAccountSync['modelAccountBaseURLType'];
command: string;
imageType: string;
containerName: string;
modelDir: string;
status: string;
@@ -32,7 +36,9 @@ interface VllmItem {
interface VllmAPI {
pageVllm: (req: ReqPage & { info: string }) => Promise<ResultData<ResPage<VllmItem>>>;
operateVllm: (req: { id: number; operate: string; taskID: string; forceDelete?: boolean }) => Promise<unknown>;
operateVllm: (
req: { id: number; operate: string; taskID: string; forceDelete?: boolean } & Partial<AI.ModelAccountSync>,
) => Promise<unknown>;
}
const vllmModules = import.meta.glob<VllmAPI>('@/xpack/api/modules/vllm.ts');
+21 -9
View File
@@ -281,7 +281,7 @@
:buttons="deploymentButtons"
:label="$t('commons.table.operate')"
fixed="right"
:ellipsis="4"
:ellipsis="2"
width="240"
/>
</ComplexTable>
@@ -305,6 +305,7 @@
<AppResources :key="`resources:${currentNode}:${route.fullPath}`" ref="appResourcesRef" @close="search" />
<TaskLog ref="taskRef" width="70%" @close="search" />
<ModelPool ref="poolRef" @updated="search" />
<SyncService ref="syncRef" @search="search" />
<component :is="templateView" v-if="templateView" ref="templateRef" />
</div>
</template>
@@ -338,6 +339,7 @@ import AppUpgrade from '@/views/app-store/installed/upgrade/index.vue';
import AppDelete from '@/views/app-store/installed/delete/index.vue';
import AppResources from '@/views/app-store/installed/check/index.vue';
import ModelPool from './components/model-pool/index.vue';
import SyncService from './components/sync/index.vue';
import ProviderLabel from '@/components/agent-provider-label/index.vue';
import { loadModelOverview, loadVllmAPI, readPages, vllmAvailable, type Deployment } from './deployment';
@@ -366,6 +368,7 @@ const upgradeRef = ref();
const appDeleteRef = ref();
const appResourcesRef = ref();
const poolRef = ref();
const syncRef = ref<InstanceType<typeof SyncService>>();
const templateRef = ref();
const templateView = shallowRef();
const loading = ref(false);
@@ -566,7 +569,9 @@ const taskRef = ref();
const pending = ref('');
const operationDisabled = (row: Deployment) =>
Boolean(pending.value) ||
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Waiting', 'Uninstalling'].includes(row.status);
['Installing', 'Starting', 'ReStarting', 'Restarting', 'Rebuilding', 'Waiting', 'Uninstalling'].includes(
row.status,
);
const statusDisabled = (row: Deployment) =>
operationDisabled(row) || !hasManagePermissionAccess() || (row.engine === 'ollama' && !hasPermission('app_manage'));
const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart') => {
@@ -601,23 +606,30 @@ const operate = async (row: Deployment, operation: 'start' | 'stop' | 'restart')
}
};
const deploymentButtons = [
{
label: i18n.global.t('commons.button.edit'),
permission: true,
show: (row: Deployment) => row.engine === 'vllm' || (row.engine === 'ollama' && hasPermission('app_manage')),
disabled: operationDisabled,
click: (row: Deployment) => manageRef.value.open(row, 'config', true),
},
{
label: i18n.global.t('aiTools.model.manage'),
show: (row: Deployment) => row.engine !== 'vllm',
click: manageDeployment,
},
{
label: i18n.global.t('commons.button.edit'),
permission: true,
show: (row: Deployment) => row.engine === 'vllm',
disabled: operationDisabled,
click: (row: Deployment) => manageRef.value.open(row, 'config', true),
},
{
label: i18n.global.t('menu.monitor'),
show: (row: Deployment) => row.engine === 'vllm',
click: (row: Deployment) => manageRef.value.open(row, 'monitor', true),
},
{
label: i18n.global.t('commons.button.sync'),
permission: true,
show: (row: Deployment) => row.engine === 'ollama' || row.engine === 'vllm',
disabled: operationDisabled,
click: (row: Deployment) => syncRef.value.open(row),
},
{
label: i18n.global.t('commons.button.log'),
click: (row: Deployment) => manageRef.value.open(row, 'log', true),
@@ -9,6 +9,7 @@
<template #after-params>
<AccountAddress
ref="accountRef"
show-sync
:form="form"
:host-port="Number(form.params.PANEL_APP_PORT_HTTP) || 11434"
container-port="11434"
@@ -51,6 +52,7 @@ const form = ref({
name: '',
params: {} as Record<string, any>,
containerName: '',
syncModelAccount: true,
modelAccountBaseURLType: 'container',
modelAccountBaseURL: '',
});
@@ -68,6 +70,7 @@ const openCreate = async () => {
formRef.value.resetForm();
formRef.value.setFormData({
restartPolicy: 'unless-stopped',
syncModelAccount: true,
modelAccountBaseURLType: 'container',
modelAccountBaseURL: '',
});
@@ -98,6 +98,15 @@
<el-input v-else v-model.trim="paramModel.params[p.key]" :disabled="!p.edit"></el-input>
</el-form-item>
</div>
<AccountAddress
v-if="appKey === 'ollama'"
ref="accountRef"
show-sync
:form="paramModel"
:host-port="Number(paramModel.params.PANEL_APP_PORT_HTTP) || paramModel.httpPort"
container-port="11434"
:host-mode="paramModel.isHostMode"
/>
<el-form-item prop="advanced">
<el-checkbox v-model="paramModel.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
@@ -179,11 +188,12 @@
</DrawerPro>
</template>
<script lang="ts" setup>
import AccountAddress from '@/views/ai/model/components/account-address/index.vue';
import Diff from '@/views/app-store/installed/upgrade/diff/index.vue';
import { App } from '@/api/interface/app';
import { getAppInstallParams, updateAppInstallParams, updateInstallConfig } from '@/api/modules/app';
import { reactive, ref } from 'vue';
import { onBeforeUnmount, reactive, ref } from 'vue';
import { FormInstance } from 'element-plus';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { MsgError, MsgSuccess } from '@/utils/message';
@@ -192,10 +202,12 @@ import { splitHttp, checkIpV4V6, checkDomain } from '@/utils/validate';
import i18n from '@/lang';
import { loadResourceLimit } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import { useGlobalStore } from '@/composables/useGlobalStore';
interface ParamProps {
id: number;
app: any;
app?: any;
edit?: boolean;
}
const paramData = ref<ParamProps>({
id: 0,
@@ -207,6 +219,7 @@ interface EditForm extends App.InstallParams {
}
const emit = defineEmits(['close']);
const { currentNode } = useGlobalStore();
const open = ref(false);
const loading = ref(false);
const params = ref<EditForm[]>();
@@ -222,10 +235,9 @@ const rules = reactive({
containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect],
});
const submitModel = reactive<any>({
webUI: '',
});
const appType = ref('');
const appKey = ref('');
const accountRef = ref<InstanceType<typeof AccountAddress>>();
const appConfigUpdate = ref<App.AppConfigUpdate>({
installID: 0,
webUI: '',
@@ -285,15 +297,15 @@ function checkPort(port: string) {
}
const acceptParams = async (props: ParamProps) => {
submitModel.installId = props.id;
params.value = [];
paramData.value.id = props.id;
paramModel.params = {};
edit.value = false;
rules.params = {};
await get();
if (!(await get())) return;
open.value = true;
openConfig.value = false;
if (props.edit) editParam();
};
const handleClose = () => {
@@ -358,6 +370,7 @@ const get = async () => {
paramModel.allowPort = res.data.allowPort;
paramModel.containerName = res.data.containerName;
paramModel.advanced = false;
paramModel.editCompose = false;
paramModel.dockerCompose = res.data.dockerCompose;
paramModel.isHostMode = res.data.hostMode;
paramModel.specifyIP = res.data.specifyIP;
@@ -372,7 +385,15 @@ const get = async () => {
webUI.protocol = httpConfig.proto + '://';
}
appType.value = res.data.type;
appKey.value = res.data.appKey;
paramModel.name = res.data.name;
paramModel.httpPort = res.data.httpPort;
paramModel.syncModelAccount = res.data.syncModelAccount;
paramModel.modelAccountBaseURLType = res.data.modelAccountBaseURLType || 'container';
paramModel.modelAccountBaseURL = res.data.modelAccountBaseURL;
return true;
} catch (error) {
return false;
} finally {
loading.value = false;
}
@@ -380,6 +401,8 @@ const get = async () => {
const submit = async (formEl: FormInstance) => {
if (!formEl) return;
const node = currentNode.value;
const installId = paramData.value.id;
await formEl.validate((valid) => {
if (!valid) {
return;
@@ -389,7 +412,13 @@ const submit = async (formEl: FormInstance) => {
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
submitModel.params = paramModel.params;
if (!open.value || node !== currentNode.value || installId !== paramData.value.id) return;
const submitModel: Record<string, any> = { installId, params: paramModel.params };
if (appKey.value === 'ollama') {
submitModel.syncModelAccount = paramModel.syncModelAccount;
submitModel.modelAccountBaseURLType = paramModel.modelAccountBaseURLType;
submitModel.modelAccountBaseURL = accountRef.value.baseURL;
}
if (paramModel.advanced) {
submitModel.advanced = paramModel.advanced;
submitModel.memoryLimit = paramModel.memoryLimit;
@@ -469,6 +498,10 @@ const changeUnit = () => {
};
defineExpose({ acceptParams });
onBeforeUnmount(() => {
open.value = false;
});
</script>
<style lang="scss" scoped>