fix(ai): refine model service forms and Ollama drawers (#14283)

This commit is contained in:
ssongliu
2026-11-03 00:07:38 +08:00
committed by GitHub
parent e29679768b
commit fac15e3292
5 changed files with 104 additions and 67 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ var catalog = map[string]Meta{
},
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
"llmman": {
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
Key: "llmman", DisplayName: "llmman", Sort: 75, DefaultAPIType: "openai-responses",
APIConfigs: []APIConfig{
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
@@ -42,7 +42,31 @@
:loading="loadingEngines"
@change="handleLocalEngineChange"
>
<el-option v-for="item in localEngines" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in localEngines" :key="item.id" :label="item.name" :value="item.id">
<div class="local-engine-option">
<span class="local-engine-option__name" :title="item.name">{{ item.name }}</span>
<el-tag
v-if="item.models.length"
size="small"
type="info"
class="local-engine-option__model"
>
<span :title="item.models[0]">{{ item.models[0] }}</span>
</el-tag>
<el-popover v-if="item.models.length > 1" trigger="click" placement="bottom" :width="320">
<template #reference>
<el-button link type="primary" class="local-engine-option__more" @click.stop>
+{{ item.models.length - 1 }}
</el-button>
</template>
<div class="local-engine-models">
<el-tag v-for="model in item.models.slice(1)" :key="model" size="small" type="info">
{{ model }}
</el-tag>
</div>
</el-popover>
</div>
</el-option>
</el-select>
<el-alert
v-if="enginesLoadFailed"
@@ -116,13 +140,7 @@
</el-form-item>
<template v-if="showModelPool">
<el-divider content-position="left">{{ $t('aiTools.agents.modelPool') }}</el-divider>
<div v-loading="loadingLocalModels" class="model-discovery">
<el-alert
v-if="localModelsLoadFailed"
type="error"
:closable="false"
:title="$t('aiTools.model.loadFailed')"
/>
<div class="model-discovery">
<div class="model-discovery__header">
<el-radio-group v-model="modelConfigMode" size="small" @change="handleModelConfigModeChange">
<el-radio-button v-if="supportsModelDiscovery || defaultModels.length" value="discover">
@@ -228,7 +246,12 @@
<el-input v-model="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.operate')" width="90" align="center">
<el-table-column
v-if="!singleModelOnly"
:label="$t('commons.table.operate')"
width="90"
align="center"
>
<template #default="{ $index }">
<el-button type="primary" link @click="removeManualModel($index)">
{{ $t('commons.button.delete') }}
@@ -236,21 +259,15 @@
</template>
</el-table-column>
</el-table>
<el-tooltip
:disabled="!singleModelOnly || manualModels.length === 0"
:content="$t('aiTools.agents.accountModelsSingle')"
<el-button
v-if="!singleModelOnly"
class="model-discovery__add"
type="primary"
plain
@click="addManualModel"
>
<span class="model-discovery__add">
<el-button
type="primary"
plain
:disabled="singleModelOnly && manualModels.length > 0"
@click="addManualModel"
>
{{ $t('commons.button.add') }}
</el-button>
</span>
</el-tooltip>
{{ $t('commons.button.add') }}
</el-button>
</template>
</div>
</template>
@@ -330,8 +347,6 @@ const { isIntl, isProductPro } = useGlobalStore();
const connectionMode = ref<'manual' | 'local'>('manual');
const loadingEngines = ref(false);
const enginesLoadFailed = ref(false);
const loadingLocalModels = ref(false);
const localModelsLoadFailed = ref(false);
const localEngines = ref<Array<{ id: number; name: string; baseURL: string; models: string[] }>>([]);
const form = reactive({
@@ -366,10 +381,7 @@ const localModelOptions = computed(() =>
const disabled = computed(
() =>
connectionMode.value === 'local' &&
(loadingEngines.value ||
loadingLocalModels.value ||
localModelsLoadFailed.value ||
!localEngines.value.some((item) => item.id === form.localEngineId)),
(loadingEngines.value || !localEngines.value.some((item) => item.id === form.localEngineId)),
);
const selectedAPIConfig = computed(() =>
selectedProvider.value?.apiTypes.find((item) => item.apiType === form.apiType),
@@ -858,12 +870,14 @@ watch([open, () => form.provider, connectionMode], async ([visible, provider, mo
const { data: item } = await checkAppInstalled('ollama', '');
if (cancelled) return;
if (item.isExist) {
const models = await readPages<AI.OllamaModelInfo>((page) => searchOllamaModel(page));
if (cancelled) return;
engines = [
{
id: item.appInstallId,
name: item.name,
baseURL: item.modelAccountBaseURL || `http://${item.containerName}:11434/v1`,
models: [],
models: models.filter((model) => model.status === 'Success').map((model) => model.name),
},
];
}
@@ -876,31 +890,6 @@ watch([open, () => form.provider, connectionMode], async ([visible, provider, mo
}
});
watch(
[open, () => form.provider, connectionMode, () => form.localEngineId],
async ([visible, provider, mode, engineId], _, onCleanup) => {
let cancelled = false;
onCleanup(() => (cancelled = true));
loadingLocalModels.value = false;
localModelsLoadFailed.value = false;
if (!visible || provider !== 'ollama' || mode !== 'local') return;
const engine = localEngines.value.find((item) => item.id === engineId);
if (!engine) return;
loadingLocalModels.value = true;
try {
const models = await readPages<AI.OllamaModelInfo>((page) => searchOllamaModel(page));
if (cancelled) return;
engine.models = models.filter((model) => model.status === 'Success').map((model) => model.name);
resetManualModels();
} catch {
if (!cancelled) localModelsLoadFailed.value = true;
} finally {
if (!cancelled) loadingLocalModels.value = false;
}
},
{ flush: 'sync' },
);
const handleAPITypeChange = () => {
const config = selectedAPIConfig.value;
if (!config) {
@@ -951,6 +940,50 @@ defineExpose({
}
}
.local-engine-option {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
&__name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
&__model {
max-width: 55%;
min-width: 0;
:deep(.el-tag__content) {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__more {
flex-shrink: 0;
}
}
.local-engine-models {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 240px;
overflow: auto;
.el-tag {
height: auto;
padding: 4px 6px;
white-space: normal;
overflow-wrap: anywhere;
max-width: 100%;
}
}
.base-url-warning {
margin-top: 8px;
}
@@ -1,5 +1,5 @@
<template>
<DialogPro size="normal" v-model="drawerVisible" :title="$t('commons.button.add')" @close="handleClose">
<DrawerPro size="normal" v-model="drawerVisible" :header="$t('commons.button.add')" @close="handleClose">
<el-alert type="info" :closable="false">
<template #title>
<span class="flx-align-center">
@@ -32,7 +32,7 @@
</el-button>
</span>
</template>
</DialogPro>
</DrawerPro>
</template>
<script lang="ts" setup>
@@ -120,11 +120,13 @@
</OpDialog>
<AddDialog ref="addRef" @search="search" @log="onLoadLog" />
<Terminal ref="terminalRef" />
<DialogPro v-model="detailVisible" :title="$t('commons.button.view')" size="large">
<CodemirrorPro v-model="detailInfo" :height-diff="300" readonly mode="json" />
</DialogPro>
<DrawerPro v-model="detailVisible" :header="$t('commons.button.view')" size="large">
<template #content>
<CodemirrorPro v-model="detailInfo" :height-diff="180" readonly mode="json" />
</template>
</DrawerPro>
<BindDomain ref="bindDomainRef" />
<TaskLog ref="taskLogRef" width="70%" @close="refresh" />
<TaskLog ref="taskLogRef" width="50%" @close="refresh" />
</div>
</template>
@@ -1,6 +1,8 @@
<template>
<DialogPro v-model="open" :title="$t('menu.terminal')" size="w-70" @close="handleClose">
<Terminal class="mt-2" style="height: 55vh" ref="terminalRef"></Terminal>
<DrawerPro v-model="open" :header="$t('menu.terminal')" size="70%" @close="handleClose">
<template #content>
<Terminal class="mt-2" style="height: 70vh" ref="terminalRef"></Terminal>
</template>
<template #footer>
<span class="dialog-footer">
@@ -9,7 +11,7 @@
</el-button>
</span>
</template>
</DialogPro>
</DrawerPro>
</template>
<script lang="ts" setup>
@@ -24,10 +26,10 @@ const itemName = ref();
const terminalRef = ref();
let operateNode = '';
interface DialogProps {
interface DrawerProps {
name: string;
}
const acceptParams = async (params: DialogProps): Promise<void> => {
const acceptParams = async (params: DrawerProps): Promise<void> => {
operateNode = currentNode.value;
itemName.value = params.name;
open.value = true;