feat: 支持固定类型 Provider OAuth 授权

- 新增 provider_type 字段区分自定义/预置 Provider 类型(claude_code/codex/gemini_cli/antigravity)
- 实现完整 OAuth 2.0 授权流程:start(生成授权 URL + PKCE)、complete(换取 token)、refresh
- 前端 KeyFormDialog 添加 OAuth 授权 UI,支持开始授权、粘贴回调 URL、完成授权、强制刷新
- 请求时自动检测 token 过期并刷新(120s 预留窗口 + Redis 分布式锁防并发)
- 固定类型 Provider 自动创建预置端点并锁定 base_url/custom_path
- 数据库迁移:添加 providers.provider_type,扩展 api_key 列为 TEXT
- 可选依赖 tls-client 用于 Claude token 请求的 TLS 指纹伪装
This commit is contained in:
AAEE86
2026-02-04 10:24:25 +08:00
parent f6dac1c38a
commit e4fdc65e52
25 changed files with 1818 additions and 36 deletions
@@ -86,6 +86,7 @@
<Input
:model-value="getEndpointEditState(endpoint.id)?.url ?? endpoint.base_url"
:placeholder="provider?.website || 'https://api.example.com'"
:disabled="isFixedProvider"
@update:model-value="(v) => updateEndpointField(endpoint.id, 'url', v)"
/>
</div>
@@ -94,13 +95,14 @@
<Input
:model-value="getEndpointEditState(endpoint.id)?.path ?? (endpoint.custom_path || '')"
:placeholder="getDefaultPath(endpoint.api_format) || '留空使用默认'"
:disabled="isFixedProvider"
@update:model-value="(v) => updateEndpointField(endpoint.id, 'path', v)"
/>
</div>
</div>
<!-- 保存/撤销按钮(URL/路径有修改时显示) -->
<div
v-if="hasUrlChanges(endpoint)"
v-if="!isFixedProvider && hasUrlChanges(endpoint)"
class="flex items-center gap-1 shrink-0"
>
<Button
@@ -371,8 +373,8 @@
<!-- 添加新端点 -->
<div
v-if="availableFormats.length > 0"
class="rounded-lg border border-dashed"
v-if="!isFixedProvider && availableFormats.length > 0"
class="rounded-lg border border-dashed p-3"
>
<!-- 卡片头部:API 格式选择 + 添加按钮 -->
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-dashed">
@@ -437,6 +439,13 @@
>
<p>所有 API 格式都已配置</p>
</div>
<div
v-else-if="isFixedProvider"
class="text-center py-6 text-xs text-muted-foreground"
>
固定类型 Provider 的端点已锁定并由系统自动维护。
</div>
</div>
<template #footer>
@@ -638,6 +647,11 @@ const RESERVED_BODY_FIELDS = new Set([
// 内部状态
const internalOpen = computed(() => props.modelValue)
const isFixedProvider = computed(() => {
const t = props.provider?.provider_type
return !!t && t !== 'custom'
})
// 新端点表单
const newEndpoint = ref({
api_format: '',