feat: add Nox-API upstream support

This commit is contained in:
liumangmang
2026-06-30 11:22:38 +08:00
parent 9600e4ceba
commit a914d3d222
8 changed files with 189 additions and 33 deletions
+1
View File
@@ -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()
+83 -13
View File
@@ -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 TokenCookie 只能在确认上游支持 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 TokenCookie 只能在确认上游支持 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