mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: scope model mappings by endpoint
This commit is contained in:
@@ -158,10 +158,12 @@ export type BodyRuleCondition =
|
||||
|
||||
export type HeaderRule = (HeaderRuleSet | HeaderRuleDrop | HeaderRuleRename) & {
|
||||
condition?: BodyRuleCondition
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type BodyRule = (BodyRuleSet | BodyRuleDrop | BodyRuleRename | BodyRuleAppend | BodyRuleInsert | BodyRuleRegexReplace) & {
|
||||
condition?: BodyRuleCondition
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -622,6 +624,7 @@ export interface ProviderModelMapping {
|
||||
name: string
|
||||
priority: number // 优先级(数字越小优先级越高)
|
||||
api_formats?: string[] // 作用域(适用的 API 格式),为空表示对所有格式生效
|
||||
endpoint_ids?: string[] // 作用域(适用的端点 ID),为空表示对所有端点生效
|
||||
}
|
||||
|
||||
// 保留别名以保持向后兼容
|
||||
|
||||
@@ -337,6 +337,7 @@
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-primary/60 bg-muted/30"
|
||||
:class="[
|
||||
!rule.enabled ? 'opacity-60 border-primary/25 bg-muted/20' : '',
|
||||
isHeaderRuleDragging(endpoint.id, index) ? 'opacity-60 border-primary bg-primary/5' : '',
|
||||
isHeaderRuleDragOver(endpoint.id, index) ? 'ring-1 ring-primary/40 bg-primary/10' : ''
|
||||
]"
|
||||
@@ -358,6 +359,12 @@
|
||||
class="text-[10px] font-semibold text-primary shrink-0"
|
||||
title="请求头"
|
||||
>H</span>
|
||||
<Switch
|
||||
:model-value="rule.enabled"
|
||||
class="shrink-0 scale-75 origin-center"
|
||||
:title="rule.enabled ? '已启用,点击禁用这条请求头规则' : '已禁用,点击启用这条请求头规则'"
|
||||
@update:model-value="(v: boolean) => updateEndpointRuleEnabled(endpoint.id, index, v)"
|
||||
/>
|
||||
<Select
|
||||
:model-value="rule.action"
|
||||
:open="ruleSelectOpen[`${endpoint.id}-${index}`]"
|
||||
@@ -459,6 +466,7 @@
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-sky-500/60 bg-muted/30"
|
||||
:class="[
|
||||
!rule.enabled ? 'opacity-60 border-sky-500/25 bg-muted/20' : '',
|
||||
isResponseRuleDragging(endpoint.id, index) ? 'opacity-60 border-sky-500 bg-sky-500/5' : '',
|
||||
isResponseRuleDragOver(endpoint.id, index) ? 'ring-1 ring-sky-500/40 bg-sky-500/10' : ''
|
||||
]"
|
||||
@@ -480,6 +488,12 @@
|
||||
class="text-[10px] font-semibold text-sky-600 dark:text-sky-400 shrink-0"
|
||||
title="响应头"
|
||||
>R</span>
|
||||
<Switch
|
||||
:model-value="rule.enabled"
|
||||
class="shrink-0 scale-75 origin-center"
|
||||
:title="rule.enabled ? '已启用,点击禁用这条响应头规则' : '已禁用,点击启用这条响应头规则'"
|
||||
@update:model-value="(v: boolean) => updateEndpointResponseRuleEnabled(endpoint.id, index, v)"
|
||||
/>
|
||||
<Select
|
||||
:model-value="rule.action"
|
||||
:open="responseRuleSelectOpen[`${endpoint.id}-${index}`]"
|
||||
@@ -657,6 +671,7 @@
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md border-l-4 border-muted-foreground/40 bg-muted/30"
|
||||
:class="[
|
||||
!rule.enabled ? 'opacity-60 border-muted-foreground/25 bg-muted/20' : '',
|
||||
isBodyRuleDragging(endpoint.id, index) ? 'opacity-60 border-muted-foreground/70 bg-muted/50' : '',
|
||||
isBodyRuleDragOver(endpoint.id, index) ? 'ring-1 ring-muted-foreground/40 bg-muted/40' : ''
|
||||
]"
|
||||
@@ -678,6 +693,12 @@
|
||||
class="text-[10px] font-semibold text-muted-foreground shrink-0"
|
||||
title="请求体"
|
||||
>B</span>
|
||||
<Switch
|
||||
:model-value="rule.enabled"
|
||||
class="shrink-0 scale-75 origin-center"
|
||||
:title="rule.enabled ? '已启用,点击禁用这条请求体规则' : '已禁用,点击启用这条请求体规则'"
|
||||
@update:model-value="(v: boolean) => updateEndpointBodyRuleEnabled(endpoint.id, index, v)"
|
||||
/>
|
||||
<Select
|
||||
:model-value="rule.action"
|
||||
:open="bodyRuleSelectOpen[`${endpoint.id}-${index}`]"
|
||||
@@ -995,6 +1016,7 @@ import {
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Collapsible,
|
||||
CollapsibleTrigger,
|
||||
CollapsibleContent,
|
||||
@@ -1036,6 +1058,7 @@ import {
|
||||
// 编辑用的规则类型(统一的可编辑结构)
|
||||
interface EditableRule {
|
||||
action: 'set' | 'drop' | 'rename'
|
||||
enabled: boolean
|
||||
key: string // set/drop 用
|
||||
value: string // set 用
|
||||
from: string // rename 用
|
||||
@@ -1048,6 +1071,7 @@ type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_r
|
||||
|
||||
interface EditableBodyRule {
|
||||
action: BodyRuleAction
|
||||
enabled: boolean
|
||||
path: string // set/drop/append/insert/regex_replace 用
|
||||
value: string // set/append/insert 用(JSON 格式)
|
||||
from: string // rename 用
|
||||
@@ -1496,6 +1520,9 @@ function requireJsonString(rule: Record<string, unknown>, key: string, label: st
|
||||
|
||||
function validateHeaderRuleJson(rule: unknown, label: string, index: number): string | null {
|
||||
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
||||
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
||||
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
||||
}
|
||||
const action = rule.action
|
||||
if (action !== 'set' && action !== 'drop' && action !== 'rename') {
|
||||
return `${label}第 ${index + 1} 条:action 必须是 set/drop/rename`
|
||||
@@ -1516,6 +1543,9 @@ function validateHeaderRuleJson(rule: unknown, label: string, index: number): st
|
||||
|
||||
function validateBodyRuleJson(rule: unknown, label: string, index: number): string | null {
|
||||
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
||||
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
||||
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
||||
}
|
||||
const action = typeof rule.action === 'string' ? rule.action : ''
|
||||
if (!BODY_RULE_JSON_ACTIONS.has(action)) {
|
||||
return `${label}第 ${index + 1} 条:action 无效`
|
||||
@@ -1940,7 +1970,7 @@ async function clearEndpointProxy(endpoint: ProviderEndpoint) {
|
||||
}
|
||||
|
||||
function emptyHeaderRule(): EditableRule {
|
||||
return { action: 'set', key: '', value: '', from: '', to: '', condition: null }
|
||||
return { action: 'set', enabled: true, key: '', value: '', from: '', to: '', condition: null }
|
||||
}
|
||||
|
||||
function editableHeaderRulesFromRules(rules: HeaderRule[] | null | undefined): EditableRule[] {
|
||||
@@ -1948,11 +1978,11 @@ function editableHeaderRulesFromRules(rules: HeaderRule[] | null | undefined): E
|
||||
const editableRules: EditableRule[] = []
|
||||
for (const rule of rules) {
|
||||
if (rule.action === 'set') {
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'set', key: rule.key, value: rule.value || '', condition: conditionToEditable(rule.condition) })
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'set', enabled: rule.enabled !== false, key: rule.key, value: rule.value || '', condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'drop') {
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'drop', key: rule.key, condition: conditionToEditable(rule.condition) })
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'drop', enabled: rule.enabled !== false, key: rule.key, condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'rename') {
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'rename', from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
||||
editableRules.push({ ...emptyHeaderRule(), action: 'rename', enabled: rule.enabled !== false, from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
||||
}
|
||||
}
|
||||
return editableRules
|
||||
@@ -1961,6 +1991,7 @@ function editableHeaderRulesFromRules(rules: HeaderRule[] | null | undefined): E
|
||||
function emptyBodyRule(action: BodyRuleAction = 'set'): EditableBodyRule {
|
||||
return {
|
||||
action,
|
||||
enabled: true,
|
||||
path: '',
|
||||
value: '',
|
||||
from: '',
|
||||
@@ -1981,20 +2012,21 @@ function editableBodyRulesFromRules(rules: BodyRule[] | null | undefined): Edita
|
||||
for (const rule of rules) {
|
||||
if (rule.action === 'set') {
|
||||
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
||||
bodyRules.push({ ...emptyBodyRule('set'), path: rule.path, value, condition: conditionToEditable(rule.condition) })
|
||||
bodyRules.push({ ...emptyBodyRule('set'), enabled: rule.enabled !== false, path: rule.path, value, condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'drop') {
|
||||
bodyRules.push({ ...emptyBodyRule('drop'), path: rule.path, condition: conditionToEditable(rule.condition) })
|
||||
bodyRules.push({ ...emptyBodyRule('drop'), enabled: rule.enabled !== false, path: rule.path, condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'rename') {
|
||||
bodyRules.push({ ...emptyBodyRule('rename'), from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
||||
bodyRules.push({ ...emptyBodyRule('rename'), enabled: rule.enabled !== false, from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'append') {
|
||||
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
||||
bodyRules.push({ ...emptyBodyRule('append'), path: rule.path || '', value, condition: conditionToEditable(rule.condition) })
|
||||
bodyRules.push({ ...emptyBodyRule('append'), enabled: rule.enabled !== false, path: rule.path || '', value, condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'insert') {
|
||||
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
||||
bodyRules.push({ ...emptyBodyRule('insert'), path: rule.path || '', value, index: String(rule.index ?? ''), condition: conditionToEditable(rule.condition) })
|
||||
bodyRules.push({ ...emptyBodyRule('insert'), enabled: rule.enabled !== false, path: rule.path || '', value, index: String(rule.index ?? ''), condition: conditionToEditable(rule.condition) })
|
||||
} else if (rule.action === 'regex_replace') {
|
||||
bodyRules.push({
|
||||
...emptyBodyRule('regex_replace'),
|
||||
enabled: rule.enabled !== false,
|
||||
path: rule.path || '',
|
||||
pattern: rule.pattern || '',
|
||||
replacement: rule.replacement || '',
|
||||
@@ -2190,7 +2222,8 @@ function updateEndpointRuleAction(endpointId: string, index: number, action: 'se
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
if (rules[index]) {
|
||||
const currentCondition = rules[index].condition
|
||||
rules[index] = { ...emptyHeaderRule(), action, condition: currentCondition }
|
||||
const currentEnabled = rules[index].enabled
|
||||
rules[index] = { ...emptyHeaderRule(), action, enabled: currentEnabled, condition: currentCondition }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2198,7 +2231,22 @@ function updateEndpointResponseRuleAction(endpointId: string, index: number, act
|
||||
const rules = getEndpointEditResponseRules(endpointId)
|
||||
if (rules[index]) {
|
||||
const currentCondition = rules[index].condition
|
||||
rules[index] = { ...emptyHeaderRule(), action, condition: currentCondition }
|
||||
const currentEnabled = rules[index].enabled
|
||||
rules[index] = { ...emptyHeaderRule(), action, enabled: currentEnabled, condition: currentCondition }
|
||||
}
|
||||
}
|
||||
|
||||
function updateEndpointRuleEnabled(endpointId: string, index: number, enabled: boolean) {
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
if (rules[index]) {
|
||||
rules[index].enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
function updateEndpointResponseRuleEnabled(endpointId: string, index: number, enabled: boolean) {
|
||||
const rules = getEndpointEditResponseRules(endpointId)
|
||||
if (rules[index]) {
|
||||
rules[index].enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2269,8 +2317,10 @@ function validateRuleKeyForEndpoint(endpointId: string, key: string, index: numb
|
||||
}
|
||||
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index && (
|
||||
(r, i) => i !== index && r.enabled && (
|
||||
((r.action === 'set' || r.action === 'drop') && r.key.trim().toLowerCase() === trimmedKey) ||
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedKey)
|
||||
)
|
||||
@@ -2288,8 +2338,10 @@ function validateRenameFromForEndpoint(endpointId: string, from: string, index:
|
||||
if (!trimmedFrom) return null
|
||||
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index &&
|
||||
(r, i) => i !== index && r.enabled &&
|
||||
((r.action === 'set' && r.key.trim().toLowerCase() === trimmedFrom) ||
|
||||
(r.action === 'drop' && r.key.trim().toLowerCase() === trimmedFrom) ||
|
||||
(r.action === 'rename' && r.from.trim().toLowerCase() === trimmedFrom))
|
||||
@@ -2311,8 +2363,10 @@ function validateRenameToForEndpoint(endpointId: string, to: string, index: numb
|
||||
}
|
||||
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index &&
|
||||
(r, i) => i !== index && r.enabled &&
|
||||
((r.action === 'set' && r.key.trim().toLowerCase() === trimmedTo) ||
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedTo))
|
||||
)
|
||||
@@ -2396,7 +2450,15 @@ function updateEndpointBodyRuleAction(endpointId: string, index: number, action:
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
if (rules[index]) {
|
||||
const currentCondition = rules[index].condition
|
||||
rules[index] = { ...emptyBodyRule(action), condition: currentCondition }
|
||||
const currentEnabled = rules[index].enabled
|
||||
rules[index] = { ...emptyBodyRule(action), enabled: currentEnabled, condition: currentCondition }
|
||||
}
|
||||
}
|
||||
|
||||
function updateEndpointBodyRuleEnabled(endpointId: string, index: number, enabled: boolean) {
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
if (rules[index]) {
|
||||
rules[index].enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2451,9 +2513,10 @@ function validateBodyRulePathForEndpoint(endpointId: string, path: string, index
|
||||
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
// 任意一方启用了条件,则不视为冲突(条件可能互斥,真正冲突在运行时处理)
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index && !currentRule.condition && !r.condition && (
|
||||
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition && (
|
||||
((r.action === 'set' || r.action === 'drop') && r.path.trim().toLowerCase() === normalizedPath) ||
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedPath)
|
||||
)
|
||||
@@ -2484,8 +2547,9 @@ function validateBodyRenameFromForEndpoint(endpointId: string, from: string, ind
|
||||
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index && !currentRule.condition && !r.condition &&
|
||||
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition &&
|
||||
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedFrom) ||
|
||||
(r.action === 'drop' && r.path.trim().toLowerCase() === normalizedFrom) ||
|
||||
(r.action === 'rename' && r.from.trim().toLowerCase() === normalizedFrom))
|
||||
@@ -2516,8 +2580,9 @@ function validateBodyRenameToForEndpoint(endpointId: string, to: string, index:
|
||||
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index && !currentRule.condition && !r.condition &&
|
||||
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition &&
|
||||
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedTo) ||
|
||||
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedTo))
|
||||
)
|
||||
@@ -2712,6 +2777,7 @@ function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
|
||||
const original = originalRules[i]
|
||||
if (!original) return true
|
||||
if (edited.action !== original.action) return true
|
||||
if (edited.enabled !== (original.enabled !== false)) return true
|
||||
if (edited.action === 'set' && original.action === 'set') {
|
||||
const baseline = initBodyRuleSetValueForEditor(original.value)
|
||||
if (edited.path !== original.path) return true
|
||||
@@ -2747,24 +2813,25 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
|
||||
for (const rule of rules) {
|
||||
const condition = editableConditionToApi(rule.condition)
|
||||
const common = { ...(rule.enabled ? {} : { enabled: false }), ...(condition ? { condition } : {}) }
|
||||
if (rule.action === 'set' && rule.path.trim()) {
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
result.push({ action: 'set', path: rule.path.trim(), value, ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'set', path: rule.path.trim(), value, ...common })
|
||||
} else if (rule.action === 'drop' && rule.path.trim()) {
|
||||
result.push({ action: 'drop', path: rule.path.trim(), ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'drop', path: rule.path.trim(), ...common })
|
||||
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...common })
|
||||
} else if (rule.action === 'append' && rule.path.trim()) {
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
result.push({ action: 'append', path: rule.path.trim(), value, ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'append', path: rule.path.trim(), value, ...common })
|
||||
} else if (rule.action === 'insert' && rule.path.trim()) {
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
if (!isStrictIntegerString(rule.index)) continue
|
||||
const idx = parseInt(rule.index.trim(), 10)
|
||||
result.push({ action: 'insert', path: rule.path.trim(), index: idx, value, ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'insert', path: rule.path.trim(), index: idx, value, ...common })
|
||||
} else if (rule.action === 'regex_replace' && rule.path.trim() && rule.pattern.trim()) {
|
||||
const entry: BodyRuleRegexReplace = {
|
||||
action: 'regex_replace',
|
||||
@@ -2774,7 +2841,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
...(rule.flags.trim() ? { flags: rule.flags.trim() } : {}),
|
||||
...(isStrictNonNegativeIntegerString(rule.count) ? { count: parseInt(rule.count.trim(), 10) } : {}),
|
||||
}
|
||||
result.push({ ...entry, ...(condition ? { condition } : {}) })
|
||||
result.push({ ...entry, ...common })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2785,6 +2852,7 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
||||
const rules = getEndpointEditBodyRules(endpointId)
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条请求体规则:`
|
||||
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
@@ -2868,6 +2936,7 @@ function editableHeaderRulesChanged(edited: EditableRule[], originalRules: Heade
|
||||
const original = originalRules[i]
|
||||
if (!original) return true
|
||||
if (edited.action !== original.action) return true
|
||||
if (edited.enabled !== (original.enabled !== false)) return true
|
||||
if (edited.action === 'set' && original.action === 'set') {
|
||||
if (edited.key !== original.key || edited.value !== (original.value || '')) return true
|
||||
} else if (edited.action === 'drop' && original.action === 'drop') {
|
||||
@@ -2956,12 +3025,13 @@ function rulesToHeaderRules(rules: EditableRule[]): HeaderRule[] | null {
|
||||
|
||||
for (const rule of rules) {
|
||||
const condition = editableConditionToApi(rule.condition)
|
||||
const common = { ...(rule.enabled ? {} : { enabled: false }), ...(condition ? { condition } : {}) }
|
||||
if (rule.action === 'set' && rule.key.trim()) {
|
||||
result.push({ action: 'set', key: rule.key.trim(), value: rule.value, ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'set', key: rule.key.trim(), value: rule.value, ...common })
|
||||
} else if (rule.action === 'drop' && rule.key.trim()) {
|
||||
result.push({ action: 'drop', key: rule.key.trim(), ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'drop', key: rule.key.trim(), ...common })
|
||||
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...(condition ? { condition } : {}) })
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...common })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2982,6 +3052,7 @@ function getHeaderValidationErrorForEndpoint(endpointId: string): string | null
|
||||
const rules = getEndpointEditRules(endpointId)
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条请求头规则:`
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
const err = validateRuleKeyForEndpoint(endpointId, rule.key, i)
|
||||
@@ -3007,8 +3078,10 @@ function validateResponseHeaderNameForEndpoint(endpointId: string, name: string,
|
||||
}
|
||||
|
||||
const rules = getEndpointEditResponseRules(endpointId)
|
||||
const currentRule = rules[index]
|
||||
if (currentRule && !currentRule.enabled) return null
|
||||
const duplicate = rules.findIndex(
|
||||
(r, i) => i !== index && (
|
||||
(r, i) => i !== index && r.enabled && (
|
||||
((r.action === 'set' || r.action === 'drop') && r.key.trim().toLowerCase() === trimmedName) ||
|
||||
(r.action === 'rename' && (field === 'from'
|
||||
? r.from.trim().toLowerCase() === trimmedName
|
||||
@@ -3026,6 +3099,7 @@ function getResponseHeaderValidationErrorForEndpoint(endpointId: string): string
|
||||
const rules = getEndpointEditResponseRules(endpointId)
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (!rule.enabled) continue
|
||||
const prefix = `第 ${i + 1} 条响应头规则:`
|
||||
if (rule.action === 'set' || rule.action === 'drop') {
|
||||
const err = validateResponseHeaderNameForEndpoint(endpointId, rule.key, i, 'key')
|
||||
|
||||
@@ -37,6 +37,27 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 端点限制 -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label class="text-xs">限制端点</Label>
|
||||
<span class="text-xs text-muted-foreground">{{ endpointScopeSummary }}</span>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model="selectedEndpointIds"
|
||||
:options="endpointOptions"
|
||||
placeholder="全部端点"
|
||||
empty-text="暂无端点"
|
||||
no-results-text="未找到端点"
|
||||
trigger-class="h-9 rounded-md"
|
||||
dropdown-min-width="24rem"
|
||||
:search-threshold="4"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
默认对全部端点生效;选择端点后,此映射只在选中的端点上生效
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 映射名称选择面板 -->
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">提供商模型</Label>
|
||||
@@ -260,14 +281,17 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
type Model,
|
||||
type ProviderEndpoint,
|
||||
type ProviderModelAlias,
|
||||
type UpstreamModel,
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
||||
|
||||
export interface AliasGroup {
|
||||
@@ -276,6 +300,8 @@ export interface AliasGroup {
|
||||
apiFormatsKey: string
|
||||
/** @deprecated */
|
||||
apiFormats: string[]
|
||||
endpointIdsKey: string
|
||||
endpointIds: string[]
|
||||
aliases: ProviderModelAlias[]
|
||||
}
|
||||
|
||||
@@ -284,6 +310,7 @@ const props = defineProps<{
|
||||
providerId: string
|
||||
/** @deprecated */
|
||||
providerApiFormats?: string[]
|
||||
endpoints?: ProviderEndpoint[]
|
||||
models: Model[]
|
||||
editingGroup?: AliasGroup | null
|
||||
preselectedModelId?: string | null
|
||||
@@ -298,6 +325,11 @@ const emit = defineEmits<{
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
type EndpointOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// 状态
|
||||
const submitting = ref(false)
|
||||
const loadingModels = ref(false)
|
||||
@@ -323,9 +355,42 @@ const formData = ref<{
|
||||
// 选中的映射名称
|
||||
const selectedNames = ref<string[]>([])
|
||||
|
||||
// 选中的端点 ID;空数组表示全部端点
|
||||
const selectedEndpointIds = ref<string[]>([])
|
||||
|
||||
// 自定义名称列表(手动添加的)
|
||||
const allCustomNames = ref<string[]>([])
|
||||
|
||||
const endpointOptions = computed<EndpointOption[]>(() => {
|
||||
return (props.endpoints ?? []).map((endpoint) => {
|
||||
const status = endpoint.is_active ? '' : '(停用)'
|
||||
return {
|
||||
value: endpoint.id,
|
||||
label: `${formatApiFormat(endpoint.api_format)}${status}`,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const normalizedSelectedEndpointIds = computed(() => {
|
||||
const validIds = new Set(endpointOptions.value.map(option => option.value))
|
||||
const selected = normalizeStringList(selectedEndpointIds.value)
|
||||
if (selected.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const invalidSelected = selected.filter(endpointId => !validIds.has(endpointId))
|
||||
const selectedValidCount = selected.filter(endpointId => validIds.has(endpointId)).length
|
||||
if (validIds.size > 0 && invalidSelected.length === 0 && selectedValidCount === validIds.size) {
|
||||
return undefined
|
||||
}
|
||||
return selected
|
||||
})
|
||||
|
||||
const endpointScopeSummary = computed(() => {
|
||||
const selected = normalizedSelectedEndpointIds.value
|
||||
if (!selected || selected.length === 0) return '全部端点'
|
||||
return `${selected.length} 个端点`
|
||||
})
|
||||
|
||||
// 所有已知名称集合
|
||||
const allKnownNames = computed(() => {
|
||||
const set = new Set<string>()
|
||||
@@ -429,6 +494,50 @@ function toggleAllUpstreamModels() {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
function findDuplicateNames(
|
||||
existingAliases: ProviderModelAlias[],
|
||||
names: string[],
|
||||
endpointIds: string[] | undefined,
|
||||
apiFormats: string[] | undefined = undefined,
|
||||
): 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)
|
||||
})
|
||||
if (duplicate) duplicates.add(name)
|
||||
}
|
||||
return Array.from(duplicates)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
function toggleGroupCollapse(group: string) {
|
||||
if (collapsedGroups.value.has(group)) {
|
||||
@@ -484,12 +593,14 @@ function initForm() {
|
||||
}
|
||||
const existingNames = props.editingGroup.aliases.map(a => a.name)
|
||||
selectedNames.value = [...existingNames]
|
||||
selectedEndpointIds.value = normalizeStringList(props.editingGroup.endpointIds)
|
||||
allCustomNames.value = [...existingNames]
|
||||
} else {
|
||||
formData.value = {
|
||||
modelId: props.preselectedModelId || ''
|
||||
}
|
||||
selectedNames.value = []
|
||||
selectedEndpointIds.value = []
|
||||
allCustomNames.value = []
|
||||
}
|
||||
searchQuery.value = ''
|
||||
@@ -505,8 +616,11 @@ function handleModelChange(value: string) {
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
if (!formats || formats.length === 0) return ''
|
||||
return [...formats].sort().join(',')
|
||||
return getScopeKey(formats)
|
||||
}
|
||||
|
||||
function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
return getScopeKey(endpointIds)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -524,25 +638,33 @@ async function handleSubmit() {
|
||||
|
||||
const currentAliases = targetModel.provider_model_mappings || []
|
||||
let newAliases: ProviderModelAlias[]
|
||||
const nextEndpointIds = normalizedSelectedEndpointIds.value
|
||||
|
||||
const buildAliases = (names: string[]): ProviderModelAlias[] => {
|
||||
return names.map((name) => ({
|
||||
name: name.trim(),
|
||||
priority: 1
|
||||
}))
|
||||
return names.map((name) => {
|
||||
const alias: ProviderModelAlias = {
|
||||
name: name.trim(),
|
||||
priority: 1
|
||||
}
|
||||
if (nextEndpointIds && nextEndpointIds.length > 0) {
|
||||
alias.endpoint_ids = nextEndpointIds
|
||||
}
|
||||
return alias
|
||||
})
|
||||
}
|
||||
|
||||
if (props.editingGroup) {
|
||||
const oldApiFormatsKey = props.editingGroup.apiFormatsKey
|
||||
const oldEndpointIdsKey = props.editingGroup.endpointIdsKey
|
||||
const oldAliasNames = new Set(props.editingGroup.aliases.map(a => a.name))
|
||||
|
||||
const filteredAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
const currentKey = getApiFormatsKey(a.api_formats)
|
||||
return !(currentKey === oldApiFormatsKey && oldAliasNames.has(a.name))
|
||||
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
||||
return !(currentKey === oldApiFormatsKey && currentEndpointIdsKey === oldEndpointIdsKey && oldAliasNames.has(a.name))
|
||||
})
|
||||
|
||||
const existingNames = new Set(filteredAliases.map((a: ProviderModelAlias) => a.name))
|
||||
const duplicates = selectedNames.value.filter(name => existingNames.has(name))
|
||||
const duplicates = findDuplicateNames(filteredAliases, selectedNames.value, nextEndpointIds)
|
||||
if (duplicates.length > 0) {
|
||||
showError(`以下映射名称已存在:${duplicates.join(', ')}`, '错误')
|
||||
return
|
||||
@@ -553,8 +675,7 @@ async function handleSubmit() {
|
||||
...buildAliases(selectedNames.value)
|
||||
]
|
||||
} else {
|
||||
const existingNames = new Set(currentAliases.map((a: ProviderModelAlias) => a.name))
|
||||
const duplicates = selectedNames.value.filter(name => existingNames.has(name))
|
||||
const duplicates = findDuplicateNames(currentAliases, selectedNames.value, nextEndpointIds)
|
||||
if (duplicates.length > 0) {
|
||||
showError(`以下映射名称已存在:${duplicates.join(', ')}`, '错误')
|
||||
return
|
||||
|
||||
@@ -33,19 +33,19 @@
|
||||
>
|
||||
<div
|
||||
v-for="group in aliasGroups"
|
||||
:key="`${group.model.id}-${group.apiFormatsKey}`"
|
||||
:key="getAliasGroupKey(group)"
|
||||
class="transition-colors"
|
||||
>
|
||||
<!-- 分组头部(可点击展开) -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
@click="toggleAliasGroupExpand(`${group.model.id}-${group.apiFormatsKey}`)"
|
||||
@click="toggleAliasGroupExpand(getAliasGroupKey(group))"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<!-- 展开/收起图标 -->
|
||||
<ChevronRight
|
||||
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform"
|
||||
:class="{ 'rotate-90': expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`) }"
|
||||
:class="{ 'rotate-90': expandedAliasGroups.has(getAliasGroupKey(group)) }"
|
||||
/>
|
||||
<!-- 模型名称 -->
|
||||
<span class="font-semibold text-sm truncate">
|
||||
@@ -69,6 +69,12 @@
|
||||
>
|
||||
{{ formatApiFormat(format) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ getEndpointScopeLabel(group) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- 映射数量 -->
|
||||
<span class="text-xs text-muted-foreground shrink-0">
|
||||
@@ -103,7 +109,7 @@
|
||||
|
||||
<!-- 展开的映射列表 -->
|
||||
<div
|
||||
v-show="expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`)"
|
||||
v-show="expandedAliasGroups.has(getAliasGroupKey(group))"
|
||||
class="bg-muted/30 border-t border-border/30"
|
||||
>
|
||||
<div class="px-4 py-2 space-y-1">
|
||||
@@ -128,11 +134,11 @@
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
title="测试映射"
|
||||
:disabled="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
:disabled="testingMapping === `${getAliasGroupKey(group)}-${mapping.name}`"
|
||||
@click="testMapping(group, mapping)"
|
||||
>
|
||||
<Loader2
|
||||
v-if="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
v-if="testingMapping === `${getAliasGroupKey(group)}-${mapping.name}`"
|
||||
class="w-3 h-3 animate-spin"
|
||||
/>
|
||||
<Play
|
||||
@@ -239,8 +245,36 @@ const providerApiFormats = computed(() => {
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
if (!formats || formats.length === 0) return ''
|
||||
return [...formats].sort().join(',')
|
||||
return getScopeKey(formats)
|
||||
}
|
||||
|
||||
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 getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
return getScopeKey(endpointIds)
|
||||
}
|
||||
|
||||
function getAliasGroupKey(group: AliasGroup): string {
|
||||
return `${group.model.id}-${group.apiFormatsKey}-${group.endpointIdsKey}`
|
||||
}
|
||||
|
||||
function getEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
}
|
||||
|
||||
// 按"模型+作用域"分组的映射列表
|
||||
@@ -253,13 +287,16 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
|
||||
for (const alias of model.provider_model_mappings) {
|
||||
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}`
|
||||
const endpointIdsKey = getEndpointIdsKey(alias.endpoint_ids)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}|${endpointIdsKey}`
|
||||
|
||||
if (!groupMap.has(groupKey)) {
|
||||
const group: AliasGroup = {
|
||||
model,
|
||||
apiFormatsKey,
|
||||
apiFormats: alias.api_formats || [],
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
@@ -278,6 +315,7 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
const nameB = (b.model.global_model_display_name || b.model.provider_model_name || '').toLowerCase()
|
||||
if (nameA !== nameB) return nameA.localeCompare(nameB)
|
||||
return a.apiFormatsKey.localeCompare(b.apiFormatsKey)
|
||||
|| a.endpointIdsKey.localeCompare(b.endpointIdsKey)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,8 +337,9 @@ const deleteConfirmDescription = computed(() => {
|
||||
const { model, aliases, apiFormats } = deletingGroup.value
|
||||
const modelName = model.global_model_display_name || model.provider_model_name
|
||||
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => formatApiFormat(f)).join(', ')
|
||||
const endpointScope = getEndpointScopeLabel(deletingGroup.value)
|
||||
const aliasNames = aliases.map(a => a.name).join(', ')
|
||||
return `确定要删除模型「${modelName}」在作用域「${scopeText}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
return `确定要删除模型「${modelName}」在作用域「${scopeText} / ${endpointScope}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
})
|
||||
|
||||
// 切换映射组展开状态
|
||||
@@ -343,14 +382,15 @@ function deleteGroup(group: AliasGroup) {
|
||||
async function confirmDelete() {
|
||||
if (!deletingGroup.value) return
|
||||
|
||||
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
||||
const { model, aliases, apiFormatsKey, endpointIdsKey } = deletingGroup.value
|
||||
|
||||
try {
|
||||
const currentAliases = model.provider_model_mappings || []
|
||||
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
||||
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
const currentKey = getApiFormatsKey(a.api_formats)
|
||||
return !(currentKey === apiFormatsKey && aliasNamesToRemove.has(a.name))
|
||||
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
||||
return !(currentKey === apiFormatsKey && currentEndpointIdsKey === endpointIdsKey && aliasNamesToRemove.has(a.name))
|
||||
})
|
||||
|
||||
await updateModel(props.provider.id, model.id, {
|
||||
@@ -375,7 +415,7 @@ async function onDialogSaved() {
|
||||
|
||||
// 测试模型映射
|
||||
async function testMapping(group: AliasGroup, mapping: ProviderModelAlias) {
|
||||
const testingKey = `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`
|
||||
const testingKey = `${getAliasGroupKey(group)}-${mapping.name}`
|
||||
testingMapping.value = testingKey
|
||||
|
||||
try {
|
||||
|
||||
@@ -72,6 +72,13 @@
|
||||
<span class="text-xs text-muted-foreground shrink-0">
|
||||
| {{ item.mappings.length }} 个映射
|
||||
</span>
|
||||
<Badge
|
||||
v-if="item.group"
|
||||
variant="outline"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
{{ getGroupEndpointScopeLabel(item.group) }}
|
||||
</Badge>
|
||||
</template>
|
||||
<!-- 正则映射 -->
|
||||
<template v-else>
|
||||
@@ -290,6 +297,7 @@
|
||||
v-model:open="dialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:models="models"
|
||||
:endpoints="endpoints"
|
||||
:editing-group="editingGroup"
|
||||
:preselected-model-id="preselectedModelId"
|
||||
:has-auto-fetch-key="hasAutoFetchKey"
|
||||
@@ -446,8 +454,32 @@ const expandedItems = ref<Set<string>>(new Set())
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
if (!formats || formats.length === 0) return ''
|
||||
return [...formats].sort().join(',')
|
||||
return getScopeKey(formats)
|
||||
}
|
||||
|
||||
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 getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
return getScopeKey(endpointIds)
|
||||
}
|
||||
|
||||
function getGroupEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
}
|
||||
|
||||
// 精确映射分组(来自 provider_model_mappings)
|
||||
@@ -460,13 +492,16 @@ const exactMappingGroups = computed<AliasGroup[]>(() => {
|
||||
|
||||
for (const alias of model.provider_model_mappings) {
|
||||
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}`
|
||||
const endpointIdsKey = getEndpointIdsKey(alias.endpoint_ids)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}|${endpointIdsKey}`
|
||||
|
||||
if (!groupMap.has(groupKey)) {
|
||||
const group: AliasGroup = {
|
||||
model,
|
||||
apiFormatsKey,
|
||||
apiFormats: alias.api_formats || [],
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
@@ -587,7 +622,8 @@ const deleteConfirmDescription = computed(() => {
|
||||
const { model, aliases } = deletingGroup.value
|
||||
const modelName = model.global_model_display_name || model.provider_model_name
|
||||
const aliasNames = aliases.map(a => a.name).join(', ')
|
||||
return `确定要删除模型「${modelName}」的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
const endpointScope = getGroupEndpointScopeLabel(deletingGroup.value)
|
||||
return `确定要删除模型「${modelName}」在「${endpointScope}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
})
|
||||
|
||||
// 切换展开状态
|
||||
@@ -634,14 +670,15 @@ function deleteGroup(group: AliasGroup) {
|
||||
async function confirmDelete() {
|
||||
if (!deletingGroup.value) return
|
||||
|
||||
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
||||
const { model, aliases, apiFormatsKey, endpointIdsKey } = deletingGroup.value
|
||||
|
||||
try {
|
||||
const currentAliases = model.provider_model_mappings || []
|
||||
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
||||
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
const currentKey = getApiFormatsKey(a.api_formats)
|
||||
return !(currentKey === apiFormatsKey && aliasNamesToRemove.has(a.name))
|
||||
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
||||
return !(currentKey === apiFormatsKey && currentEndpointIdsKey === endpointIdsKey && aliasNamesToRemove.has(a.name))
|
||||
})
|
||||
|
||||
await updateModel(props.provider.id, model.id, {
|
||||
|
||||
Reference in New Issue
Block a user