2025-12-20 02:01:17 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
:model-value="open"
|
|
|
|
|
|
:title="editingGroup ? '编辑模型映射' : '添加模型映射'"
|
2026-02-09 13:24:42 +08:00
|
|
|
|
:description="editingGroup ? '修改映射配置' : '将提供商模型映射到客户端模型'"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
:icon="Tag"
|
2026-01-14 14:28:13 +08:00
|
|
|
|
size="lg"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
@update:model-value="$emit('update:open', $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-4">
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<!-- 目标模型选择 -->
|
|
|
|
|
|
<div class="space-y-1.5">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<Label class="text-xs">客户端模型</Label>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
:model-value="formData.modelId"
|
|
|
|
|
|
:disabled="!!editingGroup"
|
|
|
|
|
|
@update:model-value="handleModelChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger class="h-9">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<SelectValue placeholder="请选择客户端模型" />
|
2026-01-14 14:28:13 +08:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="model in models"
|
|
|
|
|
|
:key="model.id"
|
|
|
|
|
|
:value="model.id"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="font-medium">{{ model.global_model_display_name || model.provider_model_name }}</span>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground font-mono">{{ model.provider_model_name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
客户端请求此模型时,将路由到选中的提供商模型
|
2026-01-14 14:28:13 +08:00
|
|
|
|
</p>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<!-- 映射名称选择面板 -->
|
|
|
|
|
|
<div class="space-y-1.5">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<Label class="text-xs">提供商模型</Label>
|
2026-01-23 12:50:25 +08:00
|
|
|
|
<!-- 搜索栏 -->
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<div class="flex-1 relative">
|
|
|
|
|
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="searchQuery"
|
2026-02-09 13:24:42 +08:00
|
|
|
|
placeholder="搜索或添加自定义提供商模型..."
|
2026-01-23 12:50:25 +08:00
|
|
|
|
class="pl-8 h-9"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-23 12:50:25 +08:00
|
|
|
|
<!-- 已选数量徽章 -->
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="selectedNames.length === 0"
|
|
|
|
|
|
class="h-7 px-2.5 text-xs rounded-md flex items-center bg-muted text-muted-foreground shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
未选择
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-7 px-2.5 text-xs rounded-md flex items-center bg-primary/10 text-primary shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
已选 {{ selectedNames.length }} 个
|
|
|
|
|
|
</span>
|
2026-02-28 11:44:08 +08:00
|
|
|
|
<!-- 刷新上游模型按钮 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="upstreamModelsLoaded"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
|
|
|
|
|
:disabled="fetchingUpstreamModels"
|
|
|
|
|
|
title="刷新上游模型"
|
|
|
|
|
|
@click="fetchUpstreamModels()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RefreshCw
|
|
|
|
|
|
class="w-4 h-4"
|
|
|
|
|
|
:class="{ 'animate-spin': fetchingUpstreamModels }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="!fetchingUpstreamModels"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="p-2 hover:bg-muted rounded-md transition-colors shrink-0"
|
|
|
|
|
|
title="从提供商获取模型"
|
|
|
|
|
|
@click="fetchUpstreamModels()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Zap class="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Loader2
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="w-4 h-4 animate-spin text-muted-foreground shrink-0"
|
|
|
|
|
|
/>
|
2026-01-23 12:50:25 +08:00
|
|
|
|
</div>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
|
2026-01-23 12:50:25 +08:00
|
|
|
|
<!-- 模型列表 -->
|
|
|
|
|
|
<div class="border rounded-lg overflow-hidden">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<div class="min-h-60 max-h-80 overflow-y-auto">
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<!-- 加载中 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="loadingModels"
|
|
|
|
|
|
class="flex items-center justify-center py-12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<!-- 添加自定义映射名称(搜索内容不在列表中时显示) -->
|
2025-12-20 02:01:17 +08:00
|
|
|
|
<div
|
2026-01-14 14:28:13 +08:00
|
|
|
|
v-if="searchQuery && canAddAsCustom"
|
|
|
|
|
|
class="px-3 py-2 border-b bg-background sticky top-0 z-30"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div
|
2026-01-14 14:28:13 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 rounded-lg border border-dashed hover:border-primary hover:bg-primary/5 cursor-pointer transition-colors"
|
|
|
|
|
|
@click="addCustomName"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Plus class="w-4 h-4 text-muted-foreground" />
|
|
|
|
|
|
<span class="text-sm font-mono">{{ searchQuery }}</span>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</div>
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<span class="text-xs text-muted-foreground">添加自定义提供商模型</span>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 自定义映射名称 -->
|
|
|
|
|
|
<div v-if="customNames.length > 0">
|
|
|
|
|
|
<div
|
2026-02-28 11:44:08 +08:00
|
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-20 cursor-pointer hover:bg-muted/80 transition-colors"
|
|
|
|
|
|
@click="toggleGroupCollapse('custom')"
|
2026-01-14 14:28:13 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
2026-02-28 11:44:08 +08:00
|
|
|
|
<ChevronDown
|
|
|
|
|
|
class="w-4 h-4 transition-transform shrink-0"
|
|
|
|
|
|
:class="collapsedGroups.has('custom') ? '-rotate-90' : ''"
|
|
|
|
|
|
/>
|
2026-02-09 13:24:42 +08:00
|
|
|
|
<span class="text-xs font-medium">自定义模型</span>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<span class="text-xs text-muted-foreground">({{ customNames.length }})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-28 11:44:08 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-show="!collapsedGroups.has('custom')"
|
|
|
|
|
|
class="space-y-1 p-2"
|
|
|
|
|
|
>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
<div
|
2026-01-14 14:28:13 +08:00
|
|
|
|
v-for="name in sortedCustomNames"
|
|
|
|
|
|
:key="name"
|
|
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
|
|
|
|
|
@click="toggleName(name)"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div
|
2026-01-14 14:28:13 +08:00
|
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
|
|
|
|
|
:class="selectedNames.includes(name) ? 'bg-primary border-primary' : ''"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<Check
|
|
|
|
|
|
v-if="selectedNames.includes(name)"
|
|
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
|
|
|
|
|
/>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</div>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<span class="text-sm font-mono truncate flex-1">{{ name }}</span>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-14 14:28:13 +08:00
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
<!-- 上游模型 -->
|
|
|
|
|
|
<template v-if="filteredUpstreamModels.length > 0">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-20 cursor-pointer hover:bg-muted/80 transition-colors"
|
|
|
|
|
|
@click="toggleGroupCollapse('upstream')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<ChevronDown
|
|
|
|
|
|
class="w-4 h-4 transition-transform shrink-0"
|
|
|
|
|
|
:class="collapsedGroups.has('upstream') ? '-rotate-90' : ''"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-xs font-medium">上游模型</span>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">({{ upstreamModelNames.length }})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="text-xs text-primary hover:underline"
|
|
|
|
|
|
@click.stop="toggleAllUpstreamModels"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isAllUpstreamModelsSelected ? '取消全选' : '全选' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-show="!collapsedGroups.has('upstream')"
|
|
|
|
|
|
class="space-y-1 p-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="name in filteredUpstreamModels"
|
|
|
|
|
|
:key="name"
|
|
|
|
|
|
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
|
|
|
|
|
@click="toggleName(name)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="w-4 h-4 border rounded flex items-center justify-center shrink-0"
|
|
|
|
|
|
:class="selectedNames.includes(name) ? 'bg-primary border-primary' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Check
|
|
|
|
|
|
v-if="selectedNames.includes(name)"
|
|
|
|
|
|
class="w-3 h-3 text-primary-foreground"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-sm font-mono truncate flex-1">{{ name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="showEmptyState"
|
|
|
|
|
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tag class="w-10 h-10 mb-2 opacity-30" />
|
|
|
|
|
|
<p class="text-sm">
|
|
|
|
|
|
{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-xs mt-1">
|
2026-02-09 13:24:42 +08:00
|
|
|
|
输入模型名称后点击添加自定义提供商模型
|
2026-01-14 14:28:13 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-14 00:30:39 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="space-y-3 border-t border-border/60 pt-4">
|
|
|
|
|
|
<div class="flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
|
|
|
|
|
<div class="min-w-0 space-y-0.5">
|
|
|
|
|
|
<h3 class="text-sm font-medium text-foreground">
|
|
|
|
|
|
适用范围
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ t('providers.modelMapping.scope.matchHelp') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="max-w-full break-words text-left text-xs text-muted-foreground sm:max-w-[55%] sm:text-right">
|
|
|
|
|
|
{{ mappingScopeSummary }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
|
<div class="min-w-0 space-y-1.5">
|
|
|
|
|
|
<Label class="text-xs">适用端点</Label>
|
|
|
|
|
|
<MultiSelect
|
|
|
|
|
|
v-model="selectedEndpointIds"
|
|
|
|
|
|
:options="endpointOptions"
|
|
|
|
|
|
:placeholder="t('providers.modelMapping.scope.allEndpoints')"
|
|
|
|
|
|
empty-text="暂无端点"
|
|
|
|
|
|
no-results-text="未找到端点"
|
|
|
|
|
|
trigger-class="h-9 rounded-md"
|
|
|
|
|
|
:search-threshold="4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
|
|
|
|
{{ t('providers.modelMapping.scope.endpointHelp') }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="min-w-0 space-y-1.5">
|
|
|
|
|
|
<Label class="text-xs">适用请求</Label>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex min-h-9 w-full flex-wrap gap-1 rounded-md bg-muted/50 p-1"
|
|
|
|
|
|
role="radiogroup"
|
|
|
|
|
|
aria-label="适用请求"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-for="option in requestScopeOptions"
|
|
|
|
|
|
:key="option.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
:variant="requestScopeValue === option.value ? 'secondary' : 'ghost'"
|
|
|
|
|
|
class="h-7 min-w-0 flex-1 basis-[9rem] px-2.5"
|
|
|
|
|
|
role="radio"
|
|
|
|
|
|
:aria-checked="requestScopeValue === option.value"
|
|
|
|
|
|
:title="option.label"
|
|
|
|
|
|
@click="handleRequestScopeChange(option.value)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="truncate">{{ option.label }}</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="text-xs leading-5 text-muted-foreground">
|
|
|
|
|
|
{{ requestScopeDescription }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="$emit('update:open', false)"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
2026-01-14 14:28:13 +08:00
|
|
|
|
:disabled="submitting || !formData.modelId || selectedNames.length === 0"
|
2025-12-20 02:01:17 +08:00
|
|
|
|
@click="handleSubmit"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Loader2
|
|
|
|
|
|
v-if="submitting"
|
|
|
|
|
|
class="w-4 h-4 mr-2 animate-spin"
|
|
|
|
|
|
/>
|
2026-07-14 00:30:39 +08:00
|
|
|
|
{{ editingGroup ? '保存映射' : '添加映射' }}
|
2025-12-20 02:01:17 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, watch } from 'vue'
|
2026-02-28 11:44:08 +08:00
|
|
|
|
import { Tag, Loader2, Plus, Search, Check, ChevronDown, RefreshCw, Zap } from 'lucide-vue-next'
|
2025-12-20 02:01:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Label,
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from '@/components/ui'
|
2026-05-07 00:48:15 +08:00
|
|
|
|
import MultiSelect from '@/components/common/MultiSelect.vue'
|
2025-12-20 02:01:17 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-07-14 00:30:39 +08:00
|
|
|
|
import { useI18n } from '@/i18n'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2025-12-20 02:01:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
type Model,
|
2026-05-07 00:48:15 +08:00
|
|
|
|
type ProviderEndpoint,
|
2026-01-29 22:58:37 +08:00
|
|
|
|
type ProviderModelAlias,
|
2026-02-28 11:44:08 +08:00
|
|
|
|
type UpstreamModel,
|
2025-12-20 02:01:17 +08:00
|
|
|
|
} from '@/api/endpoints'
|
|
|
|
|
|
import { updateModel } from '@/api/endpoints/models'
|
2026-02-28 11:44:08 +08:00
|
|
|
|
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
2026-07-14 00:30:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ALL_REQUESTS_SCOPE_VALUE,
|
|
|
|
|
|
COMPACT_REQUEST_SCOPE_VALUE,
|
|
|
|
|
|
formatModelMappingEndpointLabel,
|
|
|
|
|
|
formatModelMappingRequestScope,
|
2026-07-14 02:05:10 +08:00
|
|
|
|
modelMappingEndpointScopeSupportsSessionCompaction,
|
2026-07-14 00:30:39 +08:00
|
|
|
|
modelMappingOperationsKey,
|
|
|
|
|
|
modelMappingOperationsFromScopeValue,
|
|
|
|
|
|
modelMappingRequestScopeOptions,
|
|
|
|
|
|
modelMappingRequestScopeValue,
|
|
|
|
|
|
normalizeModelMappingOperations,
|
|
|
|
|
|
} from '../utils/modelMappingScope'
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
export interface AliasGroup {
|
|
|
|
|
|
model: Model
|
2026-02-27 13:54:46 +08:00
|
|
|
|
/** @deprecated */
|
2025-12-20 02:01:17 +08:00
|
|
|
|
apiFormatsKey: string
|
2026-02-27 13:54:46 +08:00
|
|
|
|
/** @deprecated */
|
2025-12-20 02:01:17 +08:00
|
|
|
|
apiFormats: string[]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
endpointIdsKey: string
|
|
|
|
|
|
endpointIds: string[]
|
2026-07-13 21:45:53 +08:00
|
|
|
|
operationsKey: string
|
|
|
|
|
|
operations: string[]
|
2025-12-20 02:01:17 +08:00
|
|
|
|
aliases: ProviderModelAlias[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
providerId: string
|
2026-02-27 13:54:46 +08:00
|
|
|
|
/** @deprecated */
|
2026-01-14 14:28:13 +08:00
|
|
|
|
providerApiFormats?: string[]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
endpoints?: ProviderEndpoint[]
|
2025-12-20 02:01:17 +08:00
|
|
|
|
models: Model[]
|
|
|
|
|
|
editingGroup?: AliasGroup | null
|
2026-01-11 02:06:48 +08:00
|
|
|
|
preselectedModelId?: string | null
|
2026-02-28 11:44:08 +08:00
|
|
|
|
hasAutoFetchKey?: boolean
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:open': [value: boolean]
|
|
|
|
|
|
'saved': []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-05-26 15:39:56 +08:00
|
|
|
|
const { error: showError, success: showSuccess, warning: showWarning } = useToast()
|
2026-07-14 00:30:39 +08:00
|
|
|
|
const { t } = useI18n()
|
2026-02-28 11:44:08 +08:00
|
|
|
|
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
type EndpointOption = {
|
|
|
|
|
|
value: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 02:01:17 +08:00
|
|
|
|
// 状态
|
|
|
|
|
|
const submitting = ref(false)
|
2026-01-14 14:28:13 +08:00
|
|
|
|
const loadingModels = ref(false)
|
2026-02-28 11:44:08 +08:00
|
|
|
|
const fetchingUpstreamModels = ref(false)
|
|
|
|
|
|
const upstreamModelsLoaded = ref(false)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 搜索
|
|
|
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
// 折叠状态
|
|
|
|
|
|
const collapsedGroups = ref<Set<string>>(new Set())
|
|
|
|
|
|
|
|
|
|
|
|
// 上游模型
|
|
|
|
|
|
const upstreamModels = ref<UpstreamModel[]>([])
|
|
|
|
|
|
|
2025-12-20 02:01:17 +08:00
|
|
|
|
// 表单数据
|
|
|
|
|
|
const formData = ref<{
|
|
|
|
|
|
modelId: string
|
|
|
|
|
|
}>({
|
2026-01-14 14:28:13 +08:00
|
|
|
|
modelId: ''
|
2025-12-20 02:01:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 选中的映射名称
|
|
|
|
|
|
const selectedNames = ref<string[]>([])
|
|
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
// 选中的端点 ID;空数组表示全部端点
|
|
|
|
|
|
const selectedEndpointIds = ref<string[]>([])
|
|
|
|
|
|
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const selectedOperations = ref<string[]>([])
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 自定义名称列表(手动添加的)
|
|
|
|
|
|
const allCustomNames = ref<string[]>([])
|
|
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const endpointOptions = computed<EndpointOption[]>(() => {
|
2026-07-14 00:30:39 +08:00
|
|
|
|
const endpoints = props.endpoints ?? []
|
|
|
|
|
|
return endpoints.map(endpoint => ({
|
|
|
|
|
|
value: endpoint.id,
|
|
|
|
|
|
label: formatModelMappingEndpointLabel(endpoint, endpoints),
|
|
|
|
|
|
}))
|
2026-05-07 00:48:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedSelectedEndpointIds = computed(() => {
|
|
|
|
|
|
const selected = normalizeStringList(selectedEndpointIds.value)
|
2026-07-14 00:30:39 +08:00
|
|
|
|
return selected.length > 0 ? selected : undefined
|
2026-05-07 00:48:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-14 02:05:10 +08:00
|
|
|
|
const sessionCompactionScopeAvailable = computed(() => {
|
|
|
|
|
|
return modelMappingEndpointScopeSupportsSessionCompaction(
|
|
|
|
|
|
normalizedSelectedEndpointIds.value,
|
|
|
|
|
|
props.endpoints ?? [],
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const endpointScopeSummary = computed(() => {
|
|
|
|
|
|
const selected = normalizedSelectedEndpointIds.value
|
2026-07-14 00:30:39 +08:00
|
|
|
|
if (!selected || selected.length === 0) {
|
|
|
|
|
|
return t('providers.modelMapping.scope.allEndpoints')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selected.length === 1) {
|
|
|
|
|
|
return endpointOptions.value.find(option => option.value === selected[0])?.label
|
|
|
|
|
|
?? t('providers.modelMapping.scope.endpointCount', { count: 1 })
|
|
|
|
|
|
}
|
|
|
|
|
|
return t('providers.modelMapping.scope.endpointCount', { count: selected.length })
|
2026-05-07 00:48:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-14 00:30:39 +08:00
|
|
|
|
const requestScopeLabels = computed(() => ({
|
|
|
|
|
|
allRequests: t('providers.modelMapping.scope.allRequests'),
|
|
|
|
|
|
sessionCompactionOnly: t('providers.modelMapping.scope.sessionCompactionOnly'),
|
|
|
|
|
|
customOperations: (operations: string[]) => t(
|
|
|
|
|
|
'providers.modelMapping.scope.customOperations',
|
|
|
|
|
|
{ operations: operations.join(', ') },
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const normalizedSelectedOperations = computed(() => {
|
2026-07-14 00:30:39 +08:00
|
|
|
|
const selected = normalizeModelMappingOperations(selectedOperations.value)
|
2026-07-13 21:45:53 +08:00
|
|
|
|
return selected.length > 0 ? selected : undefined
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const operationScopeSummary = computed(() => {
|
2026-07-14 00:30:39 +08:00
|
|
|
|
return formatModelMappingRequestScope(
|
|
|
|
|
|
normalizedSelectedOperations.value,
|
|
|
|
|
|
requestScopeLabels.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const mappingScopeSummary = computed(() => {
|
|
|
|
|
|
return `${endpointScopeSummary.value} · ${operationScopeSummary.value}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const requestScopeValue = computed(() => {
|
|
|
|
|
|
return modelMappingRequestScopeValue(selectedOperations.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const requestScopeOptions = computed(() => {
|
2026-07-14 02:05:10 +08:00
|
|
|
|
return modelMappingRequestScopeOptions(
|
|
|
|
|
|
selectedOperations.value,
|
|
|
|
|
|
{ sessionCompaction: sessionCompactionScopeAvailable.value },
|
|
|
|
|
|
requestScopeLabels.value,
|
|
|
|
|
|
)
|
2026-07-14 00:30:39 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const requestScopeDescription = computed(() => {
|
|
|
|
|
|
if (requestScopeValue.value === ALL_REQUESTS_SCOPE_VALUE) {
|
2026-07-14 02:05:10 +08:00
|
|
|
|
return sessionCompactionScopeAvailable.value
|
|
|
|
|
|
? t('providers.modelMapping.scope.allRequestsDescription')
|
|
|
|
|
|
: t('providers.modelMapping.scope.allRequestsDefaultDescription')
|
2026-07-14 00:30:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (requestScopeValue.value === COMPACT_REQUEST_SCOPE_VALUE) {
|
|
|
|
|
|
return t('providers.modelMapping.scope.sessionCompactionDescription')
|
|
|
|
|
|
}
|
|
|
|
|
|
return t('providers.modelMapping.scope.customOperationsDescription', {
|
|
|
|
|
|
operations: normalizeModelMappingOperations(selectedOperations.value).join(', '),
|
|
|
|
|
|
})
|
2026-07-13 21:45:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-14 02:05:10 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
[() => props.endpoints, () => selectedEndpointIds.value],
|
|
|
|
|
|
() => normalizeUnavailableSessionCompactionScope(),
|
|
|
|
|
|
{ deep: true },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
// 所有已知名称集合
|
|
|
|
|
|
const allKnownNames = computed(() => {
|
|
|
|
|
|
const set = new Set<string>()
|
|
|
|
|
|
upstreamModels.value.forEach(m => set.add(m.id))
|
|
|
|
|
|
return set
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 上游模型名称列表(去重后)
|
|
|
|
|
|
const upstreamModelNames = computed(() => {
|
|
|
|
|
|
const names = new Set<string>()
|
|
|
|
|
|
upstreamModels.value.forEach(m => {
|
|
|
|
|
|
names.add(m.id)
|
|
|
|
|
|
})
|
|
|
|
|
|
return Array.from(names).sort()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义名称列表(排除上游模型中已有的)
|
2026-01-14 14:28:13 +08:00
|
|
|
|
const customNames = computed(() => {
|
2026-02-28 11:44:08 +08:00
|
|
|
|
const upstreamSet = new Set(upstreamModelNames.value)
|
|
|
|
|
|
return allCustomNames.value.filter(name => !upstreamSet.has(name))
|
2026-01-14 14:28:13 +08:00
|
|
|
|
})
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 排序后的自定义名称
|
|
|
|
|
|
const sortedCustomNames = computed(() => {
|
|
|
|
|
|
const search = searchQuery.value.toLowerCase().trim()
|
|
|
|
|
|
if (!search) return customNames.value
|
|
|
|
|
|
|
|
|
|
|
|
const matched: string[] = []
|
|
|
|
|
|
const unmatched: string[] = []
|
|
|
|
|
|
for (const name of customNames.value) {
|
|
|
|
|
|
if (name.toLowerCase().includes(search)) {
|
|
|
|
|
|
matched.push(name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
unmatched.push(name)
|
|
|
|
|
|
}
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
2026-01-14 14:28:13 +08:00
|
|
|
|
return [...matched, ...unmatched]
|
2025-12-20 02:01:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 判断搜索内容是否可以作为自定义名称添加
|
|
|
|
|
|
const canAddAsCustom = computed(() => {
|
|
|
|
|
|
const search = searchQuery.value.trim()
|
|
|
|
|
|
if (!search) return false
|
|
|
|
|
|
if (selectedNames.value.includes(search)) return false
|
|
|
|
|
|
if (allCustomNames.value.includes(search)) return false
|
2026-02-28 11:44:08 +08:00
|
|
|
|
if (allKnownNames.value.has(search)) return false
|
2026-01-14 14:28:13 +08:00
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
// 过滤后的上游模型
|
|
|
|
|
|
const filteredUpstreamModels = computed(() => {
|
|
|
|
|
|
if (!searchQuery.value.trim()) return upstreamModelNames.value
|
|
|
|
|
|
const query = searchQuery.value.toLowerCase()
|
|
|
|
|
|
return upstreamModelNames.value.filter(name => name.toLowerCase().includes(query))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 空状态判断
|
|
|
|
|
|
const showEmptyState = computed(() => {
|
2026-02-28 11:44:08 +08:00
|
|
|
|
return filteredUpstreamModels.value.length === 0 && customNames.value.length === 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 上游模型是否全选
|
|
|
|
|
|
const isAllUpstreamModelsSelected = computed(() => {
|
|
|
|
|
|
if (filteredUpstreamModels.value.length === 0) return false
|
|
|
|
|
|
return filteredUpstreamModels.value.every(name => selectedNames.value.includes(name))
|
2026-01-14 14:28:13 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 切换名称选中状态
|
|
|
|
|
|
function toggleName(name: string) {
|
|
|
|
|
|
const idx = selectedNames.value.indexOf(name)
|
|
|
|
|
|
if (idx === -1) {
|
|
|
|
|
|
selectedNames.value.push(name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedNames.value.splice(idx, 1)
|
|
|
|
|
|
}
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
// 添加自定义名称
|
|
|
|
|
|
function addCustomName() {
|
|
|
|
|
|
const name = searchQuery.value.trim()
|
|
|
|
|
|
if (name && !selectedNames.value.includes(name)) {
|
|
|
|
|
|
selectedNames.value.push(name)
|
2026-02-28 11:44:08 +08:00
|
|
|
|
if (!allKnownNames.value.has(name) && !allCustomNames.value.includes(name)) {
|
2026-01-14 14:28:13 +08:00
|
|
|
|
allCustomNames.value.push(name)
|
2025-12-28 19:40:07 +08:00
|
|
|
|
}
|
2026-01-14 14:28:13 +08:00
|
|
|
|
searchQuery.value = ''
|
2025-12-28 19:40:07 +08:00
|
|
|
|
}
|
2026-01-14 14:28:13 +08:00
|
|
|
|
}
|
2025-12-28 19:40:07 +08:00
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
// 全选/取消全选上游模型
|
|
|
|
|
|
function toggleAllUpstreamModels() {
|
|
|
|
|
|
const allNames = filteredUpstreamModels.value
|
|
|
|
|
|
if (isAllUpstreamModelsSelected.value) {
|
|
|
|
|
|
selectedNames.value = selectedNames.value.filter(name => !allNames.includes(name))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
allNames.forEach(name => {
|
|
|
|
|
|
if (!selectedNames.value.includes(name)) {
|
|
|
|
|
|
selectedNames.value.push(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
function normalizeStringList(values: string[] | undefined): string[] {
|
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
const result: string[] = []
|
|
|
|
|
|
for (const value of values ?? []) {
|
|
|
|
|
|
const normalized = value.trim()
|
|
|
|
|
|
if (!normalized || seen.has(normalized)) continue
|
|
|
|
|
|
seen.add(normalized)
|
|
|
|
|
|
result.push(normalized)
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getScopeKey(values: string[] | undefined): string {
|
|
|
|
|
|
return normalizeStringList(values).sort().join(',')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scopesOverlap(left: string[] | undefined, right: string[] | undefined): boolean {
|
|
|
|
|
|
const leftValues = normalizeStringList(left)
|
|
|
|
|
|
const rightValues = normalizeStringList(right)
|
|
|
|
|
|
if (leftValues.length === 0 || rightValues.length === 0) return true
|
|
|
|
|
|
const rightSet = new Set(rightValues)
|
|
|
|
|
|
return leftValues.some(value => rightSet.has(value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 00:30:39 +08:00
|
|
|
|
function operationScopesOverlap(
|
|
|
|
|
|
left: string[] | undefined,
|
|
|
|
|
|
right: string[] | undefined,
|
|
|
|
|
|
): boolean {
|
|
|
|
|
|
const leftValues = normalizeModelMappingOperations(left)
|
|
|
|
|
|
const rightValues = normalizeModelMappingOperations(right)
|
|
|
|
|
|
if (leftValues.length === 0 || rightValues.length === 0) return true
|
|
|
|
|
|
const rightSet = new Set(rightValues)
|
|
|
|
|
|
return leftValues.some(value => rightSet.has(value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 00:48:15 +08:00
|
|
|
|
function findDuplicateNames(
|
|
|
|
|
|
existingAliases: ProviderModelAlias[],
|
|
|
|
|
|
names: string[],
|
|
|
|
|
|
endpointIds: string[] | undefined,
|
|
|
|
|
|
apiFormats: string[] | undefined = undefined,
|
2026-07-13 21:45:53 +08:00
|
|
|
|
operations: string[] | undefined = undefined,
|
2026-05-07 00:48:15 +08:00
|
|
|
|
): string[] {
|
|
|
|
|
|
const duplicates = new Set<string>()
|
|
|
|
|
|
for (const rawName of names) {
|
|
|
|
|
|
const name = rawName.trim()
|
|
|
|
|
|
if (!name) continue
|
|
|
|
|
|
const duplicate = existingAliases.some((alias) => {
|
|
|
|
|
|
return alias.name === name
|
|
|
|
|
|
&& scopesOverlap(alias.endpoint_ids, endpointIds)
|
|
|
|
|
|
&& scopesOverlap(alias.api_formats, apiFormats)
|
2026-07-14 00:30:39 +08:00
|
|
|
|
&& operationScopesOverlap(alias.operations, operations)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
if (duplicate) duplicates.add(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
return Array.from(duplicates)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 11:44:08 +08:00
|
|
|
|
// 切换折叠状态
|
|
|
|
|
|
function toggleGroupCollapse(group: string) {
|
|
|
|
|
|
if (collapsedGroups.value.has(group)) {
|
|
|
|
|
|
collapsedGroups.value.delete(group)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
collapsedGroups.value.add(group)
|
|
|
|
|
|
}
|
|
|
|
|
|
collapsedGroups.value = new Set(collapsedGroups.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从提供商获取模型(使用缓存)
|
|
|
|
|
|
async function fetchUpstreamModels() {
|
|
|
|
|
|
if (!props.providerId) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
loadingModels.value = true
|
|
|
|
|
|
fetchingUpstreamModels.value = true
|
|
|
|
|
|
const result = await fetchCachedModels(props.providerId)
|
|
|
|
|
|
if (result.models.length > 0) {
|
|
|
|
|
|
upstreamModels.value = result.models
|
|
|
|
|
|
upstreamModelsLoaded.value = true
|
|
|
|
|
|
// 获取上游模型后,将不在上游列表中的已选名称添加到自定义列表
|
|
|
|
|
|
const upstreamIds = new Set(result.models.map(m => m.id))
|
|
|
|
|
|
const customFromSelected = selectedNames.value.filter(name => !upstreamIds.has(name))
|
|
|
|
|
|
const mergedCustom = new Set([...allCustomNames.value, ...customFromSelected])
|
|
|
|
|
|
allCustomNames.value = Array.from(mergedCustom).filter(name => !upstreamIds.has(name))
|
|
|
|
|
|
}
|
2026-05-26 17:45:06 +08:00
|
|
|
|
if (result.warning) {
|
|
|
|
|
|
showWarning(result.warning, '部分格式获取失败')
|
|
|
|
|
|
}
|
2026-02-28 11:44:08 +08:00
|
|
|
|
if (result.error) {
|
2026-05-26 17:45:06 +08:00
|
|
|
|
showError(result.error, '获取上游模型失败')
|
2026-02-28 11:44:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
|
showError(parseApiError(err, '获取上游模型列表失败'), '错误')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingModels.value = false
|
|
|
|
|
|
fetchingUpstreamModels.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 02:01:17 +08:00
|
|
|
|
// 监听打开状态
|
2026-07-27 15:53:28 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
async (isOpen) => {
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
|
initForm()
|
|
|
|
|
|
if (props.hasAutoFetchKey) {
|
|
|
|
|
|
await fetchUpstreamModels()
|
|
|
|
|
|
}
|
2026-02-28 11:44:08 +08:00
|
|
|
|
}
|
2026-07-27 15:53:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化表单
|
|
|
|
|
|
function initForm() {
|
|
|
|
|
|
if (props.editingGroup) {
|
|
|
|
|
|
formData.value = {
|
2026-01-14 14:28:13 +08:00
|
|
|
|
modelId: props.editingGroup.model.id
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
2026-02-09 13:24:42 +08:00
|
|
|
|
const existingNames = props.editingGroup.aliases.map(a => a.name)
|
|
|
|
|
|
selectedNames.value = [...existingNames]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
selectedEndpointIds.value = normalizeStringList(props.editingGroup.endpointIds)
|
2026-07-14 00:30:39 +08:00
|
|
|
|
selectedOperations.value = normalizeModelMappingOperations(props.editingGroup.operations)
|
2026-02-09 13:24:42 +08:00
|
|
|
|
allCustomNames.value = [...existingNames]
|
2025-12-20 02:01:17 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
formData.value = {
|
2026-01-14 14:28:13 +08:00
|
|
|
|
modelId: props.preselectedModelId || ''
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
2026-01-14 14:28:13 +08:00
|
|
|
|
selectedNames.value = []
|
2026-05-07 00:48:15 +08:00
|
|
|
|
selectedEndpointIds.value = []
|
2026-07-13 21:45:53 +08:00
|
|
|
|
selectedOperations.value = []
|
2026-01-14 14:28:13 +08:00
|
|
|
|
allCustomNames.value = []
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
2026-01-14 14:28:13 +08:00
|
|
|
|
searchQuery.value = ''
|
2026-02-28 11:44:08 +08:00
|
|
|
|
upstreamModels.value = []
|
|
|
|
|
|
upstreamModelsLoaded.value = false
|
|
|
|
|
|
collapsedGroups.value = new Set()
|
2026-07-14 02:05:10 +08:00
|
|
|
|
normalizeUnavailableSessionCompactionScope()
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
2025-12-25 19:36:29 +08:00
|
|
|
|
|
2025-12-25 00:02:56 +08:00
|
|
|
|
// 处理模型选择变更
|
|
|
|
|
|
function handleModelChange(value: string) {
|
|
|
|
|
|
formData.value.modelId = value
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 00:30:39 +08:00
|
|
|
|
function handleRequestScopeChange(value: string) {
|
2026-07-14 02:05:10 +08:00
|
|
|
|
if (
|
|
|
|
|
|
value === COMPACT_REQUEST_SCOPE_VALUE
|
|
|
|
|
|
&& !sessionCompactionScopeAvailable.value
|
|
|
|
|
|
) return
|
2026-07-14 00:30:39 +08:00
|
|
|
|
selectedOperations.value = modelMappingOperationsFromScopeValue(value) ?? []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 02:05:10 +08:00
|
|
|
|
function normalizeUnavailableSessionCompactionScope() {
|
2026-07-19 16:42:32 +08:00
|
|
|
|
// An empty list is also used while endpoints are loading or after loading fails,
|
|
|
|
|
|
// so it cannot prove that the existing scope is unsupported.
|
|
|
|
|
|
if (!props.endpoints || props.endpoints.length === 0) return
|
2026-07-14 02:05:10 +08:00
|
|
|
|
if (
|
|
|
|
|
|
requestScopeValue.value === COMPACT_REQUEST_SCOPE_VALUE
|
|
|
|
|
|
&& !sessionCompactionScopeAvailable.value
|
|
|
|
|
|
) {
|
|
|
|
|
|
selectedOperations.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 02:01:17 +08:00
|
|
|
|
// 生成作用域唯一键
|
|
|
|
|
|
function getApiFormatsKey(formats: string[] | undefined): string {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
return getScopeKey(formats)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
|
|
|
|
|
return getScopeKey(endpointIds)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 21:45:53 +08:00
|
|
|
|
function getOperationsKey(operations: string[] | undefined): string {
|
2026-07-14 00:30:39 +08:00
|
|
|
|
return modelMappingOperationsKey(operations)
|
2026-07-13 21:45:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 02:01:17 +08:00
|
|
|
|
// 提交表单
|
|
|
|
|
|
async function handleSubmit() {
|
|
|
|
|
|
if (submitting.value) return
|
2026-01-14 14:28:13 +08:00
|
|
|
|
if (!formData.value.modelId || selectedNames.value.length === 0) return
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
submitting.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const targetModel = props.models.find(m => m.id === formData.value.modelId)
|
|
|
|
|
|
if (!targetModel) {
|
|
|
|
|
|
showError('模型不存在', '错误')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 02:39:10 +08:00
|
|
|
|
const currentAliases = targetModel.provider_model_mappings || []
|
2025-12-20 02:01:17 +08:00
|
|
|
|
let newAliases: ProviderModelAlias[]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const nextEndpointIds = normalizedSelectedEndpointIds.value
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const nextOperations = normalizedSelectedOperations.value
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
2026-01-14 14:28:13 +08:00
|
|
|
|
const buildAliases = (names: string[]): ProviderModelAlias[] => {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
return names.map((name) => {
|
|
|
|
|
|
const alias: ProviderModelAlias = {
|
|
|
|
|
|
name: name.trim(),
|
|
|
|
|
|
priority: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextEndpointIds && nextEndpointIds.length > 0) {
|
|
|
|
|
|
alias.endpoint_ids = nextEndpointIds
|
|
|
|
|
|
}
|
2026-07-13 21:45:53 +08:00
|
|
|
|
if (nextOperations && nextOperations.length > 0) {
|
|
|
|
|
|
alias.operations = nextOperations
|
|
|
|
|
|
}
|
2026-05-07 00:48:15 +08:00
|
|
|
|
return alias
|
|
|
|
|
|
})
|
2026-01-14 14:28:13 +08:00
|
|
|
|
}
|
2025-12-20 02:01:17 +08:00
|
|
|
|
|
|
|
|
|
|
if (props.editingGroup) {
|
|
|
|
|
|
const oldApiFormatsKey = props.editingGroup.apiFormatsKey
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const oldEndpointIdsKey = props.editingGroup.endpointIdsKey
|
2026-07-14 00:30:39 +08:00
|
|
|
|
const oldOperationsKey = modelMappingOperationsKey(props.editingGroup.operations)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
const oldAliasNames = new Set(props.editingGroup.aliases.map(a => a.name))
|
|
|
|
|
|
|
|
|
|
|
|
const filteredAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
|
|
|
|
|
const currentKey = getApiFormatsKey(a.api_formats)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const currentOperationsKey = getOperationsKey(a.operations)
|
|
|
|
|
|
return !(currentKey === oldApiFormatsKey
|
|
|
|
|
|
&& currentEndpointIdsKey === oldEndpointIdsKey
|
|
|
|
|
|
&& currentOperationsKey === oldOperationsKey
|
|
|
|
|
|
&& oldAliasNames.has(a.name))
|
2025-12-20 02:01:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const duplicates = findDuplicateNames(
|
|
|
|
|
|
filteredAliases,
|
|
|
|
|
|
selectedNames.value,
|
|
|
|
|
|
nextEndpointIds,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
nextOperations,
|
|
|
|
|
|
)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
if (duplicates.length > 0) {
|
2026-01-14 14:28:13 +08:00
|
|
|
|
showError(`以下映射名称已存在:${duplicates.join(', ')}`, '错误')
|
2025-12-20 02:01:17 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newAliases = [
|
|
|
|
|
|
...filteredAliases,
|
2026-01-14 14:28:13 +08:00
|
|
|
|
...buildAliases(selectedNames.value)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
]
|
|
|
|
|
|
} else {
|
2026-07-13 21:45:53 +08:00
|
|
|
|
const duplicates = findDuplicateNames(
|
|
|
|
|
|
currentAliases,
|
|
|
|
|
|
selectedNames.value,
|
|
|
|
|
|
nextEndpointIds,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
nextOperations,
|
|
|
|
|
|
)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
if (duplicates.length > 0) {
|
2026-01-14 14:28:13 +08:00
|
|
|
|
showError(`以下映射名称已存在:${duplicates.join(', ')}`, '错误')
|
2025-12-20 02:01:17 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
newAliases = [
|
|
|
|
|
|
...currentAliases,
|
2026-01-14 14:28:13 +08:00
|
|
|
|
...buildAliases(selectedNames.value)
|
2025-12-20 02:01:17 +08:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await updateModel(props.providerId, targetModel.id, {
|
2025-12-20 02:39:10 +08:00
|
|
|
|
provider_model_mappings: newAliases
|
2025-12-20 02:01:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
showSuccess(props.editingGroup ? '映射组已更新' : '映射已添加')
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
emit('saved')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
|
showError(parseApiError(err, '操作失败'), '错误')
|
2025-12-20 02:01:17 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|