feat: support upstream token refresh
This commit is contained in:
@@ -461,6 +461,8 @@ export interface AuthCaptureCandidate {
|
||||
cookie_names?: string[]
|
||||
new_api_user?: string
|
||||
user_id?: string
|
||||
refresh_token?: string
|
||||
refresh_token_preview?: string
|
||||
}
|
||||
|
||||
export interface AuthCaptureResult {
|
||||
|
||||
@@ -112,7 +112,18 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
(e: 'select', candidate: { type: string; value: string; source: string; cookie_name?: string; cookie_value?: string; cookie_count?: number; cookie_names?: string[]; new_api_user?: string; user_id?: string }): void
|
||||
(e: 'select', candidate: {
|
||||
type: string
|
||||
value: string
|
||||
source: string
|
||||
cookie_name?: string
|
||||
cookie_value?: string
|
||||
cookie_count?: number
|
||||
cookie_names?: string[]
|
||||
new_api_user?: string
|
||||
user_id?: string
|
||||
refresh_token?: string
|
||||
}): void
|
||||
}>()
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
@@ -330,6 +341,7 @@ async function confirmImportSelection() {
|
||||
cookie_names: fullCandidate.cookie_names,
|
||||
user_id: resolveNewApiUser(rawResult.data.result, fullCandidate),
|
||||
new_api_user: resolveNewApiUser(rawResult.data.result, fullCandidate),
|
||||
refresh_token: fullCandidate.refresh_token,
|
||||
})
|
||||
closeDialog()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -129,12 +129,14 @@
|
||||
<el-input v-model="form.base_url" placeholder="https://example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Prefix">
|
||||
<el-input v-model="form.api_prefix" placeholder="/api/v1" />
|
||||
<el-input v-model="form.api_prefix" :placeholder="apiPrefixPlaceholder" />
|
||||
<div v-if="apiPrefixHint" class="form-hint">{{ apiPrefixHint }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="认证方式">
|
||||
<el-select v-model="form.auth_type" style="width: 100%">
|
||||
<el-select v-model="form.auth_type" style="width: 100%" @change="handleAuthTypeChange">
|
||||
<el-option label="无认证" value="none" />
|
||||
<el-option label="Bearer Token" value="bearer" />
|
||||
<el-option label="New-API Access Token" value="new_api_token" />
|
||||
<el-option label="Nox Access Token" value="nox_token" />
|
||||
<el-option label="Cookie" value="cookie" />
|
||||
<el-option label="API Key" value="api_key" />
|
||||
@@ -151,6 +153,25 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.api_prefix?.replace(/^\/|\/$/g, '') === 'api/v1'" label="Refresh Token">
|
||||
<el-input v-model="form.auth_config.refresh_token" type="password" show-password placeholder="可选,真实浏览器提取后自动填入" />
|
||||
<div class="form-hint">没有 refresh token 时,access token 过期后需要重新提取或改用邮箱密码登录。</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'new_api_token'">
|
||||
<el-form-item label="New-API Access Token">
|
||||
<div class="auth-field-row">
|
||||
<el-input v-model="form.auth_config.token" type="password" show-password placeholder="上游 /api/user/token 生成的 access token" />
|
||||
<el-button size="small" @click="openAuthCapture">
|
||||
<el-icon><Pointer /></el-icon>
|
||||
提取
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="New-API User ID">
|
||||
<el-input v-model="form.auth_config.user_id" placeholder="例如 1" />
|
||||
<div class="form-hint">请求 <code>/api/user/self/groups</code> 时需要同时发送 <code>New-Api-User</code>。不要反复点上游「重新生成」,<code>/api/user/token</code> 会覆盖旧 access token。</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'nox_token'">
|
||||
<el-form-item label="Nox Access Token">
|
||||
@@ -177,6 +198,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="form-hint">Cookie 仅作为 fallback;它受 session、Cloudflare 校验和上游重启影响,通常不如 access token 稳定。</div>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'api_key'">
|
||||
<el-form-item label="API Key">
|
||||
@@ -187,6 +209,12 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'login_password'">
|
||||
<el-form-item label="账号字段">
|
||||
<el-select v-model="form.auth_config.username_field" style="width: 100%">
|
||||
<el-option label="邮箱 email" value="email" />
|
||||
<el-option label="用户名 username" value="username" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="form.auth_config.username_field === 'username' ? '登录账号' : '登录邮箱'">
|
||||
<el-input v-model="form.auth_config.email" :placeholder="form.auth_config.username_field === 'username' ? 'admin' : 'admin@example.com'" />
|
||||
</el-form-item>
|
||||
@@ -194,20 +222,20 @@
|
||||
<el-input v-model="form.auth_config.password" type="password" show-password placeholder="***" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录接口路径">
|
||||
<el-input v-model="form.auth_config.login_path" placeholder="/auth/login" />
|
||||
<el-input v-model="form.auth_config.login_path" :placeholder="loginPathPlaceholder" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="分组接口">
|
||||
<el-input v-model="form.groups_endpoint" placeholder="/groups/available" />
|
||||
<el-input v-model="form.groups_endpoint" :placeholder="groupsEndpointPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="倍率接口">
|
||||
<el-input v-model="form.rate_endpoint" placeholder="/groups/rates" />
|
||||
<el-input v-model="form.rate_endpoint" :placeholder="rateEndpointPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="余额接口">
|
||||
<el-input v-model="form.balance_endpoint" placeholder="留空则不获取余额,如 /auth/me" />
|
||||
<el-input v-model="form.balance_endpoint" :placeholder="balanceEndpointPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="余额字段路径">
|
||||
<el-input v-model="form.balance_response_path" placeholder="如 balance、data.quota" />
|
||||
<el-input v-model="form.balance_response_path" :placeholder="balanceResponsePathPlaceholder" />
|
||||
<div class="form-hint">JSON 点分路径,例如 <code>balance</code> 或 <code>data.quota</code></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="余额除数">
|
||||
@@ -446,20 +474,80 @@ const saving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
type QuickPlatform = 'sub2api' | 'new-api-user' | 'nox-api' | 'custom'
|
||||
type KnownQuickPlatform = Exclude<QuickPlatform, 'custom'>
|
||||
type AuthType = 'none' | 'bearer' | 'new_api_token' | 'nox_token' | 'cookie' | 'api_key' | 'login_password'
|
||||
|
||||
const USER_API_GROUPS_ENDPOINT = '/api/user/self/groups'
|
||||
const USER_API_BALANCE_ENDPOINT = '/api/user/self'
|
||||
const USER_API_LOGIN_PATH = '/api/user/login'
|
||||
const SUB2API_LOGIN_PATH = '/auth/login'
|
||||
|
||||
const platformDefaults: Record<KnownQuickPlatform, {
|
||||
api_prefix: string
|
||||
auth_type: AuthType
|
||||
auth_config: Record<string, any>
|
||||
rate_endpoint: string
|
||||
groups_endpoint: string
|
||||
balance_endpoint: string
|
||||
balance_response_path: string
|
||||
balance_divisor: number
|
||||
}> = {
|
||||
sub2api: {
|
||||
api_prefix: '/api/v1',
|
||||
auth_type: 'login_password',
|
||||
auth_config: { email: '', password: '', login_path: SUB2API_LOGIN_PATH, username_field: 'email' },
|
||||
rate_endpoint: '/groups/rates',
|
||||
groups_endpoint: '/groups/available',
|
||||
balance_endpoint: '/auth/me',
|
||||
balance_response_path: 'data.balance',
|
||||
balance_divisor: 1.0,
|
||||
},
|
||||
'new-api-user': {
|
||||
api_prefix: '',
|
||||
auth_type: 'login_password',
|
||||
auth_config: { email: '', password: '', login_path: USER_API_LOGIN_PATH, username_field: 'username', provider: 'new-api' },
|
||||
rate_endpoint: USER_API_GROUPS_ENDPOINT,
|
||||
groups_endpoint: USER_API_GROUPS_ENDPOINT,
|
||||
balance_endpoint: USER_API_BALANCE_ENDPOINT,
|
||||
balance_response_path: 'data.quota',
|
||||
balance_divisor: 500000,
|
||||
},
|
||||
'nox-api': {
|
||||
api_prefix: '',
|
||||
auth_type: 'login_password',
|
||||
auth_config: { email: '', password: '', login_path: USER_API_LOGIN_PATH, username_field: 'username', provider: 'nox-api' },
|
||||
rate_endpoint: USER_API_GROUPS_ENDPOINT,
|
||||
groups_endpoint: USER_API_GROUPS_ENDPOINT,
|
||||
balance_endpoint: USER_API_BALANCE_ENDPOINT,
|
||||
balance_response_path: 'data.quota',
|
||||
balance_divisor: 500000,
|
||||
},
|
||||
}
|
||||
|
||||
const platformLabels: Record<QuickPlatform, string> = {
|
||||
sub2api: 'Sub2API',
|
||||
'new-api-user': 'New-API',
|
||||
'nox-api': 'Nox-API',
|
||||
custom: '自定义',
|
||||
}
|
||||
|
||||
const quickPlatform = ref<QuickPlatform>('sub2api')
|
||||
|
||||
const defaultForm = () => ({
|
||||
name: '',
|
||||
base_url: '',
|
||||
api_prefix: '/api/v1',
|
||||
auth_type: 'login_password',
|
||||
auth_config: { email: '', password: '', login_path: '/auth/login' } as Record<string, any>,
|
||||
rate_endpoint: '/groups/rates',
|
||||
groups_endpoint: '/groups/available',
|
||||
api_prefix: platformDefaults.sub2api.api_prefix,
|
||||
auth_type: platformDefaults.sub2api.auth_type,
|
||||
auth_config: { ...platformDefaults.sub2api.auth_config } as Record<string, any>,
|
||||
rate_endpoint: platformDefaults.sub2api.rate_endpoint,
|
||||
groups_endpoint: platformDefaults.sub2api.groups_endpoint,
|
||||
enabled: true,
|
||||
check_interval_seconds: 600,
|
||||
timeout_seconds: 30,
|
||||
balance_endpoint: '/auth/me',
|
||||
balance_response_path: 'data.balance',
|
||||
balance_divisor: 1.0,
|
||||
balance_endpoint: platformDefaults.sub2api.balance_endpoint,
|
||||
balance_response_path: platformDefaults.sub2api.balance_response_path,
|
||||
balance_divisor: platformDefaults.sub2api.balance_divisor,
|
||||
balance_alert_threshold: null as number | null,
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
@@ -470,6 +558,42 @@ const rules = {
|
||||
|
||||
const authCaptureVisible = ref(false)
|
||||
|
||||
const selectedPlatformDefaults = computed(() =>
|
||||
quickPlatform.value === 'custom' ? null : platformDefaults[quickPlatform.value],
|
||||
)
|
||||
|
||||
const apiPrefixPlaceholder = computed(() => {
|
||||
const defaults = selectedPlatformDefaults.value
|
||||
if (!defaults) return '/api/v1 或留空'
|
||||
return defaults.api_prefix || `留空(${platformLabels[quickPlatform.value]} 默认)`
|
||||
})
|
||||
|
||||
const apiPrefixHint = computed(() => {
|
||||
if (quickPlatform.value !== 'new-api-user' && quickPlatform.value !== 'nox-api') return ''
|
||||
return `${platformLabels[quickPlatform.value]} 用户接口默认不加 API Prefix,留空即可。`
|
||||
})
|
||||
|
||||
const loginPathPlaceholder = computed(() => {
|
||||
const defaults = selectedPlatformDefaults.value
|
||||
return defaults?.auth_config.login_path || SUB2API_LOGIN_PATH
|
||||
})
|
||||
|
||||
const groupsEndpointPlaceholder = computed(() =>
|
||||
selectedPlatformDefaults.value?.groups_endpoint || '/groups/available',
|
||||
)
|
||||
|
||||
const rateEndpointPlaceholder = computed(() =>
|
||||
selectedPlatformDefaults.value?.rate_endpoint || '/groups/rates',
|
||||
)
|
||||
|
||||
const balanceEndpointPlaceholder = computed(() =>
|
||||
selectedPlatformDefaults.value?.balance_endpoint || '留空则不获取余额,如 /auth/me',
|
||||
)
|
||||
|
||||
const balanceResponsePathPlaceholder = computed(() =>
|
||||
selectedPlatformDefaults.value?.balance_response_path || '如 balance、data.quota',
|
||||
)
|
||||
|
||||
const authCaptureInitialUrl = computed(() => {
|
||||
const base = (form.value.base_url || '').replace(/\/+$/, '')
|
||||
if (!base) return ''
|
||||
@@ -478,7 +602,7 @@ const authCaptureInitialUrl = computed(() => {
|
||||
|
||||
const authCapturePreferredTypes = computed<AuthCaptureCandidate['type'][]>(() => {
|
||||
if (quickPlatform.value === 'sub2api') return ['bearer_token', 'api_key', 'cookie_bundle', 'cookie']
|
||||
if (quickPlatform.value === 'new-api-user') return ['cookie_bundle', 'cookie', 'bearer_token', 'api_key']
|
||||
if (quickPlatform.value === 'new-api-user') return ['bearer_token', 'cookie_bundle', 'cookie', 'api_key']
|
||||
if (quickPlatform.value === 'nox-api') return ['cookie_bundle', 'cookie', 'bearer_token', 'api_key']
|
||||
return ['bearer_token', 'api_key', 'cookie_bundle', 'cookie']
|
||||
})
|
||||
@@ -496,27 +620,47 @@ function handleAuthCaptureSelect(candidate: {
|
||||
cookie_names?: string[]
|
||||
new_api_user?: string
|
||||
user_id?: string
|
||||
refresh_token?: string
|
||||
}) {
|
||||
if (candidate.type === 'bearer_token') {
|
||||
if (quickPlatform.value === 'nox-api') {
|
||||
form.value.auth_type = 'nox_token'
|
||||
form.value.auth_config.token = candidate.value
|
||||
form.value.auth_config.provider = 'nox-api'
|
||||
applyUserApiEndpoints()
|
||||
if (candidate.user_id || candidate.new_api_user) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
}
|
||||
ElMessage.success('已填入 Nox Access Token')
|
||||
} else if (quickPlatform.value === 'new-api-user') {
|
||||
form.value.auth_type = 'new_api_token'
|
||||
form.value.auth_config.token = candidate.value
|
||||
form.value.auth_config.provider = 'new-api'
|
||||
if (candidate.user_id || candidate.new_api_user) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
form.value.auth_config.new_api_user = candidate.new_api_user || candidate.user_id
|
||||
}
|
||||
applyUserApiEndpoints()
|
||||
ElMessage.success('已填入 New-API Access Token')
|
||||
} else {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
ElMessage.success('已填入 Bearer Token')
|
||||
if (candidate.refresh_token) {
|
||||
form.value.auth_config.refresh_token = candidate.refresh_token
|
||||
ElMessage.success('已填入 Bearer Token 和 Refresh Token')
|
||||
} else {
|
||||
ElMessage.success('已填入 Bearer Token')
|
||||
if (quickPlatform.value === 'sub2api' || form.value.api_prefix?.replace(/^\/|\/$/g, '') === 'api/v1') {
|
||||
ElMessage.warning('未提取到 Refresh Token,过期后需要重新提取或改用邮箱密码登录')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (candidate.type === 'cookie_bundle') {
|
||||
// 完整 cookie 组:value 已是完整 "name1=v1; name2=v2" 字符串
|
||||
form.value.auth_type = 'cookie'
|
||||
form.value.auth_config.cookie_string = candidate.value
|
||||
if (quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') {
|
||||
form.value.auth_config.provider = quickPlatform.value
|
||||
form.value.auth_config.provider = quickPlatform.value === 'nox-api' ? 'nox-api' : 'new-api'
|
||||
}
|
||||
if (quickPlatform.value === 'sub2api') {
|
||||
ElMessage.warning('Sub2API 通常需要 Bearer Token;Cookie 只能在确认上游支持 Cookie 鉴权时使用')
|
||||
@@ -524,13 +668,9 @@ function handleAuthCaptureSelect(candidate: {
|
||||
if (candidate.new_api_user) {
|
||||
form.value.auth_config.new_api_user = candidate.new_api_user
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
applyUserApiEndpoints()
|
||||
} else if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
applyUserApiEndpoints()
|
||||
if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
ElMessage.warning(`已填入完整 Cookie 组,但未提取到 ${quickPlatform.value === 'nox-api' ? 'Nox-Api-User' : 'New-Api-User'},请重新登录后再提取`)
|
||||
}
|
||||
@@ -543,7 +683,7 @@ function handleAuthCaptureSelect(candidate: {
|
||||
? `${candidate.cookie_name}=${candidate.cookie_value}`
|
||||
: candidate.value
|
||||
if (quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') {
|
||||
form.value.auth_config.provider = quickPlatform.value
|
||||
form.value.auth_config.provider = quickPlatform.value === 'nox-api' ? 'nox-api' : 'new-api'
|
||||
}
|
||||
if (quickPlatform.value === 'sub2api') {
|
||||
ElMessage.warning('Sub2API 通常需要 Bearer Token;Cookie 只能在确认上游支持 Cookie 鉴权时使用')
|
||||
@@ -551,13 +691,9 @@ function handleAuthCaptureSelect(candidate: {
|
||||
if (candidate.new_api_user) {
|
||||
form.value.auth_config.new_api_user = candidate.new_api_user
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
applyUserApiEndpoints()
|
||||
} else if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
applyUserApiEndpoints()
|
||||
if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
ElMessage.warning(`已填入 Cookie,但未提取到 ${quickPlatform.value === 'nox-api' ? 'Nox-Api-User' : 'New-Api-User'},请重新登录后再提取`)
|
||||
}
|
||||
@@ -571,71 +707,152 @@ function handleAuthCaptureSelect(candidate: {
|
||||
} else if (candidate.type === 'credential') {
|
||||
// Try to guess — if value starts with 'sk-', treat as bearer
|
||||
if (candidate.value.startsWith('sk-')) {
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_token' : 'bearer'
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_token' : quickPlatform.value === 'new-api-user' ? 'new_api_token' : 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
if (quickPlatform.value === 'nox-api') form.value.auth_config.provider = 'nox-api'
|
||||
if (quickPlatform.value === 'nox-api' && (candidate.user_id || candidate.new_api_user)) {
|
||||
if (quickPlatform.value === 'new-api-user') form.value.auth_config.provider = 'new-api'
|
||||
if (quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') applyUserApiEndpoints()
|
||||
if ((quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') && (candidate.user_id || candidate.new_api_user)) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
if (quickPlatform.value === 'new-api-user') form.value.auth_config.new_api_user = candidate.new_api_user || candidate.user_id
|
||||
}
|
||||
ElMessage.success('已填入 Bearer Token (sk-key)')
|
||||
ElMessage.success(quickPlatform.value === 'new-api-user' ? '已填入 New-API Access Token' : '已填入 Bearer Token (sk-key)')
|
||||
} else {
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_token' : 'bearer'
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_token' : quickPlatform.value === 'new-api-user' ? 'new_api_token' : 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
if (quickPlatform.value === 'nox-api') form.value.auth_config.provider = 'nox-api'
|
||||
if (quickPlatform.value === 'nox-api' && (candidate.user_id || candidate.new_api_user)) {
|
||||
if (quickPlatform.value === 'new-api-user') form.value.auth_config.provider = 'new-api'
|
||||
if (quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') applyUserApiEndpoints()
|
||||
if ((quickPlatform.value === 'nox-api' || quickPlatform.value === 'new-api-user') && (candidate.user_id || candidate.new_api_user)) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
if (quickPlatform.value === 'new-api-user') form.value.auth_config.new_api_user = candidate.new_api_user || candidate.user_id
|
||||
}
|
||||
ElMessage.success('已填入认证信息')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const quickPlatform = ref('sub2api')
|
||||
function isKnownQuickPlatform(value: string): value is KnownQuickPlatform {
|
||||
return value === 'sub2api' || value === 'new-api-user' || value === 'nox-api'
|
||||
}
|
||||
|
||||
function inferPlatform(row: Pick<UpstreamData, 'api_prefix' | 'groups_endpoint' | 'auth_type' | 'auth_config_masked'>): string {
|
||||
function normalizeQuickPlatform(value: string): QuickPlatform {
|
||||
return isKnownQuickPlatform(value) ? value : 'custom'
|
||||
}
|
||||
|
||||
function platformProvider(platform: QuickPlatform): string {
|
||||
if (platform === 'nox-api') return 'nox-api'
|
||||
if (platform === 'new-api-user') return 'new-api'
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeAuthType(value: string): AuthType {
|
||||
if (value === 'none'
|
||||
|| value === 'bearer'
|
||||
|| value === 'new_api_token'
|
||||
|| value === 'nox_token'
|
||||
|| value === 'cookie'
|
||||
|| value === 'api_key'
|
||||
|| value === 'login_password') {
|
||||
return value
|
||||
}
|
||||
return 'none'
|
||||
}
|
||||
|
||||
function applyUserApiEndpoints() {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = USER_API_GROUPS_ENDPOINT
|
||||
form.value.rate_endpoint = USER_API_GROUPS_ENDPOINT
|
||||
}
|
||||
|
||||
function inferPlatform(row: Pick<UpstreamData, 'api_prefix' | 'groups_endpoint' | 'auth_type' | 'auth_config_masked'>): QuickPlatform {
|
||||
if (row.api_prefix?.replace(/^\/|\/$/g, '') === 'api/v1') return 'sub2api'
|
||||
if (row.api_prefix === '' && row.groups_endpoint === '/api/user/self/groups') {
|
||||
if (row.api_prefix === '' && row.groups_endpoint === USER_API_GROUPS_ENDPOINT) {
|
||||
if (row.auth_type === 'nox_token' || row.auth_config_masked?.provider === 'nox-api') return 'nox-api'
|
||||
if (row.auth_type === 'new_api_token' || row.auth_config_masked?.provider === 'new-api') return 'new-api-user'
|
||||
return 'new-api-user'
|
||||
}
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
function handlePlatformChange(val: string) {
|
||||
if (val === 'sub2api') {
|
||||
form.value.api_prefix = '/api/v1'
|
||||
form.value.groups_endpoint = '/groups/available'
|
||||
form.value.rate_endpoint = '/groups/rates'
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.login_path = '/auth/login'
|
||||
form.value.auth_config.username_field = 'email'
|
||||
form.value.balance_endpoint = '/auth/me'
|
||||
form.value.balance_response_path = 'data.balance'
|
||||
form.value.balance_divisor = 1.0
|
||||
} else if (val === 'new-api-user') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.provider = 'new-api-user'
|
||||
form.value.auth_config.login_path = '/api/user/login'
|
||||
form.value.auth_config.username_field = 'username'
|
||||
form.value.balance_endpoint = '/api/user/self'
|
||||
form.value.balance_response_path = 'data.quota'
|
||||
form.value.balance_divisor = 500000
|
||||
} else if (val === 'nox-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
form.value.auth_type = 'nox_token'
|
||||
form.value.auth_config = { token: '', user_id: '', provider: 'nox-api' }
|
||||
form.value.balance_endpoint = '/api/user/self'
|
||||
form.value.balance_response_path = 'data.quota'
|
||||
form.value.balance_divisor = 500000
|
||||
} else {
|
||||
const platform = normalizeQuickPlatform(val)
|
||||
if (platform === 'custom') {
|
||||
form.value.balance_endpoint = ''
|
||||
form.value.balance_response_path = ''
|
||||
form.value.balance_divisor = 1.0
|
||||
return
|
||||
}
|
||||
|
||||
const defaults = platformDefaults[platform]
|
||||
form.value.api_prefix = defaults.api_prefix
|
||||
form.value.groups_endpoint = defaults.groups_endpoint
|
||||
form.value.rate_endpoint = defaults.rate_endpoint
|
||||
form.value.auth_type = defaults.auth_type
|
||||
form.value.auth_config = { ...defaults.auth_config }
|
||||
form.value.balance_endpoint = defaults.balance_endpoint
|
||||
form.value.balance_response_path = defaults.balance_response_path
|
||||
form.value.balance_divisor = defaults.balance_divisor
|
||||
}
|
||||
|
||||
function isUserApiPasswordLoginPlatform(): boolean {
|
||||
return quickPlatform.value === 'new-api-user'
|
||||
|| quickPlatform.value === 'nox-api'
|
||||
|| (form.value.api_prefix === '' && form.value.groups_endpoint === USER_API_GROUPS_ENDPOINT)
|
||||
}
|
||||
|
||||
function handleAuthTypeChange(val: string) {
|
||||
const provider = platformProvider(quickPlatform.value)
|
||||
if (val === 'none') {
|
||||
form.value.auth_config = {}
|
||||
return
|
||||
}
|
||||
if (val === 'bearer') {
|
||||
form.value.auth_config = {
|
||||
token: form.value.auth_config.token || '',
|
||||
refresh_token: form.value.auth_config.refresh_token || '',
|
||||
}
|
||||
return
|
||||
}
|
||||
if (val === 'new_api_token') {
|
||||
form.value.auth_config = {
|
||||
token: form.value.auth_config.token || '',
|
||||
user_id: form.value.auth_config.user_id || form.value.auth_config.new_api_user || '',
|
||||
provider: 'new-api',
|
||||
}
|
||||
return
|
||||
}
|
||||
if (val === 'nox_token') {
|
||||
form.value.auth_config = {
|
||||
token: form.value.auth_config.token || '',
|
||||
user_id: form.value.auth_config.user_id || form.value.auth_config.new_api_user || '',
|
||||
provider: 'nox-api',
|
||||
}
|
||||
return
|
||||
}
|
||||
if (val === 'cookie') {
|
||||
form.value.auth_config = {
|
||||
cookie_string: form.value.auth_config.cookie_string || '',
|
||||
...(provider ? { provider } : {}),
|
||||
}
|
||||
return
|
||||
}
|
||||
if (val === 'api_key') {
|
||||
form.value.auth_config = {
|
||||
key: form.value.auth_config.key || '',
|
||||
header: form.value.auth_config.header || 'Authorization',
|
||||
}
|
||||
return
|
||||
}
|
||||
if (val !== 'login_password') return
|
||||
|
||||
const userApiLogin = isUserApiPasswordLoginPlatform()
|
||||
form.value.auth_config = {
|
||||
email: form.value.auth_config.email || '',
|
||||
password: form.value.auth_config.password || '',
|
||||
login_path: userApiLogin ? USER_API_LOGIN_PATH : (form.value.auth_config.login_path || SUB2API_LOGIN_PATH),
|
||||
username_field: userApiLogin ? 'username' : (form.value.auth_config.username_field || 'email'),
|
||||
...(provider ? { provider } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,9 +902,10 @@ function usesTokenEndpointUpstream(row: UpstreamData | null) {
|
||||
if (!row) return false
|
||||
return row.api_prefix === ''
|
||||
&& (
|
||||
row.groups_endpoint === '/api/user/self/groups'
|
||||
|| row.auth_config_masked?.login_path === '/api/user/login'
|
||||
row.groups_endpoint === USER_API_GROUPS_ENDPOINT
|
||||
|| row.auth_config_masked?.login_path === USER_API_LOGIN_PATH
|
||||
|| Boolean(row.auth_config_masked?.new_api_user)
|
||||
|| row.auth_type === 'new_api_token'
|
||||
|| row.auth_type === 'nox_token'
|
||||
)
|
||||
}
|
||||
@@ -714,7 +932,7 @@ const recentChecks = computed(() =>
|
||||
)
|
||||
|
||||
const statusLabel = (s: string) => ({ healthy: '健康', unhealthy: '异常', unknown: '未知' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', nox_token: 'Nox Token', cookie: 'Cookie', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', new_api_token: 'New-API Token', nox_token: 'Nox Token', cookie: 'Cookie', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
|
||||
function formatBalance(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '—'
|
||||
@@ -765,7 +983,7 @@ function openEdit(row: UpstreamData) {
|
||||
name: row.name,
|
||||
base_url: row.base_url,
|
||||
api_prefix: row.api_prefix,
|
||||
auth_type: row.auth_type,
|
||||
auth_type: normalizeAuthType(row.auth_type),
|
||||
auth_config: { ...(row.auth_config_masked as Record<string, any>) },
|
||||
rate_endpoint: row.rate_endpoint,
|
||||
groups_endpoint: row.groups_endpoint,
|
||||
@@ -785,11 +1003,23 @@ async function handleSave() {
|
||||
if (!valid) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
...form.value,
|
||||
auth_config: { ...(form.value.auth_config as Record<string, any>) },
|
||||
}
|
||||
if (payload.auth_type === 'new_api_token') {
|
||||
payload.auth_config.provider = 'new-api'
|
||||
if (payload.auth_config.user_id && !payload.auth_config.new_api_user) {
|
||||
payload.auth_config.new_api_user = payload.auth_config.user_id
|
||||
}
|
||||
} else if (payload.auth_type === 'nox_token') {
|
||||
payload.auth_config.provider = 'nox-api'
|
||||
}
|
||||
if (editingId.value) {
|
||||
await upstreamsApi.update(editingId.value, form.value)
|
||||
await upstreamsApi.update(editingId.value, payload)
|
||||
ElMessage.success('保存成功')
|
||||
} else {
|
||||
await upstreamsApi.create(form.value as any)
|
||||
await upstreamsApi.create(payload as any)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
drawerVisible.value = false
|
||||
|
||||
Reference in New Issue
Block a user