feat: support real browser auth import
This commit is contained in:
@@ -98,11 +98,18 @@
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.access_mode === 'remote_browser'" label="关联上游">
|
||||
<el-select v-model="form.linked_upstream_id" clearable placeholder="选择要一键刷新凭证的上游" style="width:100%">
|
||||
<el-select v-model="form.linked_upstream_id" clearable placeholder="选择要一键刷新凭证的上游" style="width:100%" @change="handleLinkedUpstreamChange">
|
||||
<el-option v-for="u in upstreamList" :key="u.id" :label="`${u.name} (${u.base_url})`" :value="u.id" />
|
||||
</el-select>
|
||||
<div class="form-hint">关联后可在页面查看器中一键刷新该上游的认证凭证</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.access_mode === 'remote_browser' && form.linked_upstream_id" label="上游类型">
|
||||
<el-select v-model="form.upstream_platform" style="width:100%">
|
||||
<el-option label="Sub2API" value="sub2api" />
|
||||
<el-option label="New-API" value="new-api" />
|
||||
</el-select>
|
||||
<div class="form-hint">保存后会同步该关联上游的接口路径,刷新凭证时按此类型选择 Token 或 Cookie。</div>
|
||||
</el-form-item>
|
||||
<div class="login-section">
|
||||
<div class="login-section-head">
|
||||
<span>登录自动填充</span>
|
||||
@@ -196,6 +203,8 @@ const editingId = ref<number | null>(null)
|
||||
const loginAutofillTouched = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
type UpstreamPlatform = 'sub2api' | 'new-api'
|
||||
|
||||
type PageFormState = {
|
||||
name: string
|
||||
url: string
|
||||
@@ -214,6 +223,7 @@ type PageFormState = {
|
||||
login_password_configured: boolean
|
||||
login_password_clear: boolean
|
||||
linked_upstream_id: number | null
|
||||
upstream_platform: UpstreamPlatform
|
||||
}
|
||||
|
||||
const defaultForm = (): PageFormState => ({
|
||||
@@ -234,6 +244,7 @@ const defaultForm = (): PageFormState => ({
|
||||
login_password_configured: false,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: null,
|
||||
upstream_platform: 'sub2api',
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
const rules = {
|
||||
@@ -280,10 +291,57 @@ function openEdit(page: CustomPageData) {
|
||||
login_password_configured: page.login_password_configured,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: page.linked_upstream_id ?? null,
|
||||
upstream_platform: detectUpstreamPlatform(page.linked_upstream_id ?? null),
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function selectedUpstream(id = form.value.linked_upstream_id): UpstreamData | undefined {
|
||||
return upstreamList.value.find((u) => u.id === id)
|
||||
}
|
||||
|
||||
function detectUpstreamPlatform(id: number | null): UpstreamPlatform {
|
||||
const upstream = selectedUpstream(id)
|
||||
if (!upstream) return 'sub2api'
|
||||
const cfg = upstream.auth_config_masked || {}
|
||||
if (
|
||||
(upstream.groups_endpoint || '').replace(/\/+$/, '') === '/api/user/self/groups' ||
|
||||
cfg.login_path === '/api/user/login'
|
||||
) {
|
||||
return 'new-api'
|
||||
}
|
||||
return 'sub2api'
|
||||
}
|
||||
|
||||
function handleLinkedUpstreamChange(id: number | null) {
|
||||
form.value.upstream_platform = detectUpstreamPlatform(id)
|
||||
}
|
||||
|
||||
async function syncLinkedUpstreamPlatform() {
|
||||
if (form.value.access_mode !== 'remote_browser' || !form.value.linked_upstream_id) return
|
||||
if (form.value.upstream_platform === 'new-api') {
|
||||
await upstreamsApi.update(form.value.linked_upstream_id, {
|
||||
api_prefix: '',
|
||||
groups_endpoint: '/api/user/self/groups',
|
||||
rate_endpoint: '/api/user/self/groups',
|
||||
balance_endpoint: '/api/user/self',
|
||||
balance_response_path: 'data.quota',
|
||||
balance_divisor: 500000,
|
||||
auth_config: { login_path: '/api/user/login', username_field: 'username' },
|
||||
} as any)
|
||||
return
|
||||
}
|
||||
await upstreamsApi.update(form.value.linked_upstream_id, {
|
||||
api_prefix: '/api/v1',
|
||||
groups_endpoint: '/groups/available',
|
||||
rate_endpoint: '/groups/rates',
|
||||
balance_endpoint: '/auth/me',
|
||||
balance_response_path: 'data.balance',
|
||||
balance_divisor: 1,
|
||||
auth_config: { login_path: '/auth/login', username_field: 'email' },
|
||||
} as any)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
@@ -293,6 +351,7 @@ async function handleSave() {
|
||||
...form.value,
|
||||
use_proxy: form.value.access_mode === 'proxy',
|
||||
}
|
||||
delete (payload as any).upstream_platform
|
||||
const hasNewLoginCredentials = Boolean(payload.login_username?.trim() && payload.login_password?.trim())
|
||||
if (!loginAutofillTouched.value && hasNewLoginCredentials) {
|
||||
payload.login_autofill_enabled = true
|
||||
@@ -304,6 +363,7 @@ async function handleSave() {
|
||||
} else {
|
||||
await customPagesApi.create(savePayload)
|
||||
}
|
||||
await syncLinkedUpstreamPlatform()
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
loadList()
|
||||
|
||||
@@ -120,8 +120,7 @@
|
||||
<el-form-item v-if="!editingId" label="系统类型(快捷配置)">
|
||||
<el-select v-model="quickPlatform" @change="handlePlatformChange" style="width: 100%">
|
||||
<el-option label="Sub2API" value="sub2api" />
|
||||
<el-option label="New-API (管理员Key)" value="new-api" />
|
||||
<el-option label="New-API (普通账号)" value="new-api-user" />
|
||||
<el-option label="New-API" value="new-api-user" />
|
||||
<el-option label="自定义" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -405,6 +404,7 @@
|
||||
<AuthCaptureDialog
|
||||
v-model="authCaptureVisible"
|
||||
:initial-url="authCaptureInitialUrl"
|
||||
:preferred-types="authCapturePreferredTypes"
|
||||
@select="handleAuthCaptureSelect"
|
||||
/>
|
||||
</div>
|
||||
@@ -416,7 +416,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh, Plus, Edit, List, Delete, Warning, Clock, ArrowRight, Pointer, Key } from '@element-plus/icons-vue'
|
||||
import { upstreamsApi, type GeneratedUpstreamKey, type UpstreamData, type UpstreamBatchActionResponse } from '@/api'
|
||||
import { upstreamsApi, type AuthCaptureCandidate, type GeneratedUpstreamKey, type UpstreamData, type UpstreamBatchActionResponse } from '@/api'
|
||||
import AuthCaptureDialog from '@/components/AuthCaptureDialog.vue'
|
||||
|
||||
const list = ref<(UpstreamData & { _testing?: boolean; _checking?: boolean })[]>([])
|
||||
@@ -459,6 +459,12 @@ const authCaptureInitialUrl = computed(() => {
|
||||
return base + '/login'
|
||||
})
|
||||
|
||||
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']
|
||||
return ['bearer_token', 'api_key', 'cookie_bundle', 'cookie']
|
||||
})
|
||||
|
||||
function openAuthCapture() {
|
||||
authCaptureVisible.value = true
|
||||
}
|
||||
@@ -480,6 +486,9 @@ function handleAuthCaptureSelect(candidate: {
|
||||
// 完整 cookie 组:value 已是完整 "name1=v1; name2=v2" 字符串
|
||||
form.value.auth_type = 'cookie'
|
||||
form.value.auth_config.cookie_string = candidate.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.api_prefix = ''
|
||||
@@ -498,6 +507,9 @@ 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 === '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.api_prefix = ''
|
||||
@@ -542,14 +554,6 @@ function handlePlatformChange(val: string) {
|
||||
form.value.balance_endpoint = '/auth/me'
|
||||
form.value.balance_response_path = 'data.balance'
|
||||
form.value.balance_divisor = 1.0
|
||||
} else if (val === 'new-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/group/'
|
||||
form.value.rate_endpoint = '/api/option/?key=GroupRatio'
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.balance_endpoint = '/api/user/self'
|
||||
form.value.balance_response_path = 'data.quota'
|
||||
form.value.balance_divisor = 500000
|
||||
} else if (val === 'new-api-user') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
@@ -609,6 +613,16 @@ const healthyRate = computed(() => {
|
||||
|
||||
const pendingChecks = computed(() => list.value.filter((item) => !item.last_checked_at).length)
|
||||
|
||||
function isNewApiUserUpstream(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)
|
||||
)
|
||||
}
|
||||
|
||||
const latestCheckedAt = computed(() => {
|
||||
const times = list.value
|
||||
.map((item) => item.last_checked_at)
|
||||
@@ -799,7 +813,7 @@ async function openKeyGenerate(row: UpstreamData) {
|
||||
rate_limit_5h: 0,
|
||||
rate_limit_1d: 0,
|
||||
rate_limit_7d: 0,
|
||||
endpoint: '/keys',
|
||||
endpoint: isNewApiUserUpstream(row) ? '/api/token' : '/keys',
|
||||
}
|
||||
useKeyExpiry.value = false
|
||||
keyExpiresDays.value = 30
|
||||
|
||||
Reference in New Issue
Block a user