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:
fawney19
2026-02-07 14:30:46 +08:00
parent 86dbbe83c2
commit db96c9a46e
30 changed files with 1182 additions and 243 deletions
@@ -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)