Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/model/ModelViewItemOperation.vue
陈子默 791649c7d5 feat: 增强智能体模型调用兼容能力
- 增加模型流式开关和 HTTP 传输策略配置

- 使用 AgentScope 执行基础连接、流式与 VLM 双阶段验证

- 固定多模态校验图片并统一验证状态展示
2026-07-17 19:57:06 +08:00

375 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { PropType } from 'vue';
import type { llmType } from '#/api';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import type { VerifyButtonStatus } from '#/views/ai/model/modelUtils/model-verification';
import { ref } from 'vue';
import {
CircleCheck,
CircleClose,
Delete,
Edit,
Loading,
Select,
Warning,
} from '@element-plus/icons-vue';
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
import { verifyModelConfig } from '#/api/ai/llm';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
import {
getVerifyButtonText as getStatusButtonText,
resolveModelVerificationFeedback,
} from '#/views/ai/model/modelUtils/model-verification';
const props = defineProps({
llmList: {
type: Array as PropType<llmType[]>,
default: () => [],
},
needHiddenSettingIcon: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['deleteLlm', 'editLlm']);
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
const getModelId = (llm: llmType) =>
String((llm as any).id || (llm as any).llmId || (llm as any).modelId || '');
const getVerifyStatus = (llm: llmType): VerifyButtonStatus =>
verifyStatusMap.value[getModelId(llm)] || 'idle';
const isVerifying = (llm: llmType) => getVerifyStatus(llm) === 'loading';
const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
if (!id) {
return;
}
verifyStatusMap.value[id] = status;
};
const getVerifyButtonText = (llm: llmType) => {
return getStatusButtonText(getVerifyStatus(llm));
};
const getVerifyButtonIcon = (llm: llmType) => {
const status = getVerifyStatus(llm);
if (status === 'loading') {
return Loading;
}
if (status === 'success') {
return CircleCheck;
}
if (status === 'warning') {
return Warning;
}
if (status === 'error') {
return CircleClose;
}
return Select;
};
const handleDeleteLlm = (id: string) => {
emit('deleteLlm', id);
};
const handleEditLlm = (id: string) => {
emit('editLlm', id);
};
const canVerify = (llm: llmType) => llm.modelType !== 'rerankModel';
const handleVerifyLlm = async (llm: llmType) => {
const modelId = getModelId(llm);
if (!modelId || isVerifying(llm)) {
if (!modelId) {
ElMessage.warning('当前模型缺少ID无法验证配置');
}
return;
}
setVerifyStatus(modelId, 'loading');
try {
const res = await verifyModelConfig(modelId);
const feedback = resolveModelVerificationFeedback(res, llm.modelType);
setVerifyStatus(modelId, feedback.status);
if (feedback.status === 'success') {
ElMessage.success(feedback.message);
} else if (feedback.status === 'warning') {
ElMessage.warning(feedback.message);
} else {
if (!res.message) {
ElMessage.error(feedback.message);
}
}
} catch {
setVerifyStatus(modelId, 'error');
// error toast is already handled by global response interceptors
} finally {
// keep final status for explicit visual feedback
}
};
const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
const defaultAbility = getDefaultModelAbility();
const allTags = mapLlmToModelAbility(llm, defaultAbility);
return allTags.filter((tag) => tag.selected);
};
</script>
<template>
<div class="llm-list">
<article v-for="llm in props.llmList" :key="llm.id" class="llm-item">
<div class="llm-item__main">
<ModelProviderBadge
:icon="llm.modelProvider?.icon"
:provider-name="llm.modelProvider?.providerName"
:provider-type="llm.modelProvider?.providerType"
:size="40"
/>
<div class="llm-item__content">
<div class="llm-item__headline">
<h4 class="llm-item__title">{{ llm.title }}</h4>
<div
v-if="getSelectedAbilityTagsForLlm(llm).length > 0"
class="llm-item__tags"
>
<ElTag
v-for="tag in getSelectedAbilityTagsForLlm(llm)"
:key="tag.value"
effect="plain"
size="small"
class="llm-item__tag"
>
{{ tag.label }}
</ElTag>
</div>
</div>
<p v-if="llm.description" class="llm-item__description">
{{ llm.description }}
</p>
</div>
</div>
<div class="llm-item__actions">
<ElButton
v-if="canVerify(llm)"
text
size="small"
class="llm-item__verify-btn"
:class="`is-${getVerifyStatus(llm)}`"
:disabled="isVerifying(llm) || !getModelId(llm)"
@click="handleVerifyLlm(llm)"
>
<template #icon>
<ElIcon
class="llm-item__verify-icon"
:class="`is-${getVerifyStatus(llm)}`"
>
<component :is="getVerifyButtonIcon(llm)" />
</ElIcon>
</template>
{{ getVerifyButtonText(llm) }}
</ElButton>
<ElButton
v-if="!needHiddenSettingIcon"
text
size="small"
:icon="Edit"
@click="handleEditLlm(llm.id)"
>
编辑
</ElButton>
<ElButton
text
size="small"
:icon="Delete"
class="llm-item__danger"
@click="handleDeleteLlm(llm.id)"
>
删除
</ElButton>
</div>
</article>
</div>
</template>
<style scoped>
.llm-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.llm-item {
display: flex;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
padding: 16px 18px;
background: hsl(var(--surface-panel) / 90%);
border: 1px solid hsl(var(--divider-faint) / 54%);
border-radius: 18px;
transition:
border-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
.llm-item:hover {
border-color: hsl(var(--primary) / 28%);
box-shadow: 0 18px 30px -28px hsl(var(--foreground) / 34%);
transform: translateY(-1px);
}
.llm-item__main {
display: flex;
flex: 1;
gap: 14px;
align-items: flex-start;
min-width: 0;
}
.llm-item__content {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.llm-item__headline {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.llm-item__title {
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
color: hsl(var(--text-strong));
}
.llm-item__description {
display: -webkit-box;
margin: 0;
overflow: hidden;
-webkit-line-clamp: 2;
font-size: 13px;
line-height: 1.5;
color: hsl(var(--text-muted));
-webkit-box-orient: vertical;
}
.llm-item__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.llm-item__tag {
color: hsl(var(--text-strong));
background: hsl(var(--primary) / 8%);
border-color: transparent;
}
.llm-item__actions {
display: inline-flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
justify-content: flex-end;
}
.llm-item__actions :deep(.el-button) {
border-radius: 10px;
}
.llm-item__verify-btn.is-idle {
color: hsl(var(--text-muted));
}
.llm-item__verify-btn.is-loading {
color: hsl(var(--primary));
}
.llm-item__verify-btn.is-success {
color: hsl(var(--success));
}
.llm-item__verify-btn.is-warning {
color: hsl(var(--warning));
}
.llm-item__verify-btn.is-error {
color: hsl(var(--destructive));
}
.llm-item__verify-icon {
transition:
color 0.24s ease,
transform 0.24s ease;
}
.llm-item__verify-icon.is-loading {
animation: llm-item-verify-spin 0.9s linear infinite;
}
.llm-item__verify-icon.is-success,
.llm-item__verify-icon.is-warning,
.llm-item__verify-icon.is-error {
animation: llm-item-verify-pop 0.32s ease;
}
.llm-item__danger {
color: hsl(var(--danger) / 90%);
}
.llm-item__danger:hover,
.llm-item__danger:focus-visible {
color: hsl(var(--danger));
}
@keyframes llm-item-verify-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes llm-item-verify-pop {
0% {
opacity: 0.72;
transform: scale(0.82);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
.llm-item {
flex-direction: column;
}
.llm-item__actions {
justify-content: flex-start;
width: 100%;
}
}
</style>