2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
:model-value="internalOpen"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
title="端点管理"
|
|
|
|
|
|
:description="`管理 ${provider?.name} 的 API 端点`"
|
|
|
|
|
|
:icon="Settings"
|
|
|
|
|
|
size="2xl"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@update:model-value="handleDialogUpdate"
|
|
|
|
|
|
>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<div class="flex flex-col gap-4">
|
|
|
|
|
|
<!-- 已有端点列表(可滚动) -->
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="localEndpoints.length > 0"
|
2026-02-03 18:49:17 +08:00
|
|
|
|
class="space-y-3 max-h-[50vh] overflow-y-auto scrollbar-hide"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Label class="text-muted-foreground">已配置的端点</Label>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 端点卡片列表 -->
|
|
|
|
|
|
<div class="space-y-3">
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="endpoint in localEndpoints"
|
|
|
|
|
|
:key="endpoint.id"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
class="rounded-lg border bg-card"
|
|
|
|
|
|
:class="{ 'opacity-60': !endpoint.is_active }"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<!-- 卡片头部:格式名称 + 状态 + 操作 -->
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b">
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
2026-02-19 14:51:08 +08:00
|
|
|
|
<span class="font-medium">{{ formatApiFormat(endpoint.api_format) }}</span>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<Badge
|
|
|
|
|
|
v-if="!endpoint.is_active"
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
class="text-xs"
|
|
|
|
|
|
>
|
2026-03-03 11:32:41 +08:00
|
|
|
|
停用
|
2026-01-22 01:48:56 +08:00
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center gap-1.5">
|
2026-01-27 02:17:18 +08:00
|
|
|
|
<!-- 格式转换按钮 -->
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<span
|
|
|
|
|
|
class="mr-1"
|
|
|
|
|
|
:title="isEndpointFormatConversionDisabled ? formatConversionDisabledTooltip : (endpoint.format_acceptance_config?.enabled ? '已启用格式转换(点击关闭)' : '启用格式转换')"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
:class="`h-7 w-7 ${endpoint.format_acceptance_config?.enabled ? 'text-primary' : ''} ${isEndpointFormatConversionDisabled ? 'opacity-50' : ''}`"
|
|
|
|
|
|
:disabled="togglingFormatEndpointId === endpoint.id || isEndpointFormatConversionDisabled"
|
|
|
|
|
|
@click="handleToggleFormatConversion(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Shuffle class="w-3.5 h-3.5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</span>
|
2026-04-27 20:05:43 +08:00
|
|
|
|
<!-- 端点代理 -->
|
|
|
|
|
|
<Popover
|
|
|
|
|
|
:open="endpointProxyPopoverOpen[endpoint.id] || false"
|
|
|
|
|
|
@update:open="(open: boolean) => handleEndpointProxyPopoverToggle(endpoint.id, open)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<PopoverTrigger as-child>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7"
|
|
|
|
|
|
:class="endpointProxyNodeId(endpoint) ? 'text-blue-500' : ''"
|
|
|
|
|
|
:disabled="savingEndpointId === endpoint.id"
|
|
|
|
|
|
:title="getEndpointProxyTitle(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Globe class="w-3.5 h-3.5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
|
class="w-72 p-3 !z-[90]"
|
|
|
|
|
|
side="bottom"
|
|
|
|
|
|
align="end"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<span class="text-xs font-medium">端点代理节点</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="endpointProxyNodeId(endpoint)"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-6 px-2 text-[10px] text-muted-foreground"
|
|
|
|
|
|
:disabled="savingEndpointId === endpoint.id"
|
|
|
|
|
|
@click="clearEndpointProxy(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
清除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ProxyNodeSelect
|
|
|
|
|
|
:model-value="endpointProxyNodeId(endpoint)"
|
|
|
|
|
|
trigger-class="h-8"
|
|
|
|
|
|
@update:model-value="setEndpointProxy(endpoint, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p class="text-[10px] text-muted-foreground">
|
|
|
|
|
|
{{ endpointProxyNodeId(endpoint) ? '当前使用端点级代理' : '未设置时按提供商代理、系统代理继续兜底' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
2026-02-05 17:20:34 +08:00
|
|
|
|
<!-- 上游流式三态按钮 -->
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
:class="getUpstreamStreamButtonClass(endpoint)"
|
|
|
|
|
|
:title="getUpstreamStreamTooltip(endpoint)"
|
2026-03-01 23:55:26 +08:00
|
|
|
|
:disabled="savingEndpointId === endpoint.id || isUpstreamStreamPolicyLocked(endpoint)"
|
2026-02-05 17:20:34 +08:00
|
|
|
|
@click="handleCycleUpstreamStream(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Radio class="w-3.5 h-3.5" />
|
|
|
|
|
|
</Button>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<!-- 启用/停用 -->
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7"
|
|
|
|
|
|
:title="endpoint.is_active ? '停用' : '启用'"
|
|
|
|
|
|
:disabled="togglingEndpointId === endpoint.id"
|
|
|
|
|
|
@click="handleToggleEndpoint(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Power class="w-3.5 h-3.5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<!-- 删除 -->
|
|
|
|
|
|
<Button
|
2026-02-04 16:44:26 +08:00
|
|
|
|
v-if="!isFixedProvider"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
2026-02-04 02:04:54 +08:00
|
|
|
|
class="h-7 w-7 hover:text-destructive"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
title="删除"
|
|
|
|
|
|
:disabled="deletingEndpointId === endpoint.id"
|
|
|
|
|
|
@click="handleDeleteEndpoint(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 class="w-3.5 h-3.5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 卡片内容 -->
|
|
|
|
|
|
<div class="p-4 space-y-4">
|
|
|
|
|
|
<!-- URL 配置区 -->
|
|
|
|
|
|
<div class="flex items-end gap-3">
|
2026-02-05 17:20:34 +08:00
|
|
|
|
<div class="flex-1 min-w-0 grid grid-cols-3 gap-3">
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<div class="col-span-2 space-y-1.5">
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<Label class="text-xs text-muted-foreground">Base URL</Label>
|
|
|
|
|
|
<Input
|
2026-01-22 01:48:56 +08:00
|
|
|
|
:model-value="getEndpointEditState(endpoint.id)?.url ?? endpoint.base_url"
|
2026-01-20 01:32:47 +08:00
|
|
|
|
:placeholder="provider?.website || 'https://api.example.com'"
|
2026-02-04 10:24:25 +08:00
|
|
|
|
:disabled="isFixedProvider"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
@update:model-value="(v) => updateEndpointField(endpoint.id, 'url', v)"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label class="text-xs text-muted-foreground">自定义路径</Label>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<Input
|
2026-03-19 23:52:17 +08:00
|
|
|
|
:model-value="getDisplayedPath(endpoint)"
|
2026-02-05 09:48:37 +08:00
|
|
|
|
:placeholder="getDefaultPath(endpoint.api_format, endpoint.base_url) || '留空使用默认'"
|
2026-02-04 10:24:25 +08:00
|
|
|
|
:disabled="isFixedProvider"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
@update:model-value="(v) => updateEndpointField(endpoint.id, 'path', v)"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<!-- 保存/撤销按钮(URL/路径有修改时显示) -->
|
|
|
|
|
|
<div
|
2026-02-05 15:57:52 +08:00
|
|
|
|
v-if="hasUrlChanges(endpoint)"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
class="flex items-center gap-1 shrink-0"
|
2026-01-16 02:53:47 +08:00
|
|
|
|
>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-9 w-9"
|
|
|
|
|
|
title="保存"
|
|
|
|
|
|
:disabled="savingEndpointId === endpoint.id"
|
|
|
|
|
|
@click="saveEndpoint(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Check class="w-4 h-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-9 w-9"
|
|
|
|
|
|
title="撤销"
|
|
|
|
|
|
@click="resetEndpointChanges(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RotateCcw class="w-4 h-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
<!-- 请求/响应规则(请求头、请求体和响应头规则) -->
|
2026-02-03 18:49:17 +08:00
|
|
|
|
<Collapsible v-model:open="endpointRulesExpanded[endpoint.id]">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<!-- 有规则时显示可折叠的触发器 -->
|
|
|
|
|
|
<CollapsibleTrigger
|
|
|
|
|
|
v-if="getTotalRulesCount(endpoint) > 0"
|
|
|
|
|
|
as-child
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="flex items-center gap-2 py-1.5 px-2 -mx-2 rounded-md hover:bg-muted/50 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
class="w-4 h-4 transition-transform text-muted-foreground"
|
|
|
|
|
|
:class="{ 'rotate-90': endpointRulesExpanded[endpoint.id] }"
|
|
|
|
|
|
/>
|
2026-05-06 01:29:42 +08:00
|
|
|
|
<span class="text-sm font-medium">请求/响应规则</span>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
<Badge
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
class="text-xs"
|
2026-01-16 02:53:47 +08:00
|
|
|
|
>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
{{ getTotalRulesCount(endpoint) }} 条
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
|
<!-- 没有规则时只显示标题 -->
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="text-sm text-muted-foreground py-1.5"
|
|
|
|
|
|
>
|
2026-05-06 01:29:42 +08:00
|
|
|
|
请求/响应规则
|
2026-02-03 18:49:17 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<div class="flex-1" />
|
|
|
|
|
|
<div class="flex items-center gap-1 shrink-0">
|
|
|
|
|
|
<Button
|
2026-05-06 21:30:13 +08:00
|
|
|
|
v-if="hasRulePanelChanges(endpoint)"
|
2026-02-03 18:49:17 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7"
|
|
|
|
|
|
title="保存规则"
|
|
|
|
|
|
:disabled="savingEndpointId === endpoint.id"
|
|
|
|
|
|
@click="saveEndpoint(endpoint)"
|
|
|
|
|
|
>
|
2026-02-05 20:13:41 +08:00
|
|
|
|
<Save class="w-3.5 h-3.5" />
|
2026-02-03 18:49:17 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
2026-05-06 21:30:13 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 text-xs px-2"
|
|
|
|
|
|
:title="isEndpointRulesJsonMode(endpoint.id) ? '切回表单视图' : '切到 JSON 视图'"
|
|
|
|
|
|
@click="toggleEndpointRulesJsonMode(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Code2 class="w-3 h-3 mr-1" />
|
|
|
|
|
|
{{ isEndpointRulesJsonMode(endpoint.id) ? '表单' : 'JSON' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="isEndpointRulesJsonMode(endpoint.id)"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 px-2 text-xs"
|
|
|
|
|
|
title="格式化 JSON"
|
|
|
|
|
|
@click="formatEndpointRulesJson(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<AlignLeft class="w-3 h-3 mr-1" />
|
|
|
|
|
|
格式化
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="!isEndpointRulesJsonMode(endpoint.id)"
|
2026-02-03 18:49:17 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 text-xs px-2"
|
|
|
|
|
|
title="添加请求头规则"
|
|
|
|
|
|
@click="handleAddEndpointRule(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus class="w-3 h-3 mr-1" />
|
|
|
|
|
|
请求头
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
2026-05-06 21:30:13 +08:00
|
|
|
|
v-if="!isEndpointRulesJsonMode(endpoint.id)"
|
2026-02-03 18:49:17 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 text-xs px-2"
|
|
|
|
|
|
title="添加请求体规则"
|
|
|
|
|
|
@click="handleAddEndpointBodyRule(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus class="w-3 h-3 mr-1" />
|
|
|
|
|
|
请求体
|
|
|
|
|
|
</Button>
|
2026-05-06 01:29:42 +08:00
|
|
|
|
<Button
|
2026-05-06 21:30:13 +08:00
|
|
|
|
v-if="!isEndpointRulesJsonMode(endpoint.id)"
|
2026-05-06 01:29:42 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 text-xs px-2"
|
|
|
|
|
|
title="添加响应头规则"
|
|
|
|
|
|
@click="handleAddEndpointResponseRule(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus class="w-3 h-3 mr-1" />
|
|
|
|
|
|
响应头
|
|
|
|
|
|
</Button>
|
2026-03-02 22:08:39 +08:00
|
|
|
|
<Button
|
2026-04-07 02:50:19 +08:00
|
|
|
|
v-if="isFixedProvider && hasDefaultBodyRules(endpoint.api_format)"
|
2026-03-02 22:08:39 +08:00
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 text-xs px-2"
|
|
|
|
|
|
title="重置请求体"
|
|
|
|
|
|
:disabled="resettingDefaultRulesEndpointId === endpoint.id"
|
|
|
|
|
|
@click="handleResetBodyRulesToDefault(endpoint)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RotateCcw class="w-3 h-3 mr-1" />
|
|
|
|
|
|
重置请求体
|
|
|
|
|
|
</Button>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<CollapsibleContent class="pt-3">
|
2026-05-06 21:30:13 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="isEndpointRulesJsonMode(endpoint.id)"
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
:model-value="getEndpointRulesJsonDraft(endpoint)"
|
2026-05-06 21:45:37 +08:00
|
|
|
|
class="min-h-[220px] font-mono text-xs leading-relaxed"
|
2026-05-06 21:30:13 +08:00
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
placeholder="{ "header_rules": [], "body_rules": [], "response_header_rules": [] }"
|
|
|
|
|
|
@update:model-value="(value) => updateEndpointRulesJsonDraft(endpoint.id, value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="endpointRulesJsonError[endpoint.id]"
|
|
|
|
|
|
class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ endpointRulesJsonError[endpoint.id] }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
>
|
2026-03-02 18:02:21 +08:00
|
|
|
|
<div
|
2026-05-06 01:29:42 +08:00
|
|
|
|
v-if="getEndpointRulesCount(endpoint) > 1 || getEndpointBodyRulesCount(endpoint) > 1 || getEndpointResponseRulesCount(endpoint) > 1"
|
2026-03-02 18:02:21 +08:00
|
|
|
|
class="flex items-center gap-1.5 text-xs text-muted-foreground px-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<GripVertical class="w-3.5 h-3.5" />
|
|
|
|
|
|
<span>拖拽左侧手柄可调整规则执行顺序</span>
|
|
|
|
|
|
</div>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
<!-- 请求头规则列表 - 主题色边框 -->
|
2026-03-15 20:27:53 +08:00
|
|
|
|
<template
|
2026-02-03 18:49:17 +08:00
|
|
|
|
v-for="(rule, index) in getEndpointEditRules(endpoint.id)"
|
|
|
|
|
|
:key="`header-${index}`"
|
|
|
|
|
|
>
|
2026-03-15 20:27:53 +08:00
|
|
|
|
<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="[
|
2026-05-07 00:48:15 +08:00
|
|
|
|
!rule.enabled ? 'opacity-60 border-primary/25 bg-muted/20' : '',
|
2026-03-15 20:27:53 +08:00
|
|
|
|
isHeaderRuleDragging(endpoint.id, index) ? 'opacity-60 border-primary bg-primary/5' : '',
|
|
|
|
|
|
isHeaderRuleDragOver(endpoint.id, index) ? 'ring-1 ring-primary/40 bg-primary/10' : ''
|
|
|
|
|
|
]"
|
|
|
|
|
|
@dragover.prevent="handleHeaderRuleDragOver(endpoint.id, index)"
|
|
|
|
|
|
@dragleave="handleHeaderRuleDragLeave(endpoint.id, index)"
|
|
|
|
|
|
@drop.prevent="handleHeaderRuleDrop(endpoint.id, index)"
|
2026-03-02 18:02:21 +08:00
|
|
|
|
>
|
2026-03-15 20:27:53 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="h-7 w-6 shrink-0 inline-flex items-center justify-center rounded-sm text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted cursor-grab active:cursor-grabbing"
|
|
|
|
|
|
title="拖拽排序"
|
|
|
|
|
|
draggable="true"
|
|
|
|
|
|
@dragstart="(e) => handleHeaderRuleDragStart(endpoint.id, index, e)"
|
|
|
|
|
|
@dragend="() => handleHeaderRuleDragEnd(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<GripVertical class="w-3.5 h-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="text-[10px] font-semibold text-primary shrink-0"
|
|
|
|
|
|
title="请求头"
|
|
|
|
|
|
>H</span>
|
2026-05-07 00:48:15 +08:00
|
|
|
|
<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)"
|
|
|
|
|
|
/>
|
2026-03-15 20:27:53 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
:model-value="rule.action"
|
|
|
|
|
|
:open="ruleSelectOpen[`${endpoint.id}-${index}`]"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleAction(endpoint.id, index, v as 'set' | 'drop' | 'rename')"
|
|
|
|
|
|
@update:open="(v) => handleRuleSelectOpen(endpoint.id, index, v)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger class="w-[88px] h-7 text-xs shrink-0">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="set">
|
|
|
|
|
|
覆写
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="drop">
|
|
|
|
|
|
删除
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="rename">
|
|
|
|
|
|
重命名
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
|
|
|
|
|
:class="rule.condition ? 'text-primary' : ''"
|
|
|
|
|
|
title="条件触发"
|
|
|
|
|
|
@click="toggleEndpointRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Filter class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<template v-if="rule.action === 'set'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.key"
|
|
|
|
|
|
placeholder="名称"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleField(endpoint.id, index, 'key', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">=</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.value"
|
|
|
|
|
|
placeholder="值"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleField(endpoint.id, index, 'value', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'drop'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.key"
|
|
|
|
|
|
placeholder="要删除的名称"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleField(endpoint.id, index, 'key', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'rename'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.from"
|
|
|
|
|
|
placeholder="原名"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleField(endpoint.id, index, 'from', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">→</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.to"
|
|
|
|
|
|
placeholder="新名"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointRuleField(endpoint.id, index, 'to', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
|
|
|
|
|
@click="removeEndpointRule(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<EndpointConditionEditor
|
|
|
|
|
|
v-if="rule.condition"
|
|
|
|
|
|
:model-value="rule.condition"
|
|
|
|
|
|
path-hint="请求体字段路径"
|
|
|
|
|
|
removable
|
|
|
|
|
|
@update:model-value="(condition) => updateEndpointRuleCondition(endpoint.id, index, condition)"
|
|
|
|
|
|
@remove="clearEndpointRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
<!-- 响应头规则列表 -->
|
|
|
|
|
|
<template
|
|
|
|
|
|
v-for="(rule, index) in getEndpointEditResponseRules(endpoint.id)"
|
|
|
|
|
|
:key="`response-header-${index}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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="[
|
2026-05-07 00:48:15 +08:00
|
|
|
|
!rule.enabled ? 'opacity-60 border-sky-500/25 bg-muted/20' : '',
|
2026-05-06 01:29:42 +08:00
|
|
|
|
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' : ''
|
|
|
|
|
|
]"
|
|
|
|
|
|
@dragover.prevent="handleResponseRuleDragOver(endpoint.id, index)"
|
|
|
|
|
|
@dragleave="handleResponseRuleDragLeave(endpoint.id, index)"
|
|
|
|
|
|
@drop.prevent="handleResponseRuleDrop(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="h-7 w-6 shrink-0 inline-flex items-center justify-center rounded-sm text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted cursor-grab active:cursor-grabbing"
|
|
|
|
|
|
title="拖拽排序"
|
|
|
|
|
|
draggable="true"
|
|
|
|
|
|
@dragstart="(e) => handleResponseRuleDragStart(endpoint.id, index, e)"
|
|
|
|
|
|
@dragend="() => handleResponseRuleDragEnd(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<GripVertical class="w-3.5 h-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="text-[10px] font-semibold text-sky-600 dark:text-sky-400 shrink-0"
|
|
|
|
|
|
title="响应头"
|
|
|
|
|
|
>R</span>
|
2026-05-07 00:48:15 +08:00
|
|
|
|
<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)"
|
|
|
|
|
|
/>
|
2026-05-06 01:29:42 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
:model-value="rule.action"
|
|
|
|
|
|
:open="responseRuleSelectOpen[`${endpoint.id}-${index}`]"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleAction(endpoint.id, index, v as 'set' | 'drop' | 'rename')"
|
|
|
|
|
|
@update:open="(v) => handleResponseRuleSelectOpen(endpoint.id, index, v)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger class="w-[88px] h-7 text-xs shrink-0">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="set">
|
|
|
|
|
|
覆写
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="drop">
|
|
|
|
|
|
删除
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="rename">
|
|
|
|
|
|
重命名
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
|
|
|
|
|
:class="rule.condition ? 'text-primary' : ''"
|
|
|
|
|
|
title="条件触发"
|
|
|
|
|
|
@click="toggleEndpointResponseRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Filter class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<template v-if="rule.action === 'set'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.key"
|
|
|
|
|
|
placeholder="响应头名称"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleField(endpoint.id, index, 'key', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">=</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.value"
|
|
|
|
|
|
placeholder="值"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleField(endpoint.id, index, 'value', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'drop'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.key"
|
|
|
|
|
|
placeholder="要删除的响应头"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleField(endpoint.id, index, 'key', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'rename'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.from"
|
|
|
|
|
|
placeholder="原名"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleField(endpoint.id, index, 'from', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">→</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.to"
|
|
|
|
|
|
placeholder="新名"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointResponseRuleField(endpoint.id, index, 'to', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
|
|
|
|
|
@click="removeEndpointResponseRule(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<EndpointConditionEditor
|
|
|
|
|
|
v-if="rule.condition"
|
|
|
|
|
|
:model-value="rule.condition"
|
|
|
|
|
|
path-hint="响应体字段路径"
|
|
|
|
|
|
removable
|
|
|
|
|
|
@update:model-value="(condition) => updateEndpointResponseRuleCondition(endpoint.id, index, condition)"
|
|
|
|
|
|
@remove="clearEndpointResponseRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="getEndpointEditBodyRules(endpoint.id).length > 0"
|
2026-02-11 03:16:53 +08:00
|
|
|
|
class="flex items-center gap-1 text-xs text-muted-foreground px-2"
|
2026-02-05 20:13:41 +08:00
|
|
|
|
>
|
2026-02-21 14:59:37 +08:00
|
|
|
|
<span><code class="bg-muted px-1 rounded">.</code> 嵌套字段 / <code class="bg-muted px-1 rounded">[N]</code> 数组索引 / <code class="bg-muted px-1 rounded">[*]</code> 通配符;值为 JSON 格式</span>
|
2026-02-11 03:16:53 +08:00
|
|
|
|
<div class="flex-1" />
|
|
|
|
|
|
<Popover
|
|
|
|
|
|
:open="bodyRuleHelpOpenEndpointId === endpoint.id"
|
|
|
|
|
|
@update:open="(v: boolean) => setBodyRuleHelpOpen(endpoint.id, v)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<PopoverTrigger as-child>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="shrink-0 h-6 w-6 inline-flex items-center justify-center rounded-md hover:bg-muted/60"
|
|
|
|
|
|
title="规则说明"
|
|
|
|
|
|
aria-label="规则说明"
|
|
|
|
|
|
>
|
|
|
|
|
|
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground/60" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
|
side="bottom"
|
|
|
|
|
|
align="end"
|
|
|
|
|
|
:side-offset="6"
|
|
|
|
|
|
class="w-80 p-3 !z-[90]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="text-xs space-y-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium mb-0.5">
|
|
|
|
|
|
路径语法
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-muted-foreground">
|
|
|
|
|
|
<code>metadata.user_id</code> 嵌套字段<br>
|
|
|
|
|
|
<code>messages[0].content</code> 数组索引<br>
|
2026-02-21 14:59:37 +08:00
|
|
|
|
<code>tools[*].name</code> 通配符(遍历所有元素)<br>
|
|
|
|
|
|
<code>tools[0-4].name</code> 范围(遍历索引 0~4)<br>
|
2026-02-11 03:16:53 +08:00
|
|
|
|
<code>config\.v1.key</code> 转义点号
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium mb-0.5">
|
|
|
|
|
|
值格式 (JSON)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-muted-foreground">
|
|
|
|
|
|
<code>123</code> 数字 / <code>"text"</code> 字符串 / <code>true</code> 布尔<br>
|
|
|
|
|
|
<code>{"k":"v"}</code> 对象 / <code>[1,2]</code> 数组 / <code>null</code><br>
|
|
|
|
|
|
<code v-pre>{{$original}}</code> 引用原值
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium mb-0.5">
|
|
|
|
|
|
条件运算符
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-muted-foreground">
|
|
|
|
|
|
<code>eq</code> <code>neq</code> 等于/不等于<br>
|
|
|
|
|
|
<code>gt</code> <code>lt</code> <code>gte</code> <code>lte</code> 大小比较<br>
|
|
|
|
|
|
<code>starts_with</code> <code>ends_with</code> <code>contains</code> 字符串匹配<br>
|
|
|
|
|
|
<code>matches</code> 正则匹配<br>
|
|
|
|
|
|
<code>exists</code> <code>not_exists</code> 字段存在性<br>
|
|
|
|
|
|
<code>in</code> 在列表中(值填 <code>["a","b"]</code>)<br>
|
2026-02-21 14:59:37 +08:00
|
|
|
|
<code>type_is</code> 类型判断(string/number/boolean/array/object/null)<br>
|
2026-03-15 20:27:53 +08:00
|
|
|
|
条件路径支持 <code>$item.xxx</code> 引用通配符当前元素<br>
|
2026-05-06 23:48:31 +08:00
|
|
|
|
可切换 <code>请求体</code>/<code>请求头</code> 数据源,并支持 <code>ALL</code>/<code>ANY</code> 组合条件
|
2026-02-11 03:16:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-muted-foreground">
|
2026-05-06 23:48:31 +08:00
|
|
|
|
规则按顺序执行;条件判断使用客户端原始请求体或原始请求头,不受前面规则修改影响。
|
2026-02-11 03:16:53 +08:00
|
|
|
|
</div>
|
2026-02-21 14:59:37 +08:00
|
|
|
|
<div class="text-muted-foreground">
|
2026-05-06 23:48:31 +08:00
|
|
|
|
条件默认按客户端原始请求体匹配;切到 <code>请求头</code> 时按客户端原始请求头匹配。
|
2026-02-21 14:59:37 +08:00
|
|
|
|
</div>
|
2026-02-11 03:16:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
2026-02-05 20:13:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
<!-- 请求体规则列表 - 次要色边框 -->
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<template
|
2026-02-03 18:49:17 +08:00
|
|
|
|
v-for="(rule, index) in getEndpointEditBodyRules(endpoint.id)"
|
|
|
|
|
|
:key="`body-${index}`"
|
|
|
|
|
|
>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<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"
|
2026-03-02 18:02:21 +08:00
|
|
|
|
:class="[
|
2026-05-07 00:48:15 +08:00
|
|
|
|
!rule.enabled ? 'opacity-60 border-muted-foreground/25 bg-muted/20' : '',
|
2026-03-02 18:02:21 +08:00
|
|
|
|
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' : ''
|
|
|
|
|
|
]"
|
|
|
|
|
|
@dragover.prevent="handleBodyRuleDragOver(endpoint.id, index)"
|
|
|
|
|
|
@dragleave="handleBodyRuleDragLeave(endpoint.id, index)"
|
|
|
|
|
|
@drop.prevent="handleBodyRuleDrop(endpoint.id, index)"
|
2026-02-03 18:49:17 +08:00
|
|
|
|
>
|
2026-03-02 18:02:21 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="h-7 w-6 shrink-0 inline-flex items-center justify-center rounded-sm text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted cursor-grab active:cursor-grabbing"
|
|
|
|
|
|
title="拖拽排序"
|
|
|
|
|
|
draggable="true"
|
|
|
|
|
|
@dragstart="(e) => handleBodyRuleDragStart(endpoint.id, index, e)"
|
|
|
|
|
|
@dragend="() => handleBodyRuleDragEnd(endpoint.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<GripVertical class="w-3.5 h-3.5" />
|
|
|
|
|
|
</button>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<span
|
|
|
|
|
|
class="text-[10px] font-semibold text-muted-foreground shrink-0"
|
|
|
|
|
|
title="请求体"
|
|
|
|
|
|
>B</span>
|
2026-05-07 00:48:15 +08:00
|
|
|
|
<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)"
|
|
|
|
|
|
/>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
:model-value="rule.action"
|
|
|
|
|
|
:open="bodyRuleSelectOpen[`${endpoint.id}-${index}`]"
|
|
|
|
|
|
@update:model-value="(v: string) => updateEndpointBodyRuleAction(endpoint.id, index, v as BodyRuleAction)"
|
|
|
|
|
|
@update:open="(v) => handleBodyRuleSelectOpen(endpoint.id, index, v)"
|
|
|
|
|
|
>
|
2026-05-06 22:12:21 +08:00
|
|
|
|
<SelectTrigger class="w-[88px] h-7 text-xs shrink-0">
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="set">
|
|
|
|
|
|
覆写
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="drop">
|
|
|
|
|
|
删除
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="rename">
|
|
|
|
|
|
重命名
|
|
|
|
|
|
</SelectItem>
|
2026-04-22 20:16:57 +08:00
|
|
|
|
<SelectItem value="append">
|
|
|
|
|
|
追加
|
|
|
|
|
|
</SelectItem>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<SelectItem value="insert">
|
|
|
|
|
|
插入
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="regex_replace">
|
|
|
|
|
|
正则替换
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2026-02-11 03:16:53 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
2026-03-15 20:27:53 +08:00
|
|
|
|
:class="rule.condition ? 'text-primary' : ''"
|
2026-02-11 03:16:53 +08:00
|
|
|
|
title="条件触发"
|
|
|
|
|
|
@click="toggleBodyRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Filter class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<template v-if="rule.action === 'set'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.path"
|
|
|
|
|
|
placeholder="字段路径(如 metadata.user_id)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">=</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.value"
|
|
|
|
|
|
placeholder="123 / "text" / {{$original}}"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'value', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<CheckCircle
|
|
|
|
|
|
class="w-4 h-4 shrink-0"
|
|
|
|
|
|
:class="getBodySetValueValidation(rule) === true ? 'text-green-600' : getBodySetValueValidation(rule) === false ? 'text-destructive' : 'text-muted-foreground/40'"
|
|
|
|
|
|
:title="getBodySetValueValidationTip(rule)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'drop'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.path"
|
|
|
|
|
|
placeholder="要删除的字段路径"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'rename'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.from"
|
|
|
|
|
|
placeholder="原路径"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'from', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">→</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.to"
|
|
|
|
|
|
placeholder="新路径"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-1 min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'to', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
2026-04-22 20:16:57 +08:00
|
|
|
|
<template v-else-if="rule.action === 'append'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.path"
|
|
|
|
|
|
placeholder="数组路径(如 messages)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[2] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">+=</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.value"
|
|
|
|
|
|
placeholder="值 (JSON)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[3] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'value', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<CheckCircle
|
|
|
|
|
|
class="w-4 h-4 shrink-0"
|
|
|
|
|
|
:class="getBodySetValueValidation(rule) === true ? 'text-green-600' : getBodySetValueValidation(rule) === false ? 'text-destructive' : 'text-muted-foreground/40'"
|
|
|
|
|
|
:title="getBodySetValueValidationTip(rule)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'insert'">
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.path"
|
|
|
|
|
|
placeholder="数组路径(如 messages)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[2] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.index"
|
2026-04-22 20:16:57 +08:00
|
|
|
|
placeholder="位置"
|
2026-02-11 00:50:08 +08:00
|
|
|
|
size="sm"
|
|
|
|
|
|
class="w-14 h-7 text-xs shrink-0"
|
2026-04-22 20:16:57 +08:00
|
|
|
|
title="插入位置(支持负数)"
|
2026-02-11 00:50:08 +08:00
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'index', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.value"
|
|
|
|
|
|
placeholder="值 (JSON)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[3] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'value', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<CheckCircle
|
|
|
|
|
|
class="w-4 h-4 shrink-0"
|
|
|
|
|
|
:class="getBodySetValueValidation(rule) === true ? 'text-green-600' : getBodySetValueValidation(rule) === false ? 'text-destructive' : 'text-muted-foreground/40'"
|
|
|
|
|
|
:title="getBodySetValueValidationTip(rule)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="rule.action === 'regex_replace'">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.path"
|
|
|
|
|
|
placeholder="字段路径"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[2] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.pattern"
|
|
|
|
|
|
placeholder="正则"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[2] min-w-0 h-7 text-xs font-mono"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'pattern', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted-foreground text-xs">→</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.replacement"
|
|
|
|
|
|
placeholder="替换为"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="flex-[2] min-w-0 h-7 text-xs"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'replacement', v)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.flags"
|
|
|
|
|
|
placeholder="ims"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="w-12 h-7 text-xs shrink-0 font-mono"
|
|
|
|
|
|
title="正则标志:i=忽略大小写 m=多行 s=dotall"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'flags', v)"
|
|
|
|
|
|
/>
|
2026-04-22 20:16:57 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
:model-value="rule.count"
|
|
|
|
|
|
placeholder="全部"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="w-14 h-7 text-xs shrink-0"
|
|
|
|
|
|
title="替换次数;留空=默认全部,0=全部"
|
|
|
|
|
|
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'count', v)"
|
|
|
|
|
|
/>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
<CheckCircle
|
|
|
|
|
|
class="w-4 h-4 shrink-0"
|
|
|
|
|
|
:class="getRegexPatternValidation(rule) === true ? 'text-green-600' : getRegexPatternValidation(rule) === false ? 'text-destructive' : 'text-muted-foreground/40'"
|
|
|
|
|
|
:title="getRegexPatternValidationTip(rule)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-7 w-7 shrink-0"
|
|
|
|
|
|
@click="removeEndpointBodyRule(endpoint.id, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X class="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2026-03-15 20:27:53 +08:00
|
|
|
|
<EndpointConditionEditor
|
|
|
|
|
|
v-if="rule.condition"
|
|
|
|
|
|
:model-value="rule.condition"
|
|
|
|
|
|
:path-hint="getBodyRuleConditionPathPlaceholder(rule.path)"
|
|
|
|
|
|
removable
|
|
|
|
|
|
@update:model-value="(condition) => updateEndpointBodyRuleCondition(endpoint.id, index, condition)"
|
|
|
|
|
|
@remove="clearEndpointBodyRuleCondition(endpoint.id, index)"
|
|
|
|
|
|
/>
|
2026-02-11 00:50:08 +08:00
|
|
|
|
</template>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
</div>
|
2026-02-03 18:49:17 +08:00
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
|
</Collapsible>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-18 14:42:06 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
<!-- 添加新端点 -->
|
|
|
|
|
|
<div
|
2026-02-04 10:24:25 +08:00
|
|
|
|
v-if="!isFixedProvider && availableFormats.length > 0"
|
|
|
|
|
|
class="rounded-lg border border-dashed p-3"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<!-- 卡片头部:API 格式选择 + 添加按钮 -->
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-dashed">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
v-model="newEndpoint.api_format"
|
|
|
|
|
|
:open="formatSelectOpen"
|
|
|
|
|
|
@update:open="handleFormatSelectOpen"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger class="h-auto w-auto gap-1.5 !border-0 bg-transparent !shadow-none p-0 font-medium rounded-none flex-row-reverse !ring-0 !ring-offset-0 !outline-none [&>svg]:h-4 [&>svg]:w-4 [&>svg]:opacity-70">
|
|
|
|
|
|
<SelectValue placeholder="选择格式..." />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="format in availableFormats"
|
|
|
|
|
|
:key="format.value"
|
|
|
|
|
|
:value="format.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ format.label }}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2026-01-22 01:48:56 +08:00
|
|
|
|
<Button
|
2026-02-11 11:30:29 +08:00
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 px-3"
|
2026-01-22 01:48:56 +08:00
|
|
|
|
:disabled="!newEndpoint.api_format || (!newEndpoint.base_url?.trim() && !provider?.website?.trim()) || addingEndpoint"
|
|
|
|
|
|
@click="handleAddEndpoint"
|
|
|
|
|
|
>
|
2026-02-11 11:30:29 +08:00
|
|
|
|
添加
|
2026-01-22 01:48:56 +08:00
|
|
|
|
</Button>
|
2025-12-18 14:42:06 +08:00
|
|
|
|
</div>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<!-- 卡片内容:URL 配置 -->
|
|
|
|
|
|
<div class="p-4">
|
|
|
|
|
|
<div class="flex items-end gap-3">
|
|
|
|
|
|
<div class="flex-1 min-w-0 grid grid-cols-3 gap-3">
|
|
|
|
|
|
<div class="col-span-2 space-y-1.5">
|
|
|
|
|
|
<Label class="text-xs text-muted-foreground">Base URL</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="newEndpoint.base_url"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
:placeholder="provider?.website || 'https://api.example.com'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label class="text-xs text-muted-foreground">自定义路径</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="newEndpoint.custom_path"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
:placeholder="newEndpointDefaultPath || '留空使用默认'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-18 14:42:06 +08:00
|
|
|
|
</div>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="localEndpoints.length === 0 && availableFormats.length === 0"
|
|
|
|
|
|
class="text-center py-8 text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p>所有 API 格式都已配置</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
2026-01-10 18:43:53 +08:00
|
|
|
|
@click="handleClose"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
2026-01-10 18:43:53 +08:00
|
|
|
|
关闭
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
2026-01-16 18:24:52 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 删除端点确认弹窗 -->
|
|
|
|
|
|
<AlertDialog
|
|
|
|
|
|
:model-value="deleteConfirmOpen"
|
|
|
|
|
|
title="删除端点"
|
|
|
|
|
|
:description="deleteConfirmDescription"
|
|
|
|
|
|
confirm-text="删除"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
@update:model-value="deleteConfirmOpen = $event"
|
|
|
|
|
|
@confirm="confirmDeleteEndpoint"
|
|
|
|
|
|
@cancel="deleteConfirmOpen = false"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-01-10 18:43:53 +08:00
|
|
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
2025-12-12 20:22:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
2026-05-06 21:30:13 +08:00
|
|
|
|
Textarea,
|
2025-12-12 20:22:03 +08:00
|
|
|
|
Label,
|
2026-01-22 01:48:56 +08:00
|
|
|
|
Badge,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
Select,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
2026-05-07 00:48:15 +08:00
|
|
|
|
Switch,
|
2026-01-16 01:18:54 +08:00
|
|
|
|
Collapsible,
|
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
|
CollapsibleContent,
|
2026-02-11 03:16:53 +08:00
|
|
|
|
Popover,
|
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
|
PopoverContent,
|
2025-12-12 20:22:03 +08:00
|
|
|
|
} from '@/components/ui'
|
2026-05-06 21:30:13 +08:00
|
|
|
|
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle, GripVertical, Globe, Code2, AlignLeft } from 'lucide-vue-next'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2025-12-12 20:22:03 +08:00
|
|
|
|
import { log } from '@/utils/logger'
|
2026-01-16 18:24:52 +08:00
|
|
|
|
import AlertDialog from '@/components/common/AlertDialog.vue'
|
2026-03-15 20:27:53 +08:00
|
|
|
|
import EndpointConditionEditor from './EndpointConditionEditor.vue'
|
2026-04-27 20:05:43 +08:00
|
|
|
|
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
2026-05-18 17:35:36 +00:00
|
|
|
|
import { getDefaultEndpointPath, normalizeEndpointApiFormat } from './endpoint-default-paths'
|
2026-04-27 20:05:43 +08:00
|
|
|
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createEndpoint,
|
2026-03-02 22:08:39 +08:00
|
|
|
|
getDefaultBodyRules,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
updateEndpoint,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
deleteEndpoint,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
type ProviderEndpoint,
|
2026-01-16 01:18:54 +08:00
|
|
|
|
type ProviderWithEndpointsSummary,
|
|
|
|
|
|
type HeaderRule,
|
2026-02-03 18:49:17 +08:00
|
|
|
|
type BodyRule,
|
2026-02-09 02:56:21 +08:00
|
|
|
|
type BodyRuleRegexReplace,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} from '@/api/endpoints'
|
|
|
|
|
|
import { adminApi } from '@/api/admin'
|
2026-02-19 14:51:08 +08:00
|
|
|
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
2026-03-15 20:27:53 +08:00
|
|
|
|
import {
|
|
|
|
|
|
conditionEquals,
|
|
|
|
|
|
conditionToEditable,
|
|
|
|
|
|
createEmptyConditionLeaf,
|
|
|
|
|
|
editableConditionToApi,
|
|
|
|
|
|
getBodyRuleConditionPathPlaceholder,
|
|
|
|
|
|
type EditableConditionNode,
|
|
|
|
|
|
validateEditableCondition,
|
|
|
|
|
|
} from './endpoint-rule-condition'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-16 01:18:54 +08:00
|
|
|
|
// 编辑用的规则类型(统一的可编辑结构)
|
|
|
|
|
|
interface EditableRule {
|
|
|
|
|
|
action: 'set' | 'drop' | 'rename'
|
2026-05-07 00:48:15 +08:00
|
|
|
|
enabled: boolean
|
2026-01-16 01:18:54 +08:00
|
|
|
|
key: string // set/drop 用
|
|
|
|
|
|
value: string // set 用
|
|
|
|
|
|
from: string // rename 用
|
|
|
|
|
|
to: string // rename 用
|
2026-03-15 20:27:53 +08:00
|
|
|
|
condition: EditableConditionNode | null
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 编辑用的请求体规则类型
|
2026-05-06 22:12:21 +08:00
|
|
|
|
type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_replace'
|
2026-02-09 02:56:21 +08:00
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
interface EditableBodyRule {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
action: BodyRuleAction
|
2026-05-07 00:48:15 +08:00
|
|
|
|
enabled: boolean
|
2026-02-09 02:56:21 +08:00
|
|
|
|
path: string // set/drop/append/insert/regex_replace 用
|
|
|
|
|
|
value: string // set/append/insert 用(JSON 格式)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
from: string // rename 用
|
|
|
|
|
|
to: string // rename 用
|
2026-02-09 02:56:21 +08:00
|
|
|
|
index: string // insert 用(字符串输入,保存时解析为 int)
|
|
|
|
|
|
pattern: string // regex_replace 用
|
|
|
|
|
|
replacement: string // regex_replace 用
|
|
|
|
|
|
flags: string // regex_replace 用(i/m/s)
|
2026-04-22 20:16:57 +08:00
|
|
|
|
count: string // regex_replace 用(空=默认全部;0=全部)
|
2026-03-15 20:27:53 +08:00
|
|
|
|
condition: EditableConditionNode | null
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 端点编辑状态(仅 URL、路径、规则,格式转换是直接保存的)
|
|
|
|
|
|
interface EndpointEditState {
|
|
|
|
|
|
url: string
|
|
|
|
|
|
path: string
|
2026-02-05 15:57:52 +08:00
|
|
|
|
upstreamStreamPolicy: string
|
2026-01-22 01:48:56 +08:00
|
|
|
|
rules: EditableRule[]
|
2026-05-06 01:29:42 +08:00
|
|
|
|
responseRules: EditableRule[]
|
2026-02-03 18:49:17 +08:00
|
|
|
|
bodyRules: EditableBodyRule[]
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:13 +08:00
|
|
|
|
interface EndpointRulesJsonPayload {
|
|
|
|
|
|
header_rules: HeaderRule[]
|
|
|
|
|
|
body_rules: BodyRule[]
|
|
|
|
|
|
response_header_rules: HeaderRule[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
modelValue: boolean
|
|
|
|
|
|
provider: ProviderWithEndpointsSummary | null
|
2026-01-10 18:43:53 +08:00
|
|
|
|
endpoints?: ProviderEndpoint[]
|
2026-02-04 02:04:54 +08:00
|
|
|
|
systemFormatConversionEnabled?: boolean
|
|
|
|
|
|
providerFormatConversionEnabled?: boolean
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-02-04 23:59:45 +08:00
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:modelValue': [value: boolean]
|
|
|
|
|
|
'endpointCreated': []
|
|
|
|
|
|
'endpointUpdated': []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-02-04 02:04:54 +08:00
|
|
|
|
// 计算端点级格式转换是否应该被禁用
|
|
|
|
|
|
const isEndpointFormatConversionDisabled = computed(() => {
|
|
|
|
|
|
return props.systemFormatConversionEnabled || props.providerFormatConversionEnabled
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取禁用提示
|
|
|
|
|
|
const formatConversionDisabledTooltip = computed(() => {
|
|
|
|
|
|
if (props.systemFormatConversionEnabled) {
|
|
|
|
|
|
return '请先关闭系统级开关'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.providerFormatConversionEnabled) {
|
|
|
|
|
|
return '请先关闭提供商级开关'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const { success, error: showError } = useToast()
|
2026-04-27 20:05:43 +08:00
|
|
|
|
const proxyNodesStore = useProxyNodesStore()
|
2025-12-18 16:14:37 +08:00
|
|
|
|
|
2026-01-29 17:16:30 +08:00
|
|
|
|
// 规则 Select 的展开状态(与 Collapsible 分开管理)
|
|
|
|
|
|
const ruleSelectOpen = ref<Record<string, boolean>>({})
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const responseRuleSelectOpen = ref<Record<string, boolean>>({})
|
2026-01-29 17:16:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 打开规则选择器时关闭其他所有下拉
|
|
|
|
|
|
function handleRuleSelectOpen(endpointId: string, index: number, open: boolean) {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
formatSelectOpen.value = false
|
|
|
|
|
|
// 关闭其他 Select
|
|
|
|
|
|
Object.keys(ruleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
ruleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-05-06 01:29:42 +08:00
|
|
|
|
Object.keys(responseRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
responseRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-01-29 17:16:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
ruleSelectOpen.value[`${endpointId}-${index}`] = open
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
// 打开响应头规则选择器时关闭其他所有下拉
|
|
|
|
|
|
function handleResponseRuleSelectOpen(endpointId: string, index: number, open: boolean) {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
formatSelectOpen.value = false
|
|
|
|
|
|
Object.keys(ruleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
ruleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
|
|
|
|
|
Object.keys(responseRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
responseRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
|
|
|
|
|
Object.keys(bodyRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
bodyRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
responseRuleSelectOpen.value[`${endpointId}-${index}`] = open
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:16:30 +08:00
|
|
|
|
// 打开格式选择器时关闭其他所有下拉
|
|
|
|
|
|
function handleFormatSelectOpen(open: boolean) {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
// 关闭所有规则 Select
|
|
|
|
|
|
Object.keys(ruleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
ruleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-05-06 01:29:42 +08:00
|
|
|
|
Object.keys(responseRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
responseRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-02-03 18:49:17 +08:00
|
|
|
|
Object.keys(bodyRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
bodyRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-01-29 17:16:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
formatSelectOpen.value = open
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 打开请求体规则选择器时关闭其他所有下拉
|
|
|
|
|
|
function handleBodyRuleSelectOpen(endpointId: string, index: number, open: boolean) {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
formatSelectOpen.value = false
|
|
|
|
|
|
// 关闭所有 Select
|
|
|
|
|
|
Object.keys(ruleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
ruleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-05-06 01:29:42 +08:00
|
|
|
|
Object.keys(responseRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
responseRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
2026-02-03 18:49:17 +08:00
|
|
|
|
Object.keys(bodyRuleSelectOpen.value).forEach(key => {
|
|
|
|
|
|
bodyRuleSelectOpen.value[key] = false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
bodyRuleSelectOpen.value[`${endpointId}-${index}`] = open
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function clearHeaderRuleSelectOpen(endpointId: string) {
|
|
|
|
|
|
Object.keys(ruleSelectOpen.value).forEach((key) => {
|
|
|
|
|
|
if (key.startsWith(`${endpointId}-`)) {
|
|
|
|
|
|
delete ruleSelectOpen.value[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function clearResponseRuleSelectOpen(endpointId: string) {
|
|
|
|
|
|
Object.keys(responseRuleSelectOpen.value).forEach((key) => {
|
|
|
|
|
|
if (key.startsWith(`${endpointId}-`)) {
|
|
|
|
|
|
delete responseRuleSelectOpen.value[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function clearBodyRuleSelectOpen(endpointId: string) {
|
|
|
|
|
|
Object.keys(bodyRuleSelectOpen.value).forEach((key) => {
|
|
|
|
|
|
if (key.startsWith(`${endpointId}-`)) {
|
|
|
|
|
|
delete bodyRuleSelectOpen.value[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isHeaderRuleDragging(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return headerRuleDraggedIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isHeaderRuleDragOver(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return headerRuleDragOverIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isBodyRuleDragging(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return bodyRuleDraggedIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function isResponseRuleDragging(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return responseRuleDraggedIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function isBodyRuleDragOver(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return bodyRuleDragOverIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function isResponseRuleDragOver(endpointId: string, index: number): boolean {
|
|
|
|
|
|
return responseRuleDragOverIndex.value[endpointId] === index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function clearHeaderRuleDragState(endpointId: string) {
|
|
|
|
|
|
headerRuleDraggedIndex.value[endpointId] = null
|
|
|
|
|
|
headerRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function clearResponseRuleDragState(endpointId: string) {
|
|
|
|
|
|
responseRuleDraggedIndex.value[endpointId] = null
|
|
|
|
|
|
responseRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function clearBodyRuleDragState(endpointId: string) {
|
|
|
|
|
|
bodyRuleDraggedIndex.value[endpointId] = null
|
|
|
|
|
|
bodyRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleHeaderRuleDragStart(endpointId: string, index: number, event: DragEvent) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (!rules[index]) return
|
|
|
|
|
|
|
|
|
|
|
|
headerRuleDraggedIndex.value[endpointId] = index
|
|
|
|
|
|
headerRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', `header:${endpointId}:${index}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleHeaderRuleDragOver(endpointId: string, index: number) {
|
|
|
|
|
|
const dragged = headerRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
if (dragged === null || dragged === undefined || dragged === index) return
|
|
|
|
|
|
headerRuleDragOverIndex.value[endpointId] = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleHeaderRuleDragLeave(endpointId: string, index: number) {
|
|
|
|
|
|
if (headerRuleDragOverIndex.value[endpointId] === index) {
|
|
|
|
|
|
headerRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleHeaderRuleDrop(endpointId: string, targetIndex: number) {
|
|
|
|
|
|
const dragIndex = headerRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
clearHeaderRuleDragState(endpointId)
|
|
|
|
|
|
if (dragIndex === null || dragIndex === undefined || dragIndex === targetIndex) return
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (dragIndex < 0 || dragIndex >= rules.length || targetIndex < 0 || targetIndex >= rules.length) return
|
|
|
|
|
|
|
|
|
|
|
|
const [draggedRule] = rules.splice(dragIndex, 1)
|
|
|
|
|
|
rules.splice(targetIndex, 0, draggedRule)
|
|
|
|
|
|
clearHeaderRuleSelectOpen(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleHeaderRuleDragEnd(endpointId: string) {
|
|
|
|
|
|
clearHeaderRuleDragState(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function handleResponseRuleDragStart(endpointId: string, index: number, event: DragEvent) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (!rules[index]) return
|
|
|
|
|
|
|
|
|
|
|
|
responseRuleDraggedIndex.value[endpointId] = index
|
|
|
|
|
|
responseRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', `response:${endpointId}:${index}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleResponseRuleDragOver(endpointId: string, index: number) {
|
|
|
|
|
|
const dragged = responseRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
if (dragged === null || dragged === undefined || dragged === index) return
|
|
|
|
|
|
responseRuleDragOverIndex.value[endpointId] = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleResponseRuleDragLeave(endpointId: string, index: number) {
|
|
|
|
|
|
if (responseRuleDragOverIndex.value[endpointId] === index) {
|
|
|
|
|
|
responseRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleResponseRuleDrop(endpointId: string, targetIndex: number) {
|
|
|
|
|
|
const dragIndex = responseRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
clearResponseRuleDragState(endpointId)
|
|
|
|
|
|
if (dragIndex === null || dragIndex === undefined || dragIndex === targetIndex) return
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (dragIndex < 0 || dragIndex >= rules.length || targetIndex < 0 || targetIndex >= rules.length) return
|
|
|
|
|
|
|
|
|
|
|
|
const [draggedRule] = rules.splice(dragIndex, 1)
|
|
|
|
|
|
rules.splice(targetIndex, 0, draggedRule)
|
|
|
|
|
|
clearResponseRuleSelectOpen(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleResponseRuleDragEnd(endpointId: string) {
|
|
|
|
|
|
clearResponseRuleDragState(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
function handleBodyRuleDragStart(endpointId: string, index: number, event: DragEvent) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (!rules[index]) return
|
|
|
|
|
|
|
|
|
|
|
|
bodyRuleDraggedIndex.value[endpointId] = index
|
|
|
|
|
|
bodyRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', `body:${endpointId}:${index}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBodyRuleDragOver(endpointId: string, index: number) {
|
|
|
|
|
|
const dragged = bodyRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
if (dragged === null || dragged === undefined || dragged === index) return
|
|
|
|
|
|
bodyRuleDragOverIndex.value[endpointId] = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBodyRuleDragLeave(endpointId: string, index: number) {
|
|
|
|
|
|
if (bodyRuleDragOverIndex.value[endpointId] === index) {
|
|
|
|
|
|
bodyRuleDragOverIndex.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBodyRuleDrop(endpointId: string, targetIndex: number) {
|
|
|
|
|
|
const dragIndex = bodyRuleDraggedIndex.value[endpointId]
|
|
|
|
|
|
clearBodyRuleDragState(endpointId)
|
|
|
|
|
|
if (dragIndex === null || dragIndex === undefined || dragIndex === targetIndex) return
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (dragIndex < 0 || dragIndex >= rules.length || targetIndex < 0 || targetIndex >= rules.length) return
|
|
|
|
|
|
|
|
|
|
|
|
const [draggedRule] = rules.splice(dragIndex, 1)
|
|
|
|
|
|
rules.splice(targetIndex, 0, draggedRule)
|
|
|
|
|
|
clearBodyRuleSelectOpen(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBodyRuleDragEnd(endpointId: string) {
|
|
|
|
|
|
clearBodyRuleDragState(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 状态
|
|
|
|
|
|
const addingEndpoint = ref(false)
|
|
|
|
|
|
const savingEndpointId = ref<string | null>(null)
|
2026-03-02 22:08:39 +08:00
|
|
|
|
const resettingDefaultRulesEndpointId = ref<string | null>(null)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const deletingEndpointId = ref<string | null>(null)
|
|
|
|
|
|
const togglingEndpointId = ref<string | null>(null)
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const togglingFormatEndpointId = ref<string | null>(null)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const formatSelectOpen = ref(false)
|
2026-04-27 20:05:43 +08:00
|
|
|
|
const endpointProxyPopoverOpen = ref<Record<string, boolean>>({})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-16 18:24:52 +08:00
|
|
|
|
// 删除确认弹窗状态
|
|
|
|
|
|
const deleteConfirmOpen = ref(false)
|
|
|
|
|
|
const endpointToDelete = ref<ProviderEndpoint | null>(null)
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 请求规则折叠状态
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const endpointRulesExpanded = ref<Record<string, boolean>>({})
|
2026-05-06 21:30:13 +08:00
|
|
|
|
const endpointRulesJsonMode = ref<Record<string, boolean>>({})
|
|
|
|
|
|
const endpointRulesJsonDraft = ref<Record<string, string>>({})
|
|
|
|
|
|
const endpointRulesJsonError = ref<Record<string, string | null>>({})
|
|
|
|
|
|
const endpointRulesJsonDirty = ref<Record<string, boolean>>({})
|
2026-01-22 01:48:56 +08:00
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 请求体规则 Select 的展开状态
|
|
|
|
|
|
const bodyRuleSelectOpen = ref<Record<string, boolean>>({})
|
|
|
|
|
|
|
2026-02-11 03:16:53 +08:00
|
|
|
|
// 请求体规则说明 Popover 的展开状态
|
|
|
|
|
|
const bodyRuleHelpOpenEndpointId = ref<string | null>(null)
|
|
|
|
|
|
|
2026-03-02 18:02:21 +08:00
|
|
|
|
// 规则拖拽状态(按 endpoint 维度)
|
|
|
|
|
|
const headerRuleDraggedIndex = ref<Record<string, number | null>>({})
|
|
|
|
|
|
const headerRuleDragOverIndex = ref<Record<string, number | null>>({})
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const responseRuleDraggedIndex = ref<Record<string, number | null>>({})
|
|
|
|
|
|
const responseRuleDragOverIndex = ref<Record<string, number | null>>({})
|
2026-03-02 18:02:21 +08:00
|
|
|
|
const bodyRuleDraggedIndex = ref<Record<string, number | null>>({})
|
|
|
|
|
|
const bodyRuleDragOverIndex = ref<Record<string, number | null>>({})
|
|
|
|
|
|
|
2026-02-11 03:16:53 +08:00
|
|
|
|
function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
|
|
|
|
|
|
bodyRuleHelpOpenEndpointId.value = open ? endpointId : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 每个端点的编辑状态(内联编辑)
|
|
|
|
|
|
const endpointEditStates = ref<Record<string, EndpointEditState>>({})
|
2026-03-02 22:08:39 +08:00
|
|
|
|
const defaultBodyRulesByFormat = ref<Record<string, BodyRule[]>>({})
|
|
|
|
|
|
const defaultBodyRulesLoaded = ref<Record<string, boolean>>({})
|
|
|
|
|
|
const loadingDefaultBodyRulesByFormat = ref<Record<string, boolean>>({})
|
2026-01-16 01:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 系统保留的 header 名称(不允许用户设置)
|
|
|
|
|
|
const RESERVED_HEADERS = new Set([
|
|
|
|
|
|
'authorization',
|
|
|
|
|
|
'x-api-key',
|
|
|
|
|
|
'x-goog-api-key',
|
|
|
|
|
|
'content-type',
|
|
|
|
|
|
'content-length',
|
|
|
|
|
|
'host',
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const RESERVED_RESPONSE_HEADERS = new Set([
|
|
|
|
|
|
'content-length',
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const RESPONSE_HEADER_RULES_CONFIG_KEY = 'response_header_rules'
|
|
|
|
|
|
const RESPONSE_HEADER_RULES_CAMEL_CONFIG_KEY = 'responseHeaderRules'
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 系统保留的 body 字段名(不允许用户设置)
|
|
|
|
|
|
const RESERVED_BODY_FIELDS = new Set([
|
|
|
|
|
|
'stream',
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-06 22:12:21 +08:00
|
|
|
|
const BODY_RULE_JSON_ACTIONS = new Set(['set', 'drop', 'rename', 'append', 'insert', 'regex_replace'])
|
2026-05-06 21:30:13 +08:00
|
|
|
|
const CONDITION_JSON_OPS = new Set(['eq', 'neq', 'gt', 'lt', 'gte', 'lte', 'starts_with', 'ends_with', 'contains', 'matches', 'exists', 'not_exists', 'in', 'type_is'])
|
2026-05-07 15:56:22 +08:00
|
|
|
|
const CONDITION_JSON_SOURCES = new Set(['body', 'current', 'original', 'request_headers', 'headers'])
|
2026-05-06 21:30:13 +08:00
|
|
|
|
|
|
|
|
|
|
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readJsonRulesArray(root: Record<string, unknown>, key: keyof EndpointRulesJsonPayload, label: string): { value: unknown[]; error: string | null } {
|
|
|
|
|
|
const raw = root[key]
|
|
|
|
|
|
if (raw === undefined || raw === null) return { value: [], error: null }
|
|
|
|
|
|
if (!Array.isArray(raw)) return { value: [], error: `${label} 必须是数组或 null` }
|
|
|
|
|
|
return { value: raw, error: null }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateJsonCondition(rule: Record<string, unknown>, label: string, index: number): string | null {
|
|
|
|
|
|
const raw = rule.condition
|
|
|
|
|
|
if (raw === undefined || raw === null) return null
|
|
|
|
|
|
if (!isJsonObject(raw)) return `${label}第 ${index + 1} 条:condition 必须是对象`
|
|
|
|
|
|
const shapeError = validateJsonConditionShape(raw, `${label}第 ${index + 1} 条:condition`)
|
|
|
|
|
|
if (shapeError) return shapeError
|
|
|
|
|
|
const editable = conditionToEditable(raw as BodyRule['condition'])
|
|
|
|
|
|
const err = validateEditableCondition(editable)
|
|
|
|
|
|
return err ? `${label}第 ${index + 1} 条:${err}` : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateJsonConditionShape(condition: Record<string, unknown>, label: string): string | null {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(condition, 'all')) {
|
|
|
|
|
|
if (!Array.isArray(condition.all)) return `${label}.all 必须是数组`
|
|
|
|
|
|
for (let i = 0; i < condition.all.length; i++) {
|
|
|
|
|
|
const child = condition.all[i]
|
|
|
|
|
|
if (!isJsonObject(child)) return `${label}.all[${i}] 必须是对象`
|
|
|
|
|
|
const err = validateJsonConditionShape(child, `${label}.all[${i}]`)
|
|
|
|
|
|
if (err) return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(condition, 'any')) {
|
|
|
|
|
|
if (!Array.isArray(condition.any)) return `${label}.any 必须是数组`
|
|
|
|
|
|
for (let i = 0; i < condition.any.length; i++) {
|
|
|
|
|
|
const child = condition.any[i]
|
|
|
|
|
|
if (!isJsonObject(child)) return `${label}.any[${i}] 必须是对象`
|
|
|
|
|
|
const err = validateJsonConditionShape(child, `${label}.any[${i}]`)
|
|
|
|
|
|
if (err) return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof condition.path !== 'string') return `${label}.path 必须是字符串`
|
|
|
|
|
|
if (typeof condition.op !== 'string' || !CONDITION_JSON_OPS.has(condition.op)) {
|
|
|
|
|
|
return `${label}.op 无效`
|
|
|
|
|
|
}
|
2026-05-07 15:56:22 +08:00
|
|
|
|
if (condition.source !== undefined && (typeof condition.source !== 'string' || !CONDITION_JSON_SOURCES.has(condition.source))) {
|
|
|
|
|
|
return `${label}.source 无效`
|
2026-05-06 21:30:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function requireJsonString(rule: Record<string, unknown>, key: string, label: string, index: number): string | null {
|
|
|
|
|
|
return typeof rule[key] === 'string' ? null : `${label}第 ${index + 1} 条:${key} 必须是字符串`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateHeaderRuleJson(rule: unknown, label: string, index: number): string | null {
|
|
|
|
|
|
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
|
|
|
|
|
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
|
|
|
|
|
}
|
2026-05-06 21:30:13 +08:00
|
|
|
|
const action = rule.action
|
|
|
|
|
|
if (action !== 'set' && action !== 'drop' && action !== 'rename') {
|
|
|
|
|
|
return `${label}第 ${index + 1} 条:action 必须是 set/drop/rename`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'set') {
|
|
|
|
|
|
return requireJsonString(rule, 'key', label, index)
|
|
|
|
|
|
|| requireJsonString(rule, 'value', label, index)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'drop') {
|
|
|
|
|
|
return requireJsonString(rule, 'key', label, index)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
return requireJsonString(rule, 'from', label, index)
|
|
|
|
|
|
|| requireJsonString(rule, 'to', label, index)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateBodyRuleJson(rule: unknown, label: string, index: number): string | null {
|
|
|
|
|
|
if (!isJsonObject(rule)) return `${label}第 ${index + 1} 条必须是对象`
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (rule.enabled !== undefined && typeof rule.enabled !== 'boolean') {
|
|
|
|
|
|
return `${label}第 ${index + 1} 条:enabled 必须是布尔值`
|
|
|
|
|
|
}
|
2026-05-06 21:30:13 +08:00
|
|
|
|
const action = typeof rule.action === 'string' ? rule.action : ''
|
|
|
|
|
|
if (!BODY_RULE_JSON_ACTIONS.has(action)) {
|
|
|
|
|
|
return `${label}第 ${index + 1} 条:action 无效`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (action === 'set' || action === 'append') {
|
|
|
|
|
|
return requireJsonString(rule, 'path', label, index)
|
|
|
|
|
|
|| (Object.prototype.hasOwnProperty.call(rule, 'value') ? null : `${label}第 ${index + 1} 条:value 不能为空`)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'drop') {
|
|
|
|
|
|
return requireJsonString(rule, 'path', label, index)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'rename') {
|
|
|
|
|
|
return requireJsonString(rule, 'from', label, index)
|
|
|
|
|
|
|| requireJsonString(rule, 'to', label, index)
|
|
|
|
|
|
|| validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'insert') {
|
|
|
|
|
|
if (requireJsonString(rule, 'path', label, index)) return requireJsonString(rule, 'path', label, index)
|
|
|
|
|
|
if (!Number.isInteger(rule.index)) return `${label}第 ${index + 1} 条:index 必须是整数`
|
|
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(rule, 'value')) return `${label}第 ${index + 1} 条:value 不能为空`
|
|
|
|
|
|
return validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'regex_replace') {
|
|
|
|
|
|
if (requireJsonString(rule, 'path', label, index)) return requireJsonString(rule, 'path', label, index)
|
|
|
|
|
|
if (requireJsonString(rule, 'pattern', label, index)) return requireJsonString(rule, 'pattern', label, index)
|
|
|
|
|
|
if (typeof rule.replacement !== 'string') return `${label}第 ${index + 1} 条:replacement 必须是字符串`
|
|
|
|
|
|
if (rule.flags !== undefined && typeof rule.flags !== 'string') return `${label}第 ${index + 1} 条:flags 必须是字符串`
|
|
|
|
|
|
if (rule.count !== undefined && !Number.isInteger(rule.count)) return `${label}第 ${index + 1} 条:count 必须是整数`
|
|
|
|
|
|
return validateJsonCondition(rule, label, index)
|
|
|
|
|
|
}
|
2026-05-06 22:12:21 +08:00
|
|
|
|
return `${label}第 ${index + 1} 条:action 无效`
|
2026-05-06 21:30:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseEndpointRulesJsonDraft(draft: string): { value: EndpointRulesJsonPayload | null; error: string | null } {
|
|
|
|
|
|
const raw = draft.trim()
|
|
|
|
|
|
if (!raw) {
|
|
|
|
|
|
return { value: { header_rules: [], body_rules: [], response_header_rules: [] }, error: null }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let parsed: unknown
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsed = JSON.parse(raw)
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
return { value: null, error: error instanceof Error ? error.message : 'JSON 格式无效' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isJsonObject(parsed)) return { value: null, error: '规则 JSON 必须是对象' }
|
|
|
|
|
|
|
|
|
|
|
|
const header = readJsonRulesArray(parsed, 'header_rules', 'header_rules')
|
|
|
|
|
|
if (header.error) return { value: null, error: header.error }
|
|
|
|
|
|
const body = readJsonRulesArray(parsed, 'body_rules', 'body_rules')
|
|
|
|
|
|
if (body.error) return { value: null, error: body.error }
|
|
|
|
|
|
const response = readJsonRulesArray(parsed, 'response_header_rules', 'response_header_rules')
|
|
|
|
|
|
if (response.error) return { value: null, error: response.error }
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < header.value.length; i++) {
|
|
|
|
|
|
const error = validateHeaderRuleJson(header.value[i], 'header_rules ', i)
|
|
|
|
|
|
if (error) return { value: null, error }
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = 0; i < body.value.length; i++) {
|
|
|
|
|
|
const error = validateBodyRuleJson(body.value[i], 'body_rules ', i)
|
|
|
|
|
|
if (error) return { value: null, error }
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = 0; i < response.value.length; i++) {
|
|
|
|
|
|
const error = validateHeaderRuleJson(response.value[i], 'response_header_rules ', i)
|
|
|
|
|
|
if (error) return { value: null, error }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
value: {
|
|
|
|
|
|
header_rules: header.value as HeaderRule[],
|
|
|
|
|
|
body_rules: body.value as BodyRule[],
|
|
|
|
|
|
response_header_rules: response.value as HeaderRule[],
|
|
|
|
|
|
},
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyEndpointRulesJsonDraft(
|
|
|
|
|
|
endpointId: string,
|
|
|
|
|
|
options: { notify?: boolean; notifyError?: boolean } = {},
|
|
|
|
|
|
): boolean {
|
|
|
|
|
|
const notifyError = options.notifyError !== false
|
|
|
|
|
|
const parsed = parseEndpointRulesJsonDraft(endpointRulesJsonDraft.value[endpointId] ?? '')
|
|
|
|
|
|
if (!parsed.value) {
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = parsed.error
|
|
|
|
|
|
if (notifyError) showError(parsed.error || '规则 JSON 无效')
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const state = ensureEndpointEditState(endpointId)
|
|
|
|
|
|
if (!state) {
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = '端点编辑状态不可用'
|
|
|
|
|
|
if (notifyError) showError('端点编辑状态不可用')
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
state.rules = editableHeaderRulesFromRules(parsed.value.header_rules)
|
|
|
|
|
|
state.bodyRules = editableBodyRulesFromRules(parsed.value.body_rules)
|
|
|
|
|
|
state.responseRules = editableHeaderRulesFromRules(parsed.value.response_header_rules)
|
|
|
|
|
|
|
|
|
|
|
|
clearHeaderRuleSelectOpen(endpointId)
|
|
|
|
|
|
clearResponseRuleSelectOpen(endpointId)
|
|
|
|
|
|
clearBodyRuleSelectOpen(endpointId)
|
|
|
|
|
|
clearHeaderRuleDragState(endpointId)
|
|
|
|
|
|
clearResponseRuleDragState(endpointId)
|
|
|
|
|
|
clearBodyRuleDragState(endpointId)
|
|
|
|
|
|
|
|
|
|
|
|
const validationError = getHeaderValidationErrorForEndpoint(endpointId)
|
|
|
|
|
|
|| getResponseHeaderValidationErrorForEndpoint(endpointId)
|
|
|
|
|
|
|| getBodyValidationErrorForEndpoint(endpointId)
|
|
|
|
|
|
if (validationError) {
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = validationError
|
|
|
|
|
|
if (notifyError) showError(validationError)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
endpointRulesJsonDraft.value[endpointId] = stringifyEndpointRulesJsonPayload(parsed.value)
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = null
|
|
|
|
|
|
endpointRulesJsonDirty.value[endpointId] = false
|
|
|
|
|
|
if (options.notify !== false) success('JSON 规则已应用')
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 21:01:45 +08:00
|
|
|
|
// {{$original}} 占位符处理
|
|
|
|
|
|
const ORIGINAL_PLACEHOLDER = '{{$original}}'
|
|
|
|
|
|
const ORIGINAL_SENTINEL = '__AETHER_ORIGINAL__'
|
|
|
|
|
|
|
|
|
|
|
|
// 将 {{$original}} 替换为合法 JSON 以便 JSON.parse 校验
|
|
|
|
|
|
// 处理三种写法:裸占位符 {{$original}}、带引号 "{{$original}}"、引号内拼接 "prefix_{{$original}}_suffix"
|
|
|
|
|
|
function prepareValueForJsonParse(raw: string): string {
|
|
|
|
|
|
// Step 1: 纯文本替换占位符为 sentinel
|
|
|
|
|
|
const result = raw.replaceAll(ORIGINAL_PLACEHOLDER, ORIGINAL_SENTINEL)
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: 尝试直接 parse(占位符在引号内时已经是合法 JSON)
|
|
|
|
|
|
try { JSON.parse(result); return result } catch { /* sentinel not in valid JSON position */ }
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: 有裸 sentinel 不在引号内,需要扫描并补引号
|
|
|
|
|
|
let out = ''
|
|
|
|
|
|
let inStr = false
|
|
|
|
|
|
let i = 0
|
|
|
|
|
|
while (i < result.length) {
|
|
|
|
|
|
if (result[i] === '\\' && inStr) {
|
|
|
|
|
|
out += result[i] + (result[i + 1] || '')
|
|
|
|
|
|
i += 2
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result[i] === '"') {
|
|
|
|
|
|
inStr = !inStr
|
|
|
|
|
|
out += result[i]
|
|
|
|
|
|
i++
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!inStr && result.startsWith(ORIGINAL_SENTINEL, i)) {
|
2026-02-11 00:50:08 +08:00
|
|
|
|
out += `"${ ORIGINAL_SENTINEL }"`
|
2026-02-10 21:01:45 +08:00
|
|
|
|
i += ORIGINAL_SENTINEL.length
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
out += result[i]
|
|
|
|
|
|
i++
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递归还原: 将 sentinel 字符串还原为 {{$original}}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
function restoreOriginalPlaceholder(value: unknown): unknown {
|
2026-02-10 21:01:45 +08:00
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
|
if (value === ORIGINAL_SENTINEL) return ORIGINAL_PLACEHOLDER
|
|
|
|
|
|
if (value.includes(ORIGINAL_SENTINEL)) {
|
|
|
|
|
|
return value.replaceAll(ORIGINAL_SENTINEL, ORIGINAL_PLACEHOLDER)
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
2026-03-20 01:06:15 +08:00
|
|
|
|
if (Array.isArray(value)) return value.map(item => restoreOriginalPlaceholder(item))
|
2026-02-10 21:01:45 +08:00
|
|
|
|
if (value !== null && typeof value === 'object') {
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const result: Record<string, unknown> = {}
|
|
|
|
|
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) result[k] = restoreOriginalPlaceholder(v)
|
2026-02-10 21:01:45 +08:00
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
function parseBodyRulePathParts(path: string): string[] | null {
|
|
|
|
|
|
const raw = path.trim()
|
|
|
|
|
|
if (!raw) return null
|
|
|
|
|
|
|
|
|
|
|
|
const parts: string[] = []
|
|
|
|
|
|
let current = ''
|
|
|
|
|
|
for (let i = 0; i < raw.length; i++) {
|
|
|
|
|
|
const ch = raw[i]
|
|
|
|
|
|
// 支持 \. 转义字面量点号;其他反斜杠组合按字面量保留
|
|
|
|
|
|
if (ch === '\\' && i + 1 < raw.length && raw[i + 1] === '.') {
|
|
|
|
|
|
current += '.'
|
|
|
|
|
|
i++
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ch === '.') {
|
|
|
|
|
|
if (!current) return null // 禁止空段:.a / a. / a..b
|
|
|
|
|
|
parts.push(current)
|
|
|
|
|
|
current = ''
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
current += ch
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!current) return null
|
|
|
|
|
|
parts.push(current)
|
|
|
|
|
|
return parts
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
function initBodyRuleSetValueForEditor(value: unknown): { value: string } {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
if (value === undefined) return { value: '' }
|
|
|
|
|
|
|
|
|
|
|
|
// 所有值都用 JSON 格式回显
|
|
|
|
|
|
try {
|
|
|
|
|
|
return { value: JSON.stringify(value) }
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return { value: String(value) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 内部状态
|
|
|
|
|
|
const internalOpen = computed(() => props.modelValue)
|
|
|
|
|
|
|
2026-02-04 10:24:25 +08:00
|
|
|
|
const isFixedProvider = computed(() => {
|
|
|
|
|
|
const t = props.provider?.provider_type
|
|
|
|
|
|
return !!t && t !== 'custom'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 新端点表单
|
|
|
|
|
|
const newEndpoint = ref({
|
2025-12-10 20:52:44 +08:00
|
|
|
|
api_format: '',
|
|
|
|
|
|
base_url: '',
|
|
|
|
|
|
custom_path: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// API 格式列表
|
2026-01-10 18:43:53 +08:00
|
|
|
|
const apiFormats = ref<Array<{ value: string; label: string; default_path: string }>>([])
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 本地端点列表
|
|
|
|
|
|
const localEndpoints = ref<ProviderEndpoint[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
// 可用的格式(未添加的)
|
|
|
|
|
|
const availableFormats = computed(() => {
|
|
|
|
|
|
const existingFormats = localEndpoints.value.map(e => e.api_format)
|
|
|
|
|
|
return apiFormats.value.filter(f => !existingFormats.includes(f.value))
|
2026-01-08 10:50:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-16 18:24:52 +08:00
|
|
|
|
// 删除确认弹窗描述
|
|
|
|
|
|
const deleteConfirmDescription = computed(() => {
|
|
|
|
|
|
if (!endpointToDelete.value) return ''
|
2026-02-19 14:51:08 +08:00
|
|
|
|
const formatLabel = formatApiFormat(endpointToDelete.value.api_format)
|
2026-01-16 18:24:52 +08:00
|
|
|
|
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-07 02:50:19 +08:00
|
|
|
|
function defaultBodyRulesCacheKey(apiFormat: string): string {
|
|
|
|
|
|
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
|
|
|
|
|
return providerType ? `${apiFormat}:${providerType}` : apiFormat
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasDefaultBodyRules(apiFormat: string): boolean {
|
|
|
|
|
|
const cacheKey = defaultBodyRulesCacheKey(apiFormat)
|
|
|
|
|
|
if (!defaultBodyRulesLoaded.value[cacheKey]) return false
|
|
|
|
|
|
return (defaultBodyRulesByFormat.value[cacheKey]?.length || 0) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 22:08:39 +08:00
|
|
|
|
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
|
|
|
|
|
|
if (!apiFormat) return []
|
2026-03-05 19:04:21 +08:00
|
|
|
|
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
2026-04-07 02:50:19 +08:00
|
|
|
|
const cacheKey = defaultBodyRulesCacheKey(apiFormat)
|
2026-03-05 19:04:21 +08:00
|
|
|
|
if (!force && defaultBodyRulesLoaded.value[cacheKey]) {
|
|
|
|
|
|
return defaultBodyRulesByFormat.value[cacheKey] || []
|
2026-03-02 22:08:39 +08:00
|
|
|
|
}
|
2026-03-05 19:04:21 +08:00
|
|
|
|
if (loadingDefaultBodyRulesByFormat.value[cacheKey]) {
|
|
|
|
|
|
return defaultBodyRulesByFormat.value[cacheKey] || []
|
2026-03-02 22:08:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 19:04:21 +08:00
|
|
|
|
loadingDefaultBodyRulesByFormat.value[cacheKey] = true
|
2026-03-02 22:08:39 +08:00
|
|
|
|
try {
|
2026-03-05 19:04:21 +08:00
|
|
|
|
const response = await getDefaultBodyRules(apiFormat, providerType || undefined)
|
2026-03-02 22:08:39 +08:00
|
|
|
|
const rules = response.body_rules || []
|
2026-03-05 19:04:21 +08:00
|
|
|
|
defaultBodyRulesByFormat.value[cacheKey] = rules
|
|
|
|
|
|
defaultBodyRulesLoaded.value[cacheKey] = true
|
2026-03-02 22:08:39 +08:00
|
|
|
|
return rules
|
|
|
|
|
|
} catch (error: unknown) {
|
2026-03-05 19:04:21 +08:00
|
|
|
|
defaultBodyRulesByFormat.value[cacheKey] = []
|
|
|
|
|
|
defaultBodyRulesLoaded.value[cacheKey] = true
|
2026-03-20 01:06:15 +08:00
|
|
|
|
log.warn('加载默认请求体规则失败', { apiFormat, error })
|
2026-03-02 22:08:39 +08:00
|
|
|
|
return []
|
|
|
|
|
|
} finally {
|
2026-03-05 19:04:21 +08:00
|
|
|
|
loadingDefaultBodyRulesByFormat.value[cacheKey] = false
|
2026-03-02 22:08:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function preloadDefaultBodyRules(endpoints: ProviderEndpoint[]): Promise<void> {
|
|
|
|
|
|
const formats = Array.from(new Set(endpoints.map(e => e.api_format).filter(Boolean)))
|
|
|
|
|
|
await Promise.all(formats.map(fmt => loadDefaultBodyRulesForFormat(fmt)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 获取指定 API 格式的默认路径
|
2026-02-05 09:48:37 +08:00
|
|
|
|
function getDefaultPath(apiFormat: string, baseUrl?: string): string {
|
2026-03-19 23:52:17 +08:00
|
|
|
|
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
2026-05-18 17:35:36 +00:00
|
|
|
|
return getDefaultEndpointPath({
|
|
|
|
|
|
apiFormat,
|
|
|
|
|
|
providerType,
|
|
|
|
|
|
baseUrl,
|
|
|
|
|
|
apiFormats: apiFormats.value,
|
|
|
|
|
|
})
|
2026-02-05 09:48:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 23:52:17 +08:00
|
|
|
|
function getDisplayedPath(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
if (isFixedProvider.value) {
|
|
|
|
|
|
return getDefaultPath(endpoint.api_format, endpoint.base_url)
|
|
|
|
|
|
}
|
|
|
|
|
|
return getEndpointEditState(endpoint.id)?.path ?? (endpoint.custom_path || '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:57:52 +08:00
|
|
|
|
// 读取端点的上游流式策略(endpoint.config.upstream_stream_policy)
|
|
|
|
|
|
function getEndpointUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
const cfg = endpoint.config || {}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const raw = (cfg.upstream_stream_policy ?? cfg.upstreamStreamPolicy ?? cfg.upstream_stream) as unknown
|
2026-02-05 15:57:52 +08:00
|
|
|
|
if (raw === null || raw === undefined) return 'auto'
|
|
|
|
|
|
if (typeof raw === 'boolean') return raw ? 'force_stream' : 'force_non_stream'
|
|
|
|
|
|
const s = String(raw).trim().toLowerCase()
|
|
|
|
|
|
if (!s || s === 'auto' || s === 'follow' || s === 'client' || s === 'default') return 'auto'
|
|
|
|
|
|
if (s === 'force_stream' || s === 'stream' || s === 'sse' || s === 'true' || s === '1') return 'force_stream'
|
|
|
|
|
|
if (s === 'force_non_stream' || s === 'force_sync' || s === 'non_stream' || s === 'sync' || s === 'false' || s === '0') return 'force_non_stream'
|
|
|
|
|
|
return 'auto'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 20:05:43 +08:00
|
|
|
|
function endpointProxyNodeId(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
if (endpoint.proxy?.enabled === false) return ''
|
|
|
|
|
|
return endpoint.proxy?.node_id?.trim() || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEndpointProxyNodeName(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
const nodeId = endpointProxyNodeId(endpoint)
|
|
|
|
|
|
if (!nodeId) return '未知节点'
|
|
|
|
|
|
const node = proxyNodesStore.nodes.find(n => n.id === nodeId)
|
|
|
|
|
|
return node ? node.name : `${nodeId.slice(0, 8)}...`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEndpointProxyTitle(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
const nodeId = endpointProxyNodeId(endpoint)
|
|
|
|
|
|
return nodeId ? `端点代理: ${getEndpointProxyNodeName(endpoint)}` : '设置端点代理节点'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleEndpointProxyPopoverToggle(endpointId: string, open: boolean) {
|
|
|
|
|
|
endpointProxyPopoverOpen.value[endpointId] = open
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
proxyNodesStore.ensureLoaded()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function replaceLocalEndpoint(updated: ProviderEndpoint) {
|
|
|
|
|
|
localEndpoints.value = localEndpoints.value.map(endpoint =>
|
|
|
|
|
|
endpoint.id === updated.id ? updated : endpoint,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function setEndpointProxy(endpoint: ProviderEndpoint, nodeId: string) {
|
|
|
|
|
|
const normalizedNodeId = nodeId.trim()
|
|
|
|
|
|
if (!normalizedNodeId) return
|
|
|
|
|
|
|
|
|
|
|
|
savingEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await updateEndpoint(endpoint.id, {
|
|
|
|
|
|
proxy: { node_id: normalizedNodeId, enabled: true },
|
|
|
|
|
|
})
|
|
|
|
|
|
replaceLocalEndpoint(updated)
|
|
|
|
|
|
endpointProxyPopoverOpen.value[endpoint.id] = false
|
|
|
|
|
|
success('端点代理已更新')
|
|
|
|
|
|
emit('endpointUpdated')
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '更新代理失败'), '错误')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
savingEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function clearEndpointProxy(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
savingEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await updateEndpoint(endpoint.id, { proxy: null })
|
|
|
|
|
|
replaceLocalEndpoint(updated)
|
|
|
|
|
|
endpointProxyPopoverOpen.value[endpoint.id] = false
|
|
|
|
|
|
success('端点代理已清除')
|
|
|
|
|
|
emit('endpointUpdated')
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '清除代理失败'), '错误')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
savingEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function emptyHeaderRule(): EditableRule {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
return { action: 'set', enabled: true, key: '', value: '', from: '', to: '', condition: null }
|
2026-03-15 20:27:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function editableHeaderRulesFromRules(rules: HeaderRule[] | null | undefined): EditableRule[] {
|
|
|
|
|
|
if (!Array.isArray(rules)) return []
|
|
|
|
|
|
const editableRules: EditableRule[] = []
|
|
|
|
|
|
for (const rule of rules) {
|
|
|
|
|
|
if (rule.action === 'set') {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
editableRules.push({ ...emptyHeaderRule(), action: 'set', enabled: rule.enabled !== false, key: rule.key, value: rule.value || '', condition: conditionToEditable(rule.condition) })
|
2026-05-06 01:29:42 +08:00
|
|
|
|
} else if (rule.action === 'drop') {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
editableRules.push({ ...emptyHeaderRule(), action: 'drop', enabled: rule.enabled !== false, key: rule.key, condition: conditionToEditable(rule.condition) })
|
2026-05-06 01:29:42 +08:00
|
|
|
|
} else if (rule.action === 'rename') {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
editableRules.push({ ...emptyHeaderRule(), action: 'rename', enabled: rule.enabled !== false, from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
2026-05-06 01:29:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return editableRules
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function emptyBodyRule(action: BodyRuleAction = 'set'): EditableBodyRule {
|
|
|
|
|
|
return {
|
|
|
|
|
|
action,
|
2026-05-07 00:48:15 +08:00
|
|
|
|
enabled: true,
|
2026-03-15 20:27:53 +08:00
|
|
|
|
path: '',
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
from: '',
|
|
|
|
|
|
to: '',
|
|
|
|
|
|
index: '',
|
|
|
|
|
|
pattern: '',
|
|
|
|
|
|
replacement: '',
|
|
|
|
|
|
flags: '',
|
2026-04-22 20:16:57 +08:00
|
|
|
|
count: '',
|
2026-03-15 20:27:53 +08:00
|
|
|
|
condition: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:13 +08:00
|
|
|
|
function editableBodyRulesFromRules(rules: BodyRule[] | null | undefined): EditableBodyRule[] {
|
|
|
|
|
|
const bodyRules: EditableBodyRule[] = []
|
|
|
|
|
|
if (!Array.isArray(rules)) return bodyRules
|
|
|
|
|
|
|
|
|
|
|
|
for (const rule of rules) {
|
|
|
|
|
|
if (rule.action === 'set') {
|
|
|
|
|
|
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
bodyRules.push({ ...emptyBodyRule('set'), enabled: rule.enabled !== false, path: rule.path, value, condition: conditionToEditable(rule.condition) })
|
2026-05-06 21:30:13 +08:00
|
|
|
|
} else if (rule.action === 'drop') {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
bodyRules.push({ ...emptyBodyRule('drop'), enabled: rule.enabled !== false, path: rule.path, condition: conditionToEditable(rule.condition) })
|
2026-05-06 21:30:13 +08:00
|
|
|
|
} else if (rule.action === 'rename') {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
bodyRules.push({ ...emptyBodyRule('rename'), enabled: rule.enabled !== false, from: rule.from, to: rule.to, condition: conditionToEditable(rule.condition) })
|
2026-05-06 21:30:13 +08:00
|
|
|
|
} else if (rule.action === 'append') {
|
|
|
|
|
|
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
bodyRules.push({ ...emptyBodyRule('append'), enabled: rule.enabled !== false, path: rule.path || '', value, condition: conditionToEditable(rule.condition) })
|
2026-05-06 21:30:13 +08:00
|
|
|
|
} else if (rule.action === 'insert') {
|
|
|
|
|
|
const { value } = initBodyRuleSetValueForEditor(rule.value)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
bodyRules.push({ ...emptyBodyRule('insert'), enabled: rule.enabled !== false, path: rule.path || '', value, index: String(rule.index ?? ''), condition: conditionToEditable(rule.condition) })
|
2026-05-06 21:30:13 +08:00
|
|
|
|
} else if (rule.action === 'regex_replace') {
|
|
|
|
|
|
bodyRules.push({
|
|
|
|
|
|
...emptyBodyRule('regex_replace'),
|
2026-05-07 00:48:15 +08:00
|
|
|
|
enabled: rule.enabled !== false,
|
2026-05-06 21:30:13 +08:00
|
|
|
|
path: rule.path || '',
|
|
|
|
|
|
pattern: rule.pattern || '',
|
|
|
|
|
|
replacement: rule.replacement || '',
|
|
|
|
|
|
flags: rule.flags || '',
|
|
|
|
|
|
count: rule.count === undefined || rule.count === null ? '' : String(rule.count),
|
|
|
|
|
|
condition: conditionToEditable(rule.condition),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return bodyRules
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 初始化端点的编辑状态
|
|
|
|
|
|
function initEndpointEditState(endpoint: ProviderEndpoint): EndpointEditState {
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const rules = editableHeaderRulesFromRules(endpoint.header_rules)
|
|
|
|
|
|
const responseRules = editableHeaderRulesFromRules(getEndpointResponseHeaderRules(endpoint))
|
2026-05-06 21:30:13 +08:00
|
|
|
|
const bodyRules = editableBodyRulesFromRules(endpoint.body_rules)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
return {
|
|
|
|
|
|
url: endpoint.base_url,
|
|
|
|
|
|
path: endpoint.custom_path || '',
|
2026-02-05 15:57:52 +08:00
|
|
|
|
upstreamStreamPolicy: getEndpointUpstreamStreamPolicy(endpoint),
|
2026-01-22 01:48:56 +08:00
|
|
|
|
rules,
|
2026-05-06 01:29:42 +08:00
|
|
|
|
responseRules,
|
2026-02-03 18:49:17 +08:00
|
|
|
|
bodyRules,
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取端点的编辑状态
|
|
|
|
|
|
function getEndpointEditState(endpointId: string): EndpointEditState | undefined {
|
|
|
|
|
|
return endpointEditStates.value[endpointId]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:13 +08:00
|
|
|
|
function ensureEndpointEditState(endpointId: string): EndpointEditState | null {
|
2026-01-22 01:48:56 +08:00
|
|
|
|
if (!endpointEditStates.value[endpointId]) {
|
|
|
|
|
|
const endpoint = localEndpoints.value.find(e => e.id === endpointId)
|
|
|
|
|
|
if (endpoint) {
|
|
|
|
|
|
endpointEditStates.value[endpointId] = initEndpointEditState(endpoint)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 21:30:13 +08:00
|
|
|
|
return endpointEditStates.value[endpointId] ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isEndpointRulesJsonMode(endpointId: string): boolean {
|
|
|
|
|
|
return endpointRulesJsonMode.value[endpointId] === true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildEndpointRulesJsonPayload(endpointId: string): EndpointRulesJsonPayload {
|
|
|
|
|
|
const state = ensureEndpointEditState(endpointId)
|
|
|
|
|
|
return {
|
|
|
|
|
|
header_rules: state ? (rulesToHeaderRules(state.rules) ?? []) : [],
|
|
|
|
|
|
body_rules: state ? (rulesToBodyRules(state.bodyRules) ?? []) : [],
|
|
|
|
|
|
response_header_rules: state ? (rulesToHeaderRules(state.responseRules) ?? []) : [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stringifyEndpointRulesJsonPayload(payload: EndpointRulesJsonPayload): string {
|
|
|
|
|
|
return JSON.stringify(payload, null, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function refreshEndpointRulesJsonDraft(endpointId: string) {
|
|
|
|
|
|
endpointRulesJsonDraft.value[endpointId] = stringifyEndpointRulesJsonPayload(
|
|
|
|
|
|
buildEndpointRulesJsonPayload(endpointId),
|
|
|
|
|
|
)
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = null
|
|
|
|
|
|
endpointRulesJsonDirty.value[endpointId] = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function enterEndpointRulesJsonMode(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
ensureEndpointEditState(endpoint.id)
|
|
|
|
|
|
refreshEndpointRulesJsonDraft(endpoint.id)
|
|
|
|
|
|
endpointRulesJsonMode.value[endpoint.id] = true
|
|
|
|
|
|
endpointRulesExpanded.value[endpoint.id] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleEndpointRulesJsonMode(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
if (isEndpointRulesJsonMode(endpoint.id)) {
|
|
|
|
|
|
if (endpointRulesJsonDirty.value[endpoint.id] && !applyEndpointRulesJsonDraft(endpoint.id, { notify: false })) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
endpointRulesJsonMode.value[endpoint.id] = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
enterEndpointRulesJsonMode(endpoint)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEndpointRulesJsonDraft(endpoint: ProviderEndpoint): string {
|
|
|
|
|
|
if (endpointRulesJsonDraft.value[endpoint.id] === undefined) {
|
|
|
|
|
|
endpointRulesJsonDraft.value[endpoint.id] = stringifyEndpointRulesJsonPayload(
|
|
|
|
|
|
buildEndpointRulesJsonPayload(endpoint.id),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
return endpointRulesJsonDraft.value[endpoint.id]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateEndpointRulesJsonDraft(endpointId: string, value: string) {
|
|
|
|
|
|
endpointRulesJsonDraft.value[endpointId] = value
|
|
|
|
|
|
endpointRulesJsonDirty.value[endpointId] = true
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatEndpointRulesJson(endpointId: string) {
|
|
|
|
|
|
const currentDraft = endpointRulesJsonDraft.value[endpointId] ?? ''
|
|
|
|
|
|
const parsed = parseEndpointRulesJsonDraft(endpointRulesJsonDraft.value[endpointId] ?? '')
|
|
|
|
|
|
if (!parsed.value) {
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = parsed.error
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const formattedDraft = stringifyEndpointRulesJsonPayload(parsed.value)
|
|
|
|
|
|
endpointRulesJsonDraft.value[endpointId] = formattedDraft
|
|
|
|
|
|
endpointRulesJsonError.value[endpointId] = null
|
|
|
|
|
|
if (formattedDraft !== currentDraft) {
|
|
|
|
|
|
endpointRulesJsonDirty.value[endpointId] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新端点字段
|
|
|
|
|
|
function updateEndpointField(endpointId: string, field: 'url' | 'path', value: string) {
|
|
|
|
|
|
ensureEndpointEditState(endpointId)
|
2026-01-22 01:48:56 +08:00
|
|
|
|
if (endpointEditStates.value[endpointId]) {
|
|
|
|
|
|
endpointEditStates.value[endpointId][field] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取端点的编辑规则
|
|
|
|
|
|
function getEndpointEditRules(endpointId: string): EditableRule[] {
|
|
|
|
|
|
const state = endpointEditStates.value[endpointId]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.rules
|
|
|
|
|
|
}
|
|
|
|
|
|
// 从原始端点加载
|
|
|
|
|
|
const endpoint = localEndpoints.value.find(e => e.id === endpointId)
|
|
|
|
|
|
if (endpoint) {
|
|
|
|
|
|
const newState = initEndpointEditState(endpoint)
|
|
|
|
|
|
endpointEditStates.value[endpointId] = newState
|
|
|
|
|
|
return newState.rules
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function getEndpointResponseHeaderRules(endpoint: ProviderEndpoint): HeaderRule[] {
|
|
|
|
|
|
const raw = (endpoint as ProviderEndpoint & { response_header_rules?: unknown }).response_header_rules
|
|
|
|
|
|
?? endpoint.config?.[RESPONSE_HEADER_RULES_CONFIG_KEY]
|
|
|
|
|
|
?? endpoint.config?.[RESPONSE_HEADER_RULES_CAMEL_CONFIG_KEY]
|
|
|
|
|
|
return Array.isArray(raw) ? (raw as HeaderRule[]) : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEndpointEditResponseRules(endpointId: string): EditableRule[] {
|
|
|
|
|
|
const state = endpointEditStates.value[endpointId]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.responseRules
|
|
|
|
|
|
}
|
|
|
|
|
|
const endpoint = localEndpoints.value.find(e => e.id === endpointId)
|
|
|
|
|
|
if (endpoint) {
|
|
|
|
|
|
const newState = initEndpointEditState(endpoint)
|
|
|
|
|
|
endpointEditStates.value[endpointId] = newState
|
|
|
|
|
|
return newState.responseRules
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 添加规则(同时自动展开折叠)
|
|
|
|
|
|
function handleAddEndpointRule(endpointId: string) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
2026-03-15 20:27:53 +08:00
|
|
|
|
rules.push(emptyHeaderRule())
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 自动展开折叠
|
|
|
|
|
|
endpointRulesExpanded.value[endpointId] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function handleAddEndpointResponseRule(endpointId: string) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
rules.push(emptyHeaderRule())
|
|
|
|
|
|
endpointRulesExpanded.value[endpointId] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 删除规则
|
|
|
|
|
|
function removeEndpointRule(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
rules.splice(index, 1)
|
2026-03-02 18:02:21 +08:00
|
|
|
|
clearHeaderRuleDragState(endpointId)
|
|
|
|
|
|
clearHeaderRuleSelectOpen(endpointId)
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function removeEndpointResponseRule(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
rules.splice(index, 1)
|
|
|
|
|
|
clearResponseRuleDragState(endpointId)
|
|
|
|
|
|
clearResponseRuleSelectOpen(endpointId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 更新规则类型
|
|
|
|
|
|
function updateEndpointRuleAction(endpointId: string, index: number, action: 'set' | 'drop' | 'rename') {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const currentCondition = rules[index].condition
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentEnabled = rules[index].enabled
|
|
|
|
|
|
rules[index] = { ...emptyHeaderRule(), action, enabled: currentEnabled, condition: currentCondition }
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function updateEndpointResponseRuleAction(endpointId: string, index: number, action: 'set' | 'drop' | 'rename') {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
const currentCondition = rules[index].condition
|
2026-05-07 00:48:15 +08:00
|
|
|
|
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
|
2026-05-06 01:29:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 更新规则字段
|
|
|
|
|
|
function updateEndpointRuleField(endpointId: string, index: number, field: 'key' | 'value' | 'from' | 'to', value: string) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index][field] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function updateEndpointResponseRuleField(endpointId: string, index: number, field: 'key' | 'value' | 'from' | 'to', value: string) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index][field] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function toggleEndpointRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = rules[index].condition ? null : createEmptyConditionLeaf()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function toggleEndpointResponseRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = rules[index].condition ? null : createEmptyConditionLeaf()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function updateEndpointRuleCondition(endpointId: string, index: number, condition: EditableConditionNode) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = condition
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function updateEndpointResponseRuleCondition(endpointId: string, index: number, condition: EditableConditionNode) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = condition
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function clearEndpointRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function clearEndpointResponseRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 验证规则 key(针对特定端点)
|
|
|
|
|
|
function validateRuleKeyForEndpoint(endpointId: string, key: string, index: number): string | null {
|
|
|
|
|
|
const trimmedKey = key.trim().toLowerCase()
|
|
|
|
|
|
if (!trimmedKey) return null
|
|
|
|
|
|
|
|
|
|
|
|
if (RESERVED_HEADERS.has(trimmedKey)) {
|
|
|
|
|
|
return `"${key}" 是系统保留的请求头`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentRule = rules[index]
|
|
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled && (
|
2026-01-22 01:48:56 +08:00
|
|
|
|
((r.action === 'set' || r.action === 'drop') && r.key.trim().toLowerCase() === trimmedKey) ||
|
|
|
|
|
|
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedKey)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
|
|
|
|
|
return '请求头名称重复'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 rename from
|
|
|
|
|
|
function validateRenameFromForEndpoint(endpointId: string, from: string, index: number): string | null {
|
|
|
|
|
|
const trimmedFrom = from.trim().toLowerCase()
|
|
|
|
|
|
if (!trimmedFrom) return null
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentRule = rules[index]
|
|
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled &&
|
2026-01-22 01:48:56 +08:00
|
|
|
|
((r.action === 'set' && r.key.trim().toLowerCase() === trimmedFrom) ||
|
|
|
|
|
|
(r.action === 'drop' && r.key.trim().toLowerCase() === trimmedFrom) ||
|
|
|
|
|
|
(r.action === 'rename' && r.from.trim().toLowerCase() === trimmedFrom))
|
|
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
|
|
|
|
|
return '该请求头已被其他规则处理'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 rename to
|
|
|
|
|
|
function validateRenameToForEndpoint(endpointId: string, to: string, index: number): string | null {
|
|
|
|
|
|
const trimmedTo = to.trim().toLowerCase()
|
|
|
|
|
|
if (!trimmedTo) return null
|
|
|
|
|
|
|
|
|
|
|
|
if (RESERVED_HEADERS.has(trimmedTo)) {
|
|
|
|
|
|
return `"${to}" 是系统保留的请求头`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentRule = rules[index]
|
|
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled &&
|
2026-01-22 01:48:56 +08:00
|
|
|
|
((r.action === 'set' && r.key.trim().toLowerCase() === trimmedTo) ||
|
|
|
|
|
|
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedTo))
|
|
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
|
|
|
|
|
return '请求头名称重复'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取端点的请求头规则数量(有效的规则)
|
|
|
|
|
|
function getEndpointRulesCount(endpoint: ProviderEndpoint): number {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.rules.filter(r => {
|
|
|
|
|
|
if (r.action === 'set' || r.action === 'drop') return r.key.trim()
|
|
|
|
|
|
if (r.action === 'rename') return r.from.trim() && r.to.trim()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}).length
|
|
|
|
|
|
}
|
|
|
|
|
|
return endpoint.header_rules?.length || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function getEndpointResponseRulesCount(endpoint: ProviderEndpoint): number {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.responseRules.filter(r => {
|
|
|
|
|
|
if (r.action === 'set' || r.action === 'drop') return r.key.trim()
|
|
|
|
|
|
if (r.action === 'rename') return r.from.trim() && r.to.trim()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}).length
|
|
|
|
|
|
}
|
|
|
|
|
|
return getEndpointResponseHeaderRules(endpoint).length
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 检查端点是否有任何规则(包括正在编辑的空规则)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function _hasAnyRules(endpoint: ProviderEndpoint): boolean {
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.rules.length > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return (endpoint.header_rules?.length || 0) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// ========== 请求体规则相关函数 ==========
|
|
|
|
|
|
|
|
|
|
|
|
// 获取端点的编辑请求体规则
|
|
|
|
|
|
function getEndpointEditBodyRules(endpointId: string): EditableBodyRule[] {
|
|
|
|
|
|
const state = endpointEditStates.value[endpointId]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.bodyRules
|
|
|
|
|
|
}
|
|
|
|
|
|
// 从原始端点加载
|
|
|
|
|
|
const endpoint = localEndpoints.value.find(e => e.id === endpointId)
|
|
|
|
|
|
if (endpoint) {
|
|
|
|
|
|
const newState = initEndpointEditState(endpoint)
|
|
|
|
|
|
endpointEditStates.value[endpointId] = newState
|
|
|
|
|
|
return newState.bodyRules
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加请求体规则(同时自动展开折叠)
|
|
|
|
|
|
function handleAddEndpointBodyRule(endpointId: string) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
2026-03-15 20:27:53 +08:00
|
|
|
|
rules.push(emptyBodyRule('set'))
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 自动展开折叠
|
|
|
|
|
|
endpointRulesExpanded.value[endpointId] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除请求体规则
|
|
|
|
|
|
function removeEndpointBodyRule(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
rules.splice(index, 1)
|
2026-03-02 18:02:21 +08:00
|
|
|
|
clearBodyRuleDragState(endpointId)
|
|
|
|
|
|
clearBodyRuleSelectOpen(endpointId)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新请求体规则类型
|
2026-02-09 02:56:21 +08:00
|
|
|
|
function updateEndpointBodyRuleAction(endpointId: string, index: number, action: BodyRuleAction) {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const currentCondition = rules[index].condition
|
2026-05-07 00:48:15 +08:00
|
|
|
|
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
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新请求体规则字段
|
2026-05-06 22:12:21 +08:00
|
|
|
|
function updateEndpointBodyRuleField(endpointId: string, index: number, field: 'path' | 'value' | 'from' | 'to' | 'index' | 'pattern' | 'replacement' | 'flags' | 'count', value: string) {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index][field] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 00:50:08 +08:00
|
|
|
|
function toggleBodyRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
rules[index].condition = rules[index].condition ? null : createEmptyConditionLeaf()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateEndpointBodyRuleCondition(endpointId: string, index: number, condition: EditableConditionNode) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = condition
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearEndpointBodyRuleCondition(endpointId: string, index: number) {
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
if (rules[index]) {
|
|
|
|
|
|
rules[index].condition = null
|
2026-02-11 00:50:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 验证请求体规则 path(针对特定端点)
|
|
|
|
|
|
function validateBodyRulePathForEndpoint(endpointId: string, path: string, index: number): string | null {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const raw = path.trim()
|
|
|
|
|
|
if (!raw) return null
|
2026-02-03 18:49:17 +08:00
|
|
|
|
|
2026-02-09 02:56:21 +08:00
|
|
|
|
// 基础格式校验;对含 [N] 的路径,取方括号前的部分做 dot 校验
|
|
|
|
|
|
const dotPart = raw.includes('[') ? raw.slice(0, raw.indexOf('[')) : raw
|
|
|
|
|
|
const parts = dotPart ? parseBodyRulePathParts(dotPart) : [raw.split('[')[0] || raw]
|
2026-02-05 20:13:41 +08:00
|
|
|
|
if (!parts) {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
return '路径格式无效'
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 02:56:21 +08:00
|
|
|
|
// 提取顶层 key(去除数组索引部分)
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const topKey = (parts[0] || '').trim().toLowerCase()
|
|
|
|
|
|
if (RESERVED_BODY_FIELDS.has(topKey)) {
|
|
|
|
|
|
return `"${parts[0]}" 是系统保留的顶层字段`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedPath = raw.toLowerCase()
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
2026-02-21 17:08:31 +08:00
|
|
|
|
const currentRule = rules[index]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-02-21 17:08:31 +08:00
|
|
|
|
// 任意一方启用了条件,则不视为冲突(条件可能互斥,真正冲突在运行时处理)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition && (
|
2026-02-05 20:13:41 +08:00
|
|
|
|
((r.action === 'set' || r.action === 'drop') && r.path.trim().toLowerCase() === normalizedPath) ||
|
|
|
|
|
|
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedPath)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return '字段路径重复'
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证请求体 rename from
|
|
|
|
|
|
function validateBodyRenameFromForEndpoint(endpointId: string, from: string, index: number): string | null {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const raw = from.trim()
|
|
|
|
|
|
if (!raw) return null
|
|
|
|
|
|
|
|
|
|
|
|
const parts = parseBodyRulePathParts(raw)
|
|
|
|
|
|
if (!parts) {
|
|
|
|
|
|
return '路径格式无效(不允许 .a / a. / a..b)'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const topKey = (parts[0] || '').trim().toLowerCase()
|
|
|
|
|
|
if (RESERVED_BODY_FIELDS.has(topKey)) {
|
|
|
|
|
|
return `"${parts[0]}" 是系统保留的顶层字段`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedFrom = raw.toLowerCase()
|
2026-02-03 18:49:17 +08:00
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
2026-02-21 17:08:31 +08:00
|
|
|
|
const currentRule = rules[index]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition &&
|
2026-02-05 20:13:41 +08:00
|
|
|
|
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedFrom) ||
|
|
|
|
|
|
(r.action === 'drop' && r.path.trim().toLowerCase() === normalizedFrom) ||
|
|
|
|
|
|
(r.action === 'rename' && r.from.trim().toLowerCase() === normalizedFrom))
|
2026-02-03 18:49:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return '该路径已被其他规则处理'
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证请求体 rename to
|
|
|
|
|
|
function validateBodyRenameToForEndpoint(endpointId: string, to: string, index: number): string | null {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const raw = to.trim()
|
|
|
|
|
|
if (!raw) return null
|
2026-02-03 18:49:17 +08:00
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const parts = parseBodyRulePathParts(raw)
|
|
|
|
|
|
if (!parts) {
|
|
|
|
|
|
return '路径格式无效(不允许 .a / a. / a..b)'
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const topKey = (parts[0] || '').trim().toLowerCase()
|
|
|
|
|
|
if (RESERVED_BODY_FIELDS.has(topKey)) {
|
|
|
|
|
|
return `"${parts[0]}" 是系统保留的顶层字段`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedTo = raw.toLowerCase()
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
2026-02-21 17:08:31 +08:00
|
|
|
|
const currentRule = rules[index]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled && !currentRule.condition && !r.condition &&
|
2026-02-05 20:13:41 +08:00
|
|
|
|
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedTo) ||
|
|
|
|
|
|
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedTo))
|
2026-02-03 18:49:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return '字段路径重复'
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
function validateBodySetValue(rule: EditableBodyRule): string | null {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
if (rule.action !== 'set' && rule.action !== 'append' && rule.action !== 'insert') return null
|
2026-02-05 20:13:41 +08:00
|
|
|
|
|
|
|
|
|
|
const raw = rule.value.trim()
|
|
|
|
|
|
if (!raw) return '值不能为空'
|
|
|
|
|
|
try {
|
2026-02-10 21:01:45 +08:00
|
|
|
|
JSON.parse(prepareValueForJsonParse(raw))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const msg = err instanceof Error ? err.message : String(err)
|
|
|
|
|
|
return `JSON 格式错误:${msg}`
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取值验证状态:true=有效, false=无效, null=空
|
|
|
|
|
|
function getBodySetValueValidation(rule: EditableBodyRule): boolean | null {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
if (rule.action !== 'set' && rule.action !== 'append' && rule.action !== 'insert') return null
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const raw = rule.value.trim()
|
|
|
|
|
|
if (!raw) return null
|
|
|
|
|
|
try {
|
2026-02-10 21:01:45 +08:00
|
|
|
|
JSON.parse(prepareValueForJsonParse(raw))
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 02:56:21 +08:00
|
|
|
|
// 正则表达式验证状态:true=有效, false=无效, null=空
|
|
|
|
|
|
function getRegexPatternValidation(rule: EditableBodyRule): boolean | null {
|
|
|
|
|
|
if (rule.action !== 'regex_replace') return null
|
|
|
|
|
|
const pattern = rule.pattern.trim()
|
|
|
|
|
|
if (!pattern) return null
|
|
|
|
|
|
try {
|
|
|
|
|
|
new RegExp(pattern)
|
|
|
|
|
|
// 校验 flags
|
|
|
|
|
|
const flags = rule.flags.trim()
|
|
|
|
|
|
if (flags) {
|
|
|
|
|
|
const validFlags = new Set(['i', 'm', 's'])
|
|
|
|
|
|
for (const f of flags) {
|
|
|
|
|
|
if (!validFlags.has(f)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取正则验证提示
|
|
|
|
|
|
function getRegexPatternValidationTip(rule: EditableBodyRule): string {
|
|
|
|
|
|
const validation = getRegexPatternValidation(rule)
|
|
|
|
|
|
if (validation === null) return '输入正则表达式'
|
|
|
|
|
|
if (validation === true) return '有效的正则表达式'
|
|
|
|
|
|
try {
|
|
|
|
|
|
new RegExp(rule.pattern.trim())
|
|
|
|
|
|
// 正则有效但 flags 无效
|
|
|
|
|
|
return '无效的 flags(仅允许 i/m/s)'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
return err instanceof Error ? err.message : String(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
// 获取验证提示
|
|
|
|
|
|
function getBodySetValueValidationTip(rule: EditableBodyRule): string {
|
|
|
|
|
|
const validation = getBodySetValueValidation(rule)
|
|
|
|
|
|
if (validation === null) return '点击验证 JSON'
|
|
|
|
|
|
if (validation === true) {
|
2026-02-10 21:01:45 +08:00
|
|
|
|
const parsed = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim())))
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const type = Array.isArray(parsed) ? '数组' : typeof parsed === 'object' && parsed !== null ? '对象' : typeof parsed === 'string' ? '字符串' : typeof parsed === 'number' ? '数字' : typeof parsed === 'boolean' ? '布尔' : 'null'
|
|
|
|
|
|
return `有效的 JSON (${type})`
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-02-10 21:01:45 +08:00
|
|
|
|
JSON.parse(prepareValueForJsonParse(rule.value.trim()))
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return ''
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return err instanceof Error ? err.message : String(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 20:16:57 +08:00
|
|
|
|
function isStrictIntegerString(value: string): boolean {
|
|
|
|
|
|
return /^-?\d+$/.test(value.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isStrictNonNegativeIntegerString(value: string): boolean {
|
|
|
|
|
|
return /^\d+$/.test(value.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 14:59:37 +08:00
|
|
|
|
// 判断请求体规则是否有效(必填字段已填写)
|
|
|
|
|
|
function isBodyRuleEffective(r: EditableBodyRule): boolean {
|
|
|
|
|
|
switch (r.action) {
|
|
|
|
|
|
case 'set':
|
|
|
|
|
|
case 'drop':
|
|
|
|
|
|
return !!r.path.trim()
|
|
|
|
|
|
case 'rename':
|
|
|
|
|
|
return !!(r.from.trim() && r.to.trim())
|
|
|
|
|
|
case 'append':
|
|
|
|
|
|
return !!r.path.trim()
|
2026-04-22 20:16:57 +08:00
|
|
|
|
case 'insert':
|
|
|
|
|
|
return !!(r.path.trim() && r.index.trim())
|
2026-02-21 14:59:37 +08:00
|
|
|
|
case 'regex_replace':
|
|
|
|
|
|
return !!(r.path.trim() && r.pattern.trim())
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 获取端点的请求体规则数量(有效的规则)
|
|
|
|
|
|
function getEndpointBodyRulesCount(endpoint: ProviderEndpoint): number {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (state) {
|
2026-02-21 14:59:37 +08:00
|
|
|
|
return state.bodyRules.filter(isBodyRuleEffective).length
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
return endpoint.body_rules?.length || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查端点是否有任何请求体规则(包括正在编辑的空规则)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function _hasAnyBodyRules(endpoint: ProviderEndpoint): boolean {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
return state.bodyRules.length > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return (endpoint.body_rules?.length || 0) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
// 获取端点的总规则数量(请求头 + 请求体 + 响应头)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
function getTotalRulesCount(endpoint: ProviderEndpoint): number {
|
2026-05-06 01:29:42 +08:00
|
|
|
|
return getEndpointRulesCount(endpoint) + getEndpointBodyRulesCount(endpoint) + getEndpointResponseRulesCount(endpoint)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化请求头规则的显示标签
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function _formatHeaderRuleLabel(rule: EditableRule): string {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (rule.action === 'set') {
|
|
|
|
|
|
if (!rule.key) return '(未设置)'
|
|
|
|
|
|
return `${rule.key}=${rule.value || '...'}`
|
|
|
|
|
|
} else if (rule.action === 'drop') {
|
|
|
|
|
|
if (!rule.key) return '(未设置)'
|
|
|
|
|
|
return `-${rule.key}`
|
|
|
|
|
|
} else if (rule.action === 'rename') {
|
|
|
|
|
|
if (!rule.from || !rule.to) return '(未设置)'
|
|
|
|
|
|
return `${rule.from}→${rule.to}`
|
|
|
|
|
|
}
|
|
|
|
|
|
return '(未知)'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化请求体规则的显示标签
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function _formatBodyRuleLabel(rule: EditableBodyRule): string {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (rule.action === 'set') {
|
|
|
|
|
|
if (!rule.path) return '(未设置)'
|
|
|
|
|
|
return `${rule.path}=${rule.value || '...'}`
|
|
|
|
|
|
} else if (rule.action === 'drop') {
|
|
|
|
|
|
if (!rule.path) return '(未设置)'
|
|
|
|
|
|
return `-${rule.path}`
|
|
|
|
|
|
} else if (rule.action === 'rename') {
|
|
|
|
|
|
if (!rule.from || !rule.to) return '(未设置)'
|
|
|
|
|
|
return `${rule.from}→${rule.to}`
|
2026-04-22 20:16:57 +08:00
|
|
|
|
} else if (rule.action === 'append') {
|
|
|
|
|
|
if (!rule.path) return '(未设置)'
|
|
|
|
|
|
return `${rule.path}[]+=${rule.value || '...'}`
|
|
|
|
|
|
} else if (rule.action === 'insert') {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
if (!rule.path) return '(未设置)'
|
|
|
|
|
|
const idx = rule.index?.trim() || '末尾'
|
|
|
|
|
|
return `${rule.path}[${idx}]+=${rule.value || '...'}`
|
|
|
|
|
|
} else if (rule.action === 'regex_replace') {
|
|
|
|
|
|
if (!rule.path || !rule.pattern) return '(未设置)'
|
2026-04-22 20:16:57 +08:00
|
|
|
|
const flags = rule.flags.trim()
|
|
|
|
|
|
const count = rule.count.trim()
|
|
|
|
|
|
return `${rule.path}: s/${rule.pattern}/${rule.replacement || ''}/${flags}${count ? ` ×${count}` : ''}`
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
return '(未知)'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查端点请求体规则是否有修改
|
|
|
|
|
|
function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state) return false
|
|
|
|
|
|
|
|
|
|
|
|
const originalRules = endpoint.body_rules || []
|
2026-02-21 14:59:37 +08:00
|
|
|
|
const editedRules = state.bodyRules.filter(isBodyRuleEffective)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (editedRules.length !== originalRules.length) return true
|
|
|
|
|
|
for (let i = 0; i < editedRules.length; i++) {
|
|
|
|
|
|
const edited = editedRules[i]
|
|
|
|
|
|
const original = originalRules[i]
|
|
|
|
|
|
if (!original) return true
|
|
|
|
|
|
if (edited.action !== original.action) return true
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (edited.enabled !== (original.enabled !== false)) return true
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (edited.action === 'set' && original.action === 'set') {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
const baseline = initBodyRuleSetValueForEditor(original.value)
|
2026-02-05 20:13:41 +08:00
|
|
|
|
if (edited.path !== original.path) return true
|
|
|
|
|
|
if (edited.value !== baseline.value) return true
|
2026-02-03 18:49:17 +08:00
|
|
|
|
} else if (edited.action === 'drop' && original.action === 'drop') {
|
|
|
|
|
|
if (edited.path !== original.path) return true
|
|
|
|
|
|
} else if (edited.action === 'rename' && original.action === 'rename') {
|
|
|
|
|
|
if (edited.from !== original.from || edited.to !== original.to) return true
|
2026-04-22 20:16:57 +08:00
|
|
|
|
} else if (edited.action === 'append' && original.action === 'append') {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
const baseline = initBodyRuleSetValueForEditor(original.value)
|
|
|
|
|
|
if (edited.path !== original.path) return true
|
|
|
|
|
|
if (edited.value !== baseline.value) return true
|
|
|
|
|
|
} else if (edited.action === 'insert' && original.action === 'insert') {
|
|
|
|
|
|
const baseline = initBodyRuleSetValueForEditor(original.value)
|
|
|
|
|
|
if (edited.path !== original.path) return true
|
|
|
|
|
|
if (edited.index !== String(original.index ?? '')) return true
|
|
|
|
|
|
if (edited.value !== baseline.value) return true
|
|
|
|
|
|
} else if (edited.action === 'regex_replace' && original.action === 'regex_replace') {
|
|
|
|
|
|
if (edited.path !== original.path) return true
|
|
|
|
|
|
if (edited.pattern !== (original.pattern ?? '')) return true
|
|
|
|
|
|
if (edited.replacement !== (original.replacement ?? '')) return true
|
|
|
|
|
|
if (edited.flags !== (original.flags ?? '')) return true
|
2026-04-22 20:16:57 +08:00
|
|
|
|
if (edited.count !== (original.count === undefined || original.count === null ? '' : String(original.count))) return true
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
2026-03-15 20:27:53 +08:00
|
|
|
|
if (!conditionEquals(edited.condition, conditionToEditable(original.condition))) return true
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将可编辑请求体规则数组转换为 API 需要的 BodyRule[]
|
|
|
|
|
|
function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
|
|
|
|
|
const result: BodyRule[] = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const rule of rules) {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const condition = editableConditionToApi(rule.condition)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const common = { ...(rule.enabled ? {} : { enabled: false }), ...(condition ? { condition } : {}) }
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (rule.action === 'set' && rule.path.trim()) {
|
2026-02-22 00:43:41 +08:00
|
|
|
|
let value: unknown = rule.value
|
2026-02-10 21:01:45 +08:00
|
|
|
|
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'set', path: rule.path.trim(), value, ...common })
|
2026-02-03 18:49:17 +08:00
|
|
|
|
} else if (rule.action === 'drop' && rule.path.trim()) {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'drop', path: rule.path.trim(), ...common })
|
2026-02-03 18:49:17 +08:00
|
|
|
|
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...common })
|
2026-04-22 20:16:57 +08:00
|
|
|
|
} else if (rule.action === 'append' && rule.path.trim()) {
|
2026-02-22 00:43:41 +08:00
|
|
|
|
let value: unknown = rule.value
|
2026-02-10 21:01:45 +08:00
|
|
|
|
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'append', path: rule.path.trim(), value, ...common })
|
2026-04-22 20:16:57 +08:00
|
|
|
|
} 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)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'insert', path: rule.path.trim(), index: idx, value, ...common })
|
2026-02-09 02:56:21 +08:00
|
|
|
|
} else if (rule.action === 'regex_replace' && rule.path.trim() && rule.pattern.trim()) {
|
|
|
|
|
|
const entry: BodyRuleRegexReplace = {
|
|
|
|
|
|
action: 'regex_replace',
|
|
|
|
|
|
path: rule.path.trim(),
|
|
|
|
|
|
pattern: rule.pattern,
|
|
|
|
|
|
replacement: rule.replacement || '',
|
|
|
|
|
|
...(rule.flags.trim() ? { flags: rule.flags.trim() } : {}),
|
2026-04-22 20:16:57 +08:00
|
|
|
|
...(isStrictNonNegativeIntegerString(rule.count) ? { count: parseInt(rule.count.trim(), 10) } : {}),
|
2026-02-09 02:56:21 +08:00
|
|
|
|
}
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ ...entry, ...common })
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result.length > 0 ? result : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 20:13:41 +08:00
|
|
|
|
function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
2026-02-03 18:49:17 +08:00
|
|
|
|
const rules = getEndpointEditBodyRules(endpointId)
|
|
|
|
|
|
for (let i = 0; i < rules.length; i++) {
|
|
|
|
|
|
const rule = rules[i]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (!rule.enabled) continue
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const prefix = `第 ${i + 1} 条请求体规则:`
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
if (rule.action === 'set' || rule.action === 'drop') {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
|
|
|
|
|
if (pathErr) return `${prefix}${pathErr}`
|
|
|
|
|
|
if (rule.action === 'set') {
|
|
|
|
|
|
const valueErr = validateBodySetValue(rule)
|
|
|
|
|
|
if (valueErr) return `${prefix}${valueErr}`
|
|
|
|
|
|
}
|
2026-02-03 18:49:17 +08:00
|
|
|
|
} else if (rule.action === 'rename') {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const fromErr = validateBodyRenameFromForEndpoint(endpointId, rule.from, i)
|
|
|
|
|
|
if (fromErr) return `${prefix}${fromErr}`
|
|
|
|
|
|
const toErr = validateBodyRenameToForEndpoint(endpointId, rule.to, i)
|
|
|
|
|
|
if (toErr) return `${prefix}${toErr}`
|
2026-04-22 20:16:57 +08:00
|
|
|
|
} else if (rule.action === 'append') {
|
|
|
|
|
|
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
|
|
|
|
|
if (pathErr) return `${prefix}${pathErr}`
|
|
|
|
|
|
const valueErr = validateBodySetValue(rule)
|
|
|
|
|
|
if (valueErr) return `${prefix}${valueErr}`
|
|
|
|
|
|
} else if (rule.action === 'insert') {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
|
|
|
|
|
if (pathErr) return `${prefix}${pathErr}`
|
|
|
|
|
|
const indexStr = rule.index.trim()
|
2026-04-22 20:16:57 +08:00
|
|
|
|
if (!indexStr) return `${prefix}插入位置不能为空`
|
|
|
|
|
|
if (!isStrictIntegerString(indexStr)) return `${prefix}位置必须为整数`
|
2026-02-09 02:56:21 +08:00
|
|
|
|
const valueErr = validateBodySetValue(rule)
|
|
|
|
|
|
if (valueErr) return `${prefix}${valueErr}`
|
|
|
|
|
|
} else if (rule.action === 'regex_replace') {
|
|
|
|
|
|
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
|
|
|
|
|
|
if (pathErr) return `${prefix}${pathErr}`
|
|
|
|
|
|
if (!rule.pattern.trim()) return `${prefix}正则表达式不能为空`
|
|
|
|
|
|
try {
|
|
|
|
|
|
new RegExp(rule.pattern.trim())
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-02-09 02:56:21 +08:00
|
|
|
|
return `${prefix}正则表达式无效:${err instanceof Error ? err.message : String(err)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
const flags = rule.flags.trim()
|
|
|
|
|
|
if (flags) {
|
|
|
|
|
|
const validFlags = new Set(['i', 'm', 's'])
|
|
|
|
|
|
for (const f of flags) {
|
|
|
|
|
|
if (!validFlags.has(f)) return `${prefix}flags 仅允许 i/m/s,非法字符: ${f}`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-22 20:16:57 +08:00
|
|
|
|
const count = rule.count.trim()
|
|
|
|
|
|
if (count) {
|
|
|
|
|
|
if (!isStrictNonNegativeIntegerString(count)) return `${prefix}替换次数必须是大于等于 0 的整数`
|
|
|
|
|
|
}
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
2026-03-15 20:27:53 +08:00
|
|
|
|
|
|
|
|
|
|
const conditionErr = validateEditableCondition(rule.condition)
|
|
|
|
|
|
if (conditionErr) return `${prefix}${conditionErr}`
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查请求体规则是否有验证错误
|
2026-02-09 01:05:48 +08:00
|
|
|
|
function _hasBodyValidationErrorsForEndpoint(endpointId: string): boolean {
|
2026-02-05 20:13:41 +08:00
|
|
|
|
return !!getBodyValidationErrorForEndpoint(endpointId)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 检查端点 URL/路径是否有修改
|
|
|
|
|
|
function hasUrlChanges(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state) return false
|
|
|
|
|
|
if (state.url !== endpoint.base_url) return true
|
|
|
|
|
|
if (state.path !== (endpoint.custom_path || '')) return true
|
2026-02-05 17:20:34 +08:00
|
|
|
|
// 注:upstreamStreamPolicy 现在由头部按钮直接保存,无需在此检查
|
2026-01-22 01:48:56 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查端点规则是否有修改
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function editableHeaderRulesChanged(edited: EditableRule[], originalRules: HeaderRule[]): boolean {
|
|
|
|
|
|
const editedRules = edited.filter(r => {
|
2026-01-22 01:48:56 +08:00
|
|
|
|
if (r.action === 'set' || r.action === 'drop') return r.key.trim()
|
|
|
|
|
|
if (r.action === 'rename') return r.from.trim() && r.to.trim()
|
|
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
if (editedRules.length !== originalRules.length) return true
|
|
|
|
|
|
for (let i = 0; i < editedRules.length; i++) {
|
|
|
|
|
|
const edited = editedRules[i]
|
|
|
|
|
|
const original = originalRules[i]
|
|
|
|
|
|
if (!original) return true
|
|
|
|
|
|
if (edited.action !== original.action) return true
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (edited.enabled !== (original.enabled !== false)) return true
|
2026-01-22 01:48:56 +08:00
|
|
|
|
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') {
|
|
|
|
|
|
if (edited.key !== original.key) return true
|
|
|
|
|
|
} else if (edited.action === 'rename' && original.action === 'rename') {
|
|
|
|
|
|
if (edited.from !== original.from || edited.to !== original.to) return true
|
|
|
|
|
|
}
|
2026-03-15 20:27:53 +08:00
|
|
|
|
if (!conditionEquals(edited.condition, conditionToEditable(original.condition))) return true
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function hasRulesChanges(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state) return false
|
|
|
|
|
|
return editableHeaderRulesChanged(state.rules, endpoint.header_rules || [])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasResponseHeaderRulesChanges(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state) return false
|
|
|
|
|
|
return editableHeaderRulesChanged(state.responseRules, getEndpointResponseHeaderRules(endpoint))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:13 +08:00
|
|
|
|
function hasRulePanelChanges(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
return hasRulesChanges(endpoint)
|
|
|
|
|
|
|| hasBodyRulesChanges(endpoint)
|
|
|
|
|
|
|| hasResponseHeaderRulesChanges(endpoint)
|
|
|
|
|
|
|| (isEndpointRulesJsonMode(endpoint.id) && endpointRulesJsonDirty.value[endpoint.id] === true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 检查端点是否有修改(URL、路径或规则)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 注:当前模板直接使用各子函数,此聚合函数保留供未来使用
|
|
|
|
|
|
function _hasEndpointChanges(endpoint: ProviderEndpoint): boolean {
|
2026-05-06 21:30:13 +08:00
|
|
|
|
return hasUrlChanges(endpoint) || hasRulePanelChanges(endpoint)
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置端点修改
|
|
|
|
|
|
function resetEndpointChanges(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
|
2026-05-06 21:30:13 +08:00
|
|
|
|
if (isEndpointRulesJsonMode(endpoint.id)) {
|
|
|
|
|
|
refreshEndpointRulesJsonDraft(endpoint.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
endpointRulesJsonError.value[endpoint.id] = null
|
|
|
|
|
|
endpointRulesJsonDirty.value[endpoint.id] = false
|
|
|
|
|
|
}
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-02 22:08:39 +08:00
|
|
|
|
async function handleResetBodyRulesToDefault(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
resettingDefaultRulesEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const defaultRules = await loadDefaultBodyRulesForFormat(endpoint.api_format, true)
|
|
|
|
|
|
if (!defaultRules.length) {
|
|
|
|
|
|
showError('该端点没有默认请求体规则')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!endpointEditStates.value[endpoint.id]) {
|
|
|
|
|
|
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
|
|
|
|
|
|
}
|
|
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state) return
|
|
|
|
|
|
|
|
|
|
|
|
const resetState = initEndpointEditState({
|
|
|
|
|
|
...endpoint,
|
|
|
|
|
|
body_rules: defaultRules,
|
|
|
|
|
|
})
|
|
|
|
|
|
state.bodyRules = resetState.bodyRules
|
2026-05-06 01:29:42 +08:00
|
|
|
|
endpointRulesExpanded.value[endpoint.id] = (state.rules.length + state.responseRules.length + state.bodyRules.length) > 0
|
2026-03-02 22:08:39 +08:00
|
|
|
|
clearBodyRuleDragState(endpoint.id)
|
|
|
|
|
|
clearBodyRuleSelectOpen(endpoint.id)
|
2026-05-06 21:30:13 +08:00
|
|
|
|
if (isEndpointRulesJsonMode(endpoint.id)) {
|
|
|
|
|
|
refreshEndpointRulesJsonDraft(endpoint.id)
|
|
|
|
|
|
}
|
2026-03-02 22:08:39 +08:00
|
|
|
|
success('已重置请求体为默认规则,请点击保存生效')
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '重置失败'), '错误')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
resettingDefaultRulesEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 01:18:54 +08:00
|
|
|
|
// 将可编辑规则数组转换为 API 需要的 HeaderRule[]
|
2026-01-16 20:06:20 +08:00
|
|
|
|
function rulesToHeaderRules(rules: EditableRule[]): HeaderRule[] | null {
|
2026-01-16 01:18:54 +08:00
|
|
|
|
const result: HeaderRule[] = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const rule of rules) {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const condition = editableConditionToApi(rule.condition)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const common = { ...(rule.enabled ? {} : { enabled: false }), ...(condition ? { condition } : {}) }
|
2026-01-16 01:18:54 +08:00
|
|
|
|
if (rule.action === 'set' && rule.key.trim()) {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'set', key: rule.key.trim(), value: rule.value, ...common })
|
2026-01-16 01:18:54 +08:00
|
|
|
|
} else if (rule.action === 'drop' && rule.key.trim()) {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'drop', key: rule.key.trim(), ...common })
|
2026-01-16 01:18:54 +08:00
|
|
|
|
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
2026-05-07 00:48:15 +08:00
|
|
|
|
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...common })
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 20:06:20 +08:00
|
|
|
|
return result.length > 0 ? result : null
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function endpointConfigWithResponseHeaderRules(endpoint: ProviderEndpoint, rules: HeaderRule[] | null): Record<string, unknown> | null {
|
|
|
|
|
|
const merged: Record<string, unknown> = { ...(endpoint.config || {}) }
|
|
|
|
|
|
delete merged[RESPONSE_HEADER_RULES_CONFIG_KEY]
|
|
|
|
|
|
delete merged[RESPONSE_HEADER_RULES_CAMEL_CONFIG_KEY]
|
|
|
|
|
|
if (rules && rules.length > 0) {
|
|
|
|
|
|
merged[RESPONSE_HEADER_RULES_CONFIG_KEY] = rules
|
|
|
|
|
|
}
|
|
|
|
|
|
return Object.keys(merged).length > 0 ? merged : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 20:27:53 +08:00
|
|
|
|
function getHeaderValidationErrorForEndpoint(endpointId: string): string | null {
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const rules = getEndpointEditRules(endpointId)
|
|
|
|
|
|
for (let i = 0; i < rules.length; i++) {
|
|
|
|
|
|
const rule = rules[i]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (!rule.enabled) continue
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const prefix = `第 ${i + 1} 条请求头规则:`
|
2026-01-16 01:18:54 +08:00
|
|
|
|
if (rule.action === 'set' || rule.action === 'drop') {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const err = validateRuleKeyForEndpoint(endpointId, rule.key, i)
|
|
|
|
|
|
if (err) return `${prefix}${err}`
|
2026-01-16 01:18:54 +08:00
|
|
|
|
} else if (rule.action === 'rename') {
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const fromErr = validateRenameFromForEndpoint(endpointId, rule.from, i)
|
|
|
|
|
|
if (fromErr) return `${prefix}${fromErr}`
|
|
|
|
|
|
const toErr = validateRenameToForEndpoint(endpointId, rule.to, i)
|
|
|
|
|
|
if (toErr) return `${prefix}${toErr}`
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const conditionErr = validateEditableCondition(rule.condition)
|
|
|
|
|
|
if (conditionErr) return `${prefix}${conditionErr}`
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
2026-03-15 20:27:53 +08:00
|
|
|
|
return null
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
function validateResponseHeaderNameForEndpoint(endpointId: string, name: string, index: number, field: 'key' | 'from' | 'to'): string | null {
|
|
|
|
|
|
const trimmedName = name.trim().toLowerCase()
|
|
|
|
|
|
if (!trimmedName) return null
|
|
|
|
|
|
|
|
|
|
|
|
if ((field === 'key' || field === 'to') && RESERVED_RESPONSE_HEADERS.has(trimmedName)) {
|
|
|
|
|
|
return `"${name}" 是系统保留的响应头`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
2026-05-07 00:48:15 +08:00
|
|
|
|
const currentRule = rules[index]
|
|
|
|
|
|
if (currentRule && !currentRule.enabled) return null
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const duplicate = rules.findIndex(
|
2026-05-07 00:48:15 +08:00
|
|
|
|
(r, i) => i !== index && r.enabled && (
|
2026-05-06 01:29:42 +08:00
|
|
|
|
((r.action === 'set' || r.action === 'drop') && r.key.trim().toLowerCase() === trimmedName) ||
|
|
|
|
|
|
(r.action === 'rename' && (field === 'from'
|
|
|
|
|
|
? r.from.trim().toLowerCase() === trimmedName
|
|
|
|
|
|
: r.to.trim().toLowerCase() === trimmedName))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (duplicate >= 0) {
|
|
|
|
|
|
return field === 'from' ? '该响应头已被其他规则处理' : '响应头名称重复'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getResponseHeaderValidationErrorForEndpoint(endpointId: string): string | null {
|
|
|
|
|
|
const rules = getEndpointEditResponseRules(endpointId)
|
|
|
|
|
|
for (let i = 0; i < rules.length; i++) {
|
|
|
|
|
|
const rule = rules[i]
|
2026-05-07 00:48:15 +08:00
|
|
|
|
if (!rule.enabled) continue
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const prefix = `第 ${i + 1} 条响应头规则:`
|
|
|
|
|
|
if (rule.action === 'set' || rule.action === 'drop') {
|
|
|
|
|
|
const err = validateResponseHeaderNameForEndpoint(endpointId, rule.key, i, 'key')
|
|
|
|
|
|
if (err) return `${prefix}${err}`
|
|
|
|
|
|
} else if (rule.action === 'rename') {
|
|
|
|
|
|
const fromErr = validateResponseHeaderNameForEndpoint(endpointId, rule.from, i, 'from')
|
|
|
|
|
|
if (fromErr) return `${prefix}${fromErr}`
|
|
|
|
|
|
const toErr = validateResponseHeaderNameForEndpoint(endpointId, rule.to, i, 'to')
|
|
|
|
|
|
if (toErr) return `${prefix}${toErr}`
|
|
|
|
|
|
}
|
|
|
|
|
|
const conditionErr = validateEditableCondition(rule.condition)
|
|
|
|
|
|
if (conditionErr) return `${prefix}${conditionErr}`
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 新端点选择的格式的默认路径
|
|
|
|
|
|
const newEndpointDefaultPath = computed(() => {
|
2026-02-05 09:48:37 +08:00
|
|
|
|
// 使用填写的 base_url 或 provider 的 website 来判断是否是 Codex 端点
|
|
|
|
|
|
const baseUrl = newEndpoint.value.base_url || props.provider?.website || ''
|
|
|
|
|
|
return getDefaultPath(newEndpoint.value.api_format, baseUrl)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 加载 API 格式列表
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const loadApiFormats = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await adminApi.getApiFormats()
|
|
|
|
|
|
apiFormats.value = response.formats
|
|
|
|
|
|
} catch (error) {
|
2025-12-12 20:22:03 +08:00
|
|
|
|
log.error('加载API格式失败:', error)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadApiFormats()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 监听 props 变化
|
|
|
|
|
|
watch(() => props.modelValue, (open) => {
|
2026-02-11 03:16:53 +08:00
|
|
|
|
bodyRuleHelpOpenEndpointId.value = null
|
2026-03-02 18:02:21 +08:00
|
|
|
|
ruleSelectOpen.value = {}
|
2026-05-06 01:29:42 +08:00
|
|
|
|
responseRuleSelectOpen.value = {}
|
2026-03-02 18:02:21 +08:00
|
|
|
|
bodyRuleSelectOpen.value = {}
|
|
|
|
|
|
headerRuleDraggedIndex.value = {}
|
|
|
|
|
|
headerRuleDragOverIndex.value = {}
|
2026-05-06 01:29:42 +08:00
|
|
|
|
responseRuleDraggedIndex.value = {}
|
|
|
|
|
|
responseRuleDragOverIndex.value = {}
|
2026-03-02 18:02:21 +08:00
|
|
|
|
bodyRuleDraggedIndex.value = {}
|
|
|
|
|
|
bodyRuleDragOverIndex.value = {}
|
2026-05-06 21:30:13 +08:00
|
|
|
|
endpointRulesJsonMode.value = {}
|
|
|
|
|
|
endpointRulesJsonDraft.value = {}
|
|
|
|
|
|
endpointRulesJsonError.value = {}
|
|
|
|
|
|
endpointRulesJsonDirty.value = {}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if (open) {
|
|
|
|
|
|
localEndpoints.value = [...(props.endpoints || [])]
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 清空编辑状态,重新从端点加载
|
|
|
|
|
|
endpointEditStates.value = {}
|
|
|
|
|
|
endpointRulesExpanded.value = {}
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 初始化每个端点的编辑状态,有规则时默认展开
|
2026-01-22 01:48:56 +08:00
|
|
|
|
for (const endpoint of localEndpoints.value) {
|
|
|
|
|
|
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 有规则时默认展开
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const hasRules = (endpoint.header_rules?.length || 0) + getEndpointResponseHeaderRules(endpoint).length + (endpoint.body_rules?.length || 0) > 0
|
2026-02-03 18:49:17 +08:00
|
|
|
|
endpointRulesExpanded.value[endpoint.id] = hasRules
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
2026-03-02 22:08:39 +08:00
|
|
|
|
void preloadDefaultBodyRules(localEndpoints.value)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 关闭对话框时完全清空新端点表单
|
|
|
|
|
|
newEndpoint.value = { api_format: '', base_url: '', custom_path: '' }
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => props.endpoints, (endpoints) => {
|
|
|
|
|
|
if (props.modelValue) {
|
|
|
|
|
|
localEndpoints.value = [...(endpoints || [])]
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 初始化新添加端点的编辑状态
|
|
|
|
|
|
for (const endpoint of localEndpoints.value) {
|
|
|
|
|
|
if (!endpointEditStates.value[endpoint.id]) {
|
|
|
|
|
|
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
|
|
|
|
|
|
}
|
2026-05-06 21:30:13 +08:00
|
|
|
|
if (isEndpointRulesJsonMode(endpoint.id) && !endpointRulesJsonDirty.value[endpoint.id]) {
|
|
|
|
|
|
refreshEndpointRulesJsonDraft(endpoint.id)
|
|
|
|
|
|
}
|
2026-01-22 01:48:56 +08:00
|
|
|
|
}
|
2026-03-02 22:08:39 +08:00
|
|
|
|
const newFormats = localEndpoints.value
|
2026-04-07 02:50:19 +08:00
|
|
|
|
.filter(e => e.api_format && !defaultBodyRulesLoaded.value[defaultBodyRulesCacheKey(e.api_format)])
|
2026-03-02 22:08:39 +08:00
|
|
|
|
.map(e => ({ api_format: e.api_format }) as ProviderEndpoint)
|
|
|
|
|
|
if (newFormats.length) {
|
|
|
|
|
|
void preloadDefaultBodyRules(newFormats)
|
|
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
|
|
|
|
|
// 保存端点
|
2026-01-22 01:48:56 +08:00
|
|
|
|
async function saveEndpoint(endpoint: ProviderEndpoint) {
|
2026-05-06 21:30:13 +08:00
|
|
|
|
if (isEndpointRulesJsonMode(endpoint.id) && endpointRulesJsonDirty.value[endpoint.id]) {
|
|
|
|
|
|
if (!applyEndpointRulesJsonDraft(endpoint.id, { notify: false })) return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
if (!state || !state.url) return
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-16 01:18:54 +08:00
|
|
|
|
// 检查规则是否有验证错误
|
2026-03-15 20:27:53 +08:00
|
|
|
|
const headerErr = getHeaderValidationErrorForEndpoint(endpoint.id)
|
|
|
|
|
|
if (headerErr) {
|
|
|
|
|
|
showError(headerErr)
|
2026-01-16 01:18:54 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 01:29:42 +08:00
|
|
|
|
const responseHeaderErr = getResponseHeaderValidationErrorForEndpoint(endpoint.id)
|
|
|
|
|
|
if (responseHeaderErr) {
|
|
|
|
|
|
showError(responseHeaderErr)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 18:49:17 +08:00
|
|
|
|
// 检查请求体规则是否有验证错误
|
2026-02-05 20:13:41 +08:00
|
|
|
|
const bodyErr = getBodyValidationErrorForEndpoint(endpoint.id)
|
|
|
|
|
|
if (bodyErr) {
|
|
|
|
|
|
showError(bodyErr)
|
2026-02-03 18:49:17 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
savingEndpointId.value = endpoint.id
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try {
|
2026-02-05 15:57:52 +08:00
|
|
|
|
// 仅提交变更字段,避免 fixed provider 因 base_url/custom_path 被锁定而更新失败
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const payload: Record<string, unknown> = {}
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
|
|
|
|
|
if (!isFixedProvider.value) {
|
|
|
|
|
|
if (state.url !== endpoint.base_url) payload.base_url = state.url
|
|
|
|
|
|
if (state.path !== (endpoint.custom_path || '')) payload.custom_path = state.path || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasRulesChanges(endpoint)) payload.header_rules = rulesToHeaderRules(state.rules)
|
2026-05-06 01:29:42 +08:00
|
|
|
|
if (hasResponseHeaderRulesChanges(endpoint)) {
|
|
|
|
|
|
payload.config = endpointConfigWithResponseHeaderRules(
|
|
|
|
|
|
endpoint,
|
|
|
|
|
|
rulesToHeaderRules(state.responseRules),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-02-05 15:57:52 +08:00
|
|
|
|
if (hasBodyRulesChanges(endpoint)) payload.body_rules = rulesToBodyRules(state.bodyRules)
|
|
|
|
|
|
|
2026-02-05 17:20:34 +08:00
|
|
|
|
// 注:upstreamStreamPolicy 现在由头部按钮直接保存,不在此处处理
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
|
|
|
|
|
if (Object.keys(payload).length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
await updateEndpoint(endpoint.id, payload)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
success('端点已更新')
|
|
|
|
|
|
emit('endpointUpdated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '更新失败'), '错误')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} finally {
|
2026-01-10 18:43:53 +08:00
|
|
|
|
savingEndpointId.value = null
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-18 16:14:37 +08:00
|
|
|
|
|
2026-01-22 01:48:56 +08:00
|
|
|
|
// 切换格式转换(直接保存)
|
|
|
|
|
|
async function handleToggleFormatConversion(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
const currentEnabled = endpoint.format_acceptance_config?.enabled || false
|
|
|
|
|
|
const newEnabled = !currentEnabled
|
|
|
|
|
|
|
|
|
|
|
|
togglingFormatEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
await updateEndpoint(endpoint.id, {
|
|
|
|
|
|
format_acceptance_config: newEnabled ? { enabled: true } : null,
|
|
|
|
|
|
})
|
|
|
|
|
|
success(newEnabled ? '已启用格式转换' : '已关闭格式转换')
|
|
|
|
|
|
emit('endpointUpdated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '操作失败'), '错误')
|
2026-01-22 01:48:56 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
togglingFormatEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:20:34 +08:00
|
|
|
|
// 获取上游流式按钮的当前状态(优先使用编辑状态)
|
|
|
|
|
|
function getCurrentUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
if (isUpstreamStreamPolicyLocked(endpoint)) return 'force_stream'
|
2026-02-05 17:20:34 +08:00
|
|
|
|
const state = endpointEditStates.value[endpoint.id]
|
|
|
|
|
|
return state?.upstreamStreamPolicy ?? getEndpointUpstreamStreamPolicy(endpoint)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 23:55:26 +08:00
|
|
|
|
function isUpstreamStreamPolicyLocked(endpoint: ProviderEndpoint): boolean {
|
|
|
|
|
|
return (props.provider?.provider_type || '').toLowerCase() === 'codex'
|
2026-05-18 17:35:36 +00:00
|
|
|
|
&& normalizeEndpointApiFormat(endpoint.api_format) === 'openai:responses'
|
2026-03-01 23:55:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:20:34 +08:00
|
|
|
|
// 获取上游流式按钮的样式类
|
|
|
|
|
|
function getUpstreamStreamButtonClass(endpoint: ProviderEndpoint): string {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
if (isUpstreamStreamPolicyLocked(endpoint)) {
|
|
|
|
|
|
return 'h-7 w-7 text-primary/70 cursor-not-allowed'
|
|
|
|
|
|
}
|
2026-02-05 17:20:34 +08:00
|
|
|
|
const policy = getCurrentUpstreamStreamPolicy(endpoint)
|
|
|
|
|
|
const base = 'h-7 w-7'
|
|
|
|
|
|
if (policy === 'force_stream') return `${base} text-primary`
|
|
|
|
|
|
if (policy === 'force_non_stream') return `${base} text-destructive`
|
|
|
|
|
|
return `${base} text-muted-foreground` // auto - 跟随请求,淡色显示
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取上游流式按钮的提示文字
|
|
|
|
|
|
function getUpstreamStreamTooltip(endpoint: ProviderEndpoint): string {
|
2026-04-26 20:32:55 +08:00
|
|
|
|
if (isUpstreamStreamPolicyLocked(endpoint)) return '固定流式(Codex OpenAI Responses,已锁定)'
|
2026-02-05 17:20:34 +08:00
|
|
|
|
const policy = getCurrentUpstreamStreamPolicy(endpoint)
|
|
|
|
|
|
if (policy === 'force_stream') return '固定流式(点击切换为固定非流)'
|
|
|
|
|
|
if (policy === 'force_non_stream') return '固定非流(点击切换为跟随请求)'
|
|
|
|
|
|
return '跟随请求(点击切换为固定流式)'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 循环切换上游流式策略并直接保存
|
|
|
|
|
|
async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
2026-03-01 23:55:26 +08:00
|
|
|
|
if (isUpstreamStreamPolicyLocked(endpoint)) return
|
|
|
|
|
|
|
2026-02-05 17:20:34 +08:00
|
|
|
|
const currentPolicy = getCurrentUpstreamStreamPolicy(endpoint)
|
|
|
|
|
|
let nextPolicy: string
|
|
|
|
|
|
let nextLabel: string
|
|
|
|
|
|
|
|
|
|
|
|
// 循环:auto -> force_stream -> force_non_stream -> auto
|
|
|
|
|
|
if (currentPolicy === 'auto') {
|
|
|
|
|
|
nextPolicy = 'force_stream'
|
|
|
|
|
|
nextLabel = '固定流式'
|
|
|
|
|
|
} else if (currentPolicy === 'force_stream') {
|
|
|
|
|
|
nextPolicy = 'force_non_stream'
|
|
|
|
|
|
nextLabel = '固定非流'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nextPolicy = 'auto'
|
|
|
|
|
|
nextLabel = '跟随请求'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
savingEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const merged: Record<string, unknown> = { ...(endpoint.config || {}) }
|
2026-02-05 17:20:34 +08:00
|
|
|
|
// 清理旧的 key
|
|
|
|
|
|
delete merged.upstream_stream_policy
|
|
|
|
|
|
delete merged.upstreamStreamPolicy
|
|
|
|
|
|
delete merged.upstream_stream
|
|
|
|
|
|
|
|
|
|
|
|
if (nextPolicy !== 'auto') {
|
|
|
|
|
|
merged.upstream_stream_policy = nextPolicy
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await updateEndpoint(endpoint.id, {
|
|
|
|
|
|
config: Object.keys(merged).length > 0 ? merged : null,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地编辑状态
|
|
|
|
|
|
if (endpointEditStates.value[endpoint.id]) {
|
|
|
|
|
|
endpointEditStates.value[endpoint.id].upstreamStreamPolicy = nextPolicy
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
success(`已切换为${nextLabel}`)
|
|
|
|
|
|
emit('endpointUpdated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '操作失败'), '错误')
|
2026-02-05 17:20:34 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
savingEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 添加端点
|
|
|
|
|
|
async function handleAddEndpoint() {
|
2026-01-20 01:32:47 +08:00
|
|
|
|
if (!props.provider || !newEndpoint.value.api_format) return
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有输入 base_url,使用提供商的 website 作为默认值
|
|
|
|
|
|
const baseUrl = newEndpoint.value.base_url || props.provider.website
|
|
|
|
|
|
if (!baseUrl) {
|
|
|
|
|
|
showError('请输入 Base URL')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
addingEndpoint.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await createEndpoint(props.provider.id, {
|
|
|
|
|
|
provider_id: props.provider.id,
|
|
|
|
|
|
api_format: newEndpoint.value.api_format,
|
2026-01-20 01:32:47 +08:00
|
|
|
|
base_url: baseUrl,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
custom_path: newEndpoint.value.custom_path || undefined,
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
})
|
2026-02-19 14:51:08 +08:00
|
|
|
|
success(`已添加 ${formatApiFormat(newEndpoint.value.api_format)} 端点`)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
// 重置表单,保留 URL
|
2026-01-20 01:32:47 +08:00
|
|
|
|
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
2026-01-10 18:43:53 +08:00
|
|
|
|
emit('endpointCreated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '添加失败'), '错误')
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
addingEndpoint.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 切换端点启用状态
|
|
|
|
|
|
async function handleToggleEndpoint(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
togglingEndpointId.value = endpoint.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const newStatus = !endpoint.is_active
|
|
|
|
|
|
await updateEndpoint(endpoint.id, { is_active: newStatus })
|
|
|
|
|
|
success(newStatus ? '端点已启用' : '端点已停用')
|
|
|
|
|
|
emit('endpointUpdated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '操作失败'), '错误')
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
togglingEndpointId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 18:24:52 +08:00
|
|
|
|
// 删除端点 - 打开确认弹窗
|
|
|
|
|
|
function handleDeleteEndpoint(endpoint: ProviderEndpoint) {
|
|
|
|
|
|
endpointToDelete.value = endpoint
|
|
|
|
|
|
deleteConfirmOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确认删除端点
|
|
|
|
|
|
async function confirmDeleteEndpoint() {
|
|
|
|
|
|
if (!endpointToDelete.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const endpoint = endpointToDelete.value
|
|
|
|
|
|
deleteConfirmOpen.value = false
|
2026-01-10 18:43:53 +08:00
|
|
|
|
deletingEndpointId.value = endpoint.id
|
2026-01-16 18:24:52 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await deleteEndpoint(endpoint.id)
|
2026-02-19 14:51:08 +08:00
|
|
|
|
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
emit('endpointUpdated')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '删除失败'), '错误')
|
2026-01-10 18:43:53 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
deletingEndpointId.value = null
|
2026-01-16 18:24:52 +08:00
|
|
|
|
endpointToDelete.value = null
|
2026-01-10 18:43:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭对话框
|
|
|
|
|
|
function handleDialogUpdate(value: boolean) {
|
|
|
|
|
|
emit('update:modelValue', value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleClose() {
|
|
|
|
|
|
emit('update:modelValue', false)
|
2025-12-18 16:14:37 +08:00
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</script>
|