feat: add Nox-API upstream support
This commit is contained in:
@@ -460,6 +460,7 @@ export interface AuthCaptureCandidate {
|
||||
cookie_count?: number
|
||||
cookie_names?: string[]
|
||||
new_api_user?: string
|
||||
user_id?: string
|
||||
}
|
||||
|
||||
export interface AuthCaptureResult {
|
||||
|
||||
@@ -112,7 +112,7 @@ 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 }): 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
|
||||
}>()
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
@@ -170,6 +170,7 @@ function resolveCandidateValue(candidate: AuthCaptureCandidate): string {
|
||||
}
|
||||
|
||||
function resolveNewApiUser(rawResult: AuthCaptureResult, candidate: AuthCaptureCandidate): string | undefined {
|
||||
if (candidate.user_id) return candidate.user_id
|
||||
if (candidate.new_api_user) return candidate.new_api_user
|
||||
const stores = [rawResult.storage, rawResult.session_storage]
|
||||
for (const store of stores) {
|
||||
@@ -327,6 +328,7 @@ async function confirmImportSelection() {
|
||||
cookie_value: fullCandidate.cookie_value,
|
||||
cookie_count: fullCandidate.cookie_count,
|
||||
cookie_names: fullCandidate.cookie_names,
|
||||
user_id: resolveNewApiUser(rawResult.data.result, fullCandidate),
|
||||
new_api_user: resolveNewApiUser(rawResult.data.result, fullCandidate),
|
||||
})
|
||||
closeDialog()
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
<el-select v-model="quickPlatform" @change="handlePlatformChange" style="width: 100%">
|
||||
<el-option label="Sub2API" value="sub2api" />
|
||||
<el-option label="New-API" value="new-api-user" />
|
||||
<el-option label="Nox-API" value="nox-api" />
|
||||
<el-option label="自定义" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -134,6 +135,7 @@
|
||||
<el-select v-model="form.auth_type" style="width: 100%">
|
||||
<el-option label="无认证" value="none" />
|
||||
<el-option label="Bearer Token" value="bearer" />
|
||||
<el-option label="Nox Access Token" value="nox_token" />
|
||||
<el-option label="Cookie" value="cookie" />
|
||||
<el-option label="API Key" value="api_key" />
|
||||
<el-option label="邮箱密码登录" value="login_password" />
|
||||
@@ -150,6 +152,21 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'nox_token'">
|
||||
<el-form-item label="Nox Access Token">
|
||||
<div class="auth-field-row">
|
||||
<el-input v-model="form.auth_config.token" type="password" show-password placeholder="***" />
|
||||
<el-button size="small" @click="openAuthCapture">
|
||||
<el-icon><Pointer /></el-icon>
|
||||
提取
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Nox User ID">
|
||||
<el-input v-model="form.auth_config.user_id" placeholder="例如 1" />
|
||||
<div class="form-hint">请求 <code>/api/user/self/groups</code> 时需要同时发送 <code>Nox-Api-User</code></div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'cookie'">
|
||||
<el-form-item label="Cookie">
|
||||
<div class="auth-field-row">
|
||||
@@ -195,7 +212,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="余额除数">
|
||||
<el-input-number v-model="form.balance_divisor" :min="1" :max="999999999" style="width: 100%" />
|
||||
<div class="form-hint">原始值除以该数得到实际余额。New-API 填 <code>500000</code>,Sub2API 填 <code>1</code></div>
|
||||
<div class="form-hint">原始值除以该数得到实际余额。New-API / Nox-API 填 <code>500000</code>,Sub2API 填 <code>1</code></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="余额告警阈值">
|
||||
<el-input-number v-model="form.balance_alert_threshold" :min="0" :precision="2" :value-on-clear="null" style="width: 100%" />
|
||||
@@ -462,6 +479,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 === 'nox-api') return ['cookie_bundle', 'cookie', 'bearer_token', 'api_key']
|
||||
return ['bearer_token', 'api_key', 'cookie_bundle', 'cookie']
|
||||
})
|
||||
|
||||
@@ -477,28 +495,45 @@ function handleAuthCaptureSelect(candidate: {
|
||||
cookie_count?: number
|
||||
cookie_names?: string[]
|
||||
new_api_user?: string
|
||||
user_id?: string
|
||||
}) {
|
||||
if (candidate.type === 'bearer_token') {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
ElMessage.success('已填入 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'
|
||||
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 {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
ElMessage.success('已填入 Bearer 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
|
||||
}
|
||||
if (quickPlatform.value === 'sub2api') {
|
||||
ElMessage.warning('Sub2API 通常需要 Bearer Token;Cookie 只能在确认上游支持 Cookie 鉴权时使用')
|
||||
}
|
||||
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'
|
||||
} else if (quickPlatform.value === 'new-api-user') {
|
||||
} 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'
|
||||
ElMessage.warning('已填入完整 Cookie 组,但未提取到 New-Api-User,请重新登录后再提取')
|
||||
if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
ElMessage.warning(`已填入完整 Cookie 组,但未提取到 ${quickPlatform.value === 'nox-api' ? 'Nox-Api-User' : 'New-Api-User'},请重新登录后再提取`)
|
||||
}
|
||||
}
|
||||
const countLabel = candidate.cookie_count ? `(${candidate.cookie_count} 个 cookie)` : ''
|
||||
ElMessage.success(`已填入完整 Cookie 组${countLabel}`)
|
||||
@@ -507,19 +542,25 @@ function handleAuthCaptureSelect(candidate: {
|
||||
form.value.auth_config.cookie_string = candidate.cookie_name && candidate.cookie_value
|
||||
? `${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
|
||||
}
|
||||
if (quickPlatform.value === 'sub2api') {
|
||||
ElMessage.warning('Sub2API 通常需要 Bearer Token;Cookie 只能在确认上游支持 Cookie 鉴权时使用')
|
||||
}
|
||||
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'
|
||||
} else if (quickPlatform.value === 'new-api-user') {
|
||||
} 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'
|
||||
ElMessage.warning('已填入 Cookie,但未提取到 New-Api-User,请重新登录后再提取')
|
||||
if (quickPlatform.value === 'new-api-user' || quickPlatform.value === 'nox-api') {
|
||||
ElMessage.warning(`已填入 Cookie,但未提取到 ${quickPlatform.value === 'nox-api' ? 'Nox-Api-User' : 'New-Api-User'},请重新登录后再提取`)
|
||||
}
|
||||
}
|
||||
ElMessage.success('已填入 Cookie')
|
||||
} else if (candidate.type === 'api_key') {
|
||||
@@ -530,12 +571,20 @@ 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 = 'bearer'
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_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)) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
}
|
||||
ElMessage.success('已填入 Bearer Token (sk-key)')
|
||||
} else {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_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)) {
|
||||
form.value.auth_config.user_id = candidate.user_id || candidate.new_api_user
|
||||
}
|
||||
ElMessage.success('已填入认证信息')
|
||||
}
|
||||
}
|
||||
@@ -543,6 +592,15 @@ function handleAuthCaptureSelect(candidate: {
|
||||
|
||||
const quickPlatform = ref('sub2api')
|
||||
|
||||
function inferPlatform(row: Pick<UpstreamData, 'api_prefix' | 'groups_endpoint' | 'auth_type' | 'auth_config_masked'>): string {
|
||||
if (row.api_prefix?.replace(/^\/|\/$/g, '') === 'api/v1') return 'sub2api'
|
||||
if (row.api_prefix === '' && row.groups_endpoint === '/api/user/self/groups') {
|
||||
if (row.auth_type === 'nox_token' || row.auth_config_masked?.provider === 'nox-api') return 'nox-api'
|
||||
return 'new-api-user'
|
||||
}
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
function handlePlatformChange(val: string) {
|
||||
if (val === 'sub2api') {
|
||||
form.value.api_prefix = '/api/v1'
|
||||
@@ -559,11 +617,21 @@ function handlePlatformChange(val: string) {
|
||||
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 {
|
||||
form.value.balance_endpoint = ''
|
||||
form.value.balance_response_path = ''
|
||||
@@ -613,13 +681,14 @@ const healthyRate = computed(() => {
|
||||
|
||||
const pendingChecks = computed(() => list.value.filter((item) => !item.last_checked_at).length)
|
||||
|
||||
function isNewApiUserUpstream(row: UpstreamData | null) {
|
||||
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'
|
||||
|| Boolean(row.auth_config_masked?.new_api_user)
|
||||
|| row.auth_type === 'nox_token'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -645,7 +714,7 @@ const recentChecks = computed(() =>
|
||||
)
|
||||
|
||||
const statusLabel = (s: string) => ({ healthy: '健康', unhealthy: '异常', unknown: '未知' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', cookie: 'Cookie', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', 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 '—'
|
||||
@@ -691,6 +760,7 @@ function openCreate() {
|
||||
|
||||
function openEdit(row: UpstreamData) {
|
||||
editingId.value = row.id
|
||||
quickPlatform.value = inferPlatform(row)
|
||||
form.value = {
|
||||
name: row.name,
|
||||
base_url: row.base_url,
|
||||
@@ -813,7 +883,7 @@ async function openKeyGenerate(row: UpstreamData) {
|
||||
rate_limit_5h: 0,
|
||||
rate_limit_1d: 0,
|
||||
rate_limit_7d: 0,
|
||||
endpoint: isNewApiUserUpstream(row) ? '/api/token' : '/keys',
|
||||
endpoint: usesTokenEndpointUpstream(row) ? '/api/token' : '/keys',
|
||||
}
|
||||
useKeyExpiry.value = false
|
||||
keyExpiresDays.value = 30
|
||||
|
||||
Reference in New Issue
Block a user