mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
feat: 手动代理节点支持、系统默认代理与跨格式流式 usage 提取
代理节点: - 支持手动添加代理节点(HTTP/HTTPS/SOCKS5),含地址、认证信息和区域标签 - 新增手动节点的 CRUD API 和前端管理界面 - 提供商代理配置从 URL 字符串迁移至代理节点选择器(proxy_node_id) - 新增系统默认代理节点设置,未单独配置代理的提供商自动回退使用 - 删除节点时自动清除系统默认代理引用并失效缓存 - 健康检查跳过手动节点(无心跳,始终在线) 流式处理: - CLI handler 跨格式转换时委托基类解析 Provider 原始事件的 usage - StreamProcessor 新增 _extract_usage_from_converted_event 从转换后事件补充提取 usage - 支持 Claude/OpenAI/OpenAI Responses/Gemini 多种 usage 格式
This commit is contained in:
@@ -53,7 +53,7 @@
|
||||
v-for="(group, groupIndex) in fieldGroups"
|
||||
:key="groupIndex"
|
||||
>
|
||||
<!-- 可折叠的分组(如代理配置) -->
|
||||
<!-- 可折叠的分组(代理配置 - 代理节点选择) -->
|
||||
<div
|
||||
v-if="group.collapsible && group.hasToggle && group.toggleKey"
|
||||
class="space-y-2"
|
||||
@@ -65,50 +65,22 @@
|
||||
<span class="text-xs text-muted-foreground">启用代理</span>
|
||||
<Switch
|
||||
:model-value="formData[group.toggleKey] || false"
|
||||
@update:model-value="formData[group.toggleKey] = $event"
|
||||
@update:model-value="handleProxyToggle(group.toggleKey, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开内容(卡片) -->
|
||||
<!-- 展开内容(卡片)- 代理节点选择 -->
|
||||
<div
|
||||
v-if="formData[group.toggleKey]"
|
||||
class="rounded-lg border border-border bg-muted/30 px-4 py-3"
|
||||
>
|
||||
<!-- 横向排列的字段:代理地址占更多空间 -->
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
v-for="(field, fieldIndex) in group.fields"
|
||||
:key="field.key"
|
||||
class="space-y-1"
|
||||
:class="fieldIndex === 0 ? 'flex-[2]' : 'flex-1'"
|
||||
>
|
||||
<Label class="text-xs text-muted-foreground">
|
||||
{{ field.label }}
|
||||
</Label>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<Input
|
||||
v-if="field.type === 'text'"
|
||||
v-model="formData[field.key]"
|
||||
:placeholder="field.sensitive ? (sensitivePlaceholders[field.key] || field.placeholder) : field.placeholder"
|
||||
:masked="field.sensitive"
|
||||
disable-autofill
|
||||
class="h-8 text-sm"
|
||||
@update:model-value="handleFieldChange(field.key, $event)"
|
||||
/>
|
||||
|
||||
<!-- 密码/敏感输入 -->
|
||||
<Input
|
||||
v-else-if="field.type === 'password'"
|
||||
v-model="formData[field.key]"
|
||||
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
||||
masked
|
||||
class="h-8 text-sm"
|
||||
@update:model-value="handleFieldChange(field.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ProxyNodeSelect
|
||||
ref="proxyNodeSelectRef"
|
||||
:model-value="formData.proxy_node_id || ''"
|
||||
trigger-class="h-8"
|
||||
@update:model-value="(v: string) => { formData.proxy_node_id = v; handleFieldChange('proxy_node_id', v) }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,6 +276,8 @@ import {
|
||||
type AuthTemplate,
|
||||
type AuthTemplateFieldGroup,
|
||||
} from '../auth-templates'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -322,6 +296,16 @@ const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cooki
|
||||
|
||||
const { success: showSuccess, error: showError } = useToast()
|
||||
const { confirmDanger } = useConfirm()
|
||||
const proxyNodeSelectRef = ref<InstanceType<typeof ProxyNodeSelect> | null>(null)
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
|
||||
/** 启用代理时加载节点列表(直接调用 store,避免 ref 未挂载时静默失败) */
|
||||
function handleProxyToggle(toggleKey: string, value: boolean) {
|
||||
formData.value[toggleKey] = value
|
||||
if (value) {
|
||||
proxyNodesStore.ensureLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
const isSaving = ref(false)
|
||||
|
||||
@@ -238,47 +238,20 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
:model-value="form.proxy_enabled"
|
||||
@update:model-value="(v: boolean) => form.proxy_enabled = v"
|
||||
@update:model-value="handleProxyToggle"
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">启用代理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.proxy_enabled"
|
||||
class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50"
|
||||
class="space-y-1.5 p-3 border rounded-lg bg-muted/50"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">代理地址 *</Label>
|
||||
<Input
|
||||
v-model="form.proxy_url"
|
||||
placeholder="http://proxy:port 或 socks5://proxy:port"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">用户名</Label>
|
||||
<Input
|
||||
v-model="form.proxy_username"
|
||||
placeholder="可选"
|
||||
autocomplete="off"
|
||||
data-form-type="other"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">密码</Label>
|
||||
<Input
|
||||
v-model="form.proxy_password"
|
||||
type="password"
|
||||
placeholder="可选"
|
||||
autocomplete="new-password"
|
||||
data-form-type="other"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Label class="text-xs">代理节点 *</Label>
|
||||
<ProxyNodeSelect
|
||||
ref="proxyNodeSelectRef"
|
||||
v-model="form.proxy_node_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -322,6 +295,8 @@ import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { createProvider, updateProvider, type ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -337,6 +312,16 @@ const emit = defineEmits<{
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const loading = ref(false)
|
||||
const proxyNodeSelectRef = ref<InstanceType<typeof ProxyNodeSelect> | null>(null)
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
|
||||
/** 启用代理时懒加载节点列表 */
|
||||
function handleProxyToggle(v: boolean) {
|
||||
form.value.proxy_enabled = v
|
||||
if (v) {
|
||||
proxyNodesStore.ensureLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
// 内部状态
|
||||
const internalOpen = computed(() => props.modelValue)
|
||||
@@ -372,11 +357,9 @@ const form = ref({
|
||||
// 超时配置(秒)
|
||||
stream_first_byte_timeout: undefined as number | undefined,
|
||||
request_timeout: undefined as number | undefined,
|
||||
// 代理配置(扁平化便于表单绑定)
|
||||
// 代理配置
|
||||
proxy_enabled: false,
|
||||
proxy_url: '',
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
proxy_node_id: '',
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
@@ -403,9 +386,7 @@ function resetForm() {
|
||||
request_timeout: undefined,
|
||||
// 代理配置
|
||||
proxy_enabled: false,
|
||||
proxy_url: '',
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
proxy_node_id: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,9 +419,12 @@ function loadProviderData() {
|
||||
request_timeout: props.provider.request_timeout ?? undefined,
|
||||
// 代理配置
|
||||
proxy_enabled: proxy?.enabled ?? false,
|
||||
proxy_url: proxy?.url || '',
|
||||
proxy_username: proxy?.username || '',
|
||||
proxy_password: proxy?.password || '',
|
||||
proxy_node_id: proxy?.node_id || '',
|
||||
}
|
||||
|
||||
// 如果有代理配置,确保加载节点列表(直接调用 store,避免 ref 未挂载时静默失败)
|
||||
if (proxy?.enabled) {
|
||||
proxyNodesStore.ensureLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,19 +446,17 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 启用代理时必须填写代理地址
|
||||
if (form.value.proxy_enabled && !form.value.proxy_url) {
|
||||
showError('启用代理时必须填写代理地址', '验证失败')
|
||||
// 启用代理时的验证
|
||||
if (form.value.proxy_enabled && !form.value.proxy_node_id) {
|
||||
showError('请选择代理节点', '验证失败')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 构建代理配置
|
||||
const proxy = form.value.proxy_enabled ? {
|
||||
url: form.value.proxy_url,
|
||||
username: form.value.proxy_username || undefined,
|
||||
password: form.value.proxy_password || undefined,
|
||||
const proxy = form.value.proxy_enabled && form.value.proxy_node_id ? {
|
||||
node_id: form.value.proxy_node_id,
|
||||
enabled: true,
|
||||
} : null
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<Select
|
||||
:model-value="modelValue"
|
||||
:disabled="proxyNodesStore.loading || nodeOptions.length === 0"
|
||||
@update:model-value="(v: string) => $emit('update:modelValue', v)"
|
||||
>
|
||||
<SelectTrigger :class="triggerClass">
|
||||
<SelectValue
|
||||
:placeholder="proxyNodesStore.loading
|
||||
? '加载节点列表中...'
|
||||
: nodeOptions.length === 0
|
||||
? '暂无可用节点'
|
||||
: '选择代理节点...'"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="node in nodeOptions"
|
||||
:key="node.id"
|
||||
:value="node.id"
|
||||
>
|
||||
{{ node.name }}{{ node.region ? ` · ${node.region}` : '' }} ({{ node.ip }}:{{ node.port }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p
|
||||
v-if="!proxyNodesStore.loading && nodeOptions.length === 0"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
暂无在线代理节点,请在「代理节点」页面添加
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '@/components/ui'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
triggerClass?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const proxyNodesStore = useProxyNodesStore()
|
||||
|
||||
/** 在线节点 + 保留当前已选节点(可能已离线) */
|
||||
const nodeOptions = computed(() => {
|
||||
const online = proxyNodesStore.onlineNodes
|
||||
if (props.modelValue) {
|
||||
const found = online.find(n => n.id === props.modelValue)
|
||||
if (!found) {
|
||||
const allNode = proxyNodesStore.nodes.find(n => n.id === props.modelValue)
|
||||
if (allNode) return [allNode, ...online]
|
||||
}
|
||||
}
|
||||
return online
|
||||
})
|
||||
|
||||
/** 供父组件调用:启用代理时懒加载节点列表 */
|
||||
function ensureLoaded() {
|
||||
proxyNodesStore.ensureLoaded()
|
||||
}
|
||||
|
||||
defineExpose({ ensureLoaded })
|
||||
</script>
|
||||
Reference in New Issue
Block a user