2026-01-17 19:50:35 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Dialog
|
|
|
|
|
|
:open="open"
|
|
|
|
|
|
title="用户认证"
|
|
|
|
|
|
description="配置提供商的用户认证信息,用于余额查询、签到等操作"
|
|
|
|
|
|
:icon="KeyRound"
|
|
|
|
|
|
size="md"
|
|
|
|
|
|
@update:open="$emit('update:open', $event)"
|
|
|
|
|
|
>
|
2026-01-29 11:06:30 +08:00
|
|
|
|
<form
|
|
|
|
|
|
:name="`provider-auth-${Date.now()}`"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
@submit.prevent
|
|
|
|
|
|
>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="isLoadingConfig"
|
|
|
|
|
|
class="flex items-center justify-center py-8"
|
|
|
|
|
|
>
|
2026-01-29 11:06:30 +08:00
|
|
|
|
<div class="text-sm text-muted-foreground">
|
|
|
|
|
|
加载配置中...
|
|
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="space-y-4"
|
|
|
|
|
|
>
|
2026-02-15 16:32:23 +08:00
|
|
|
|
<!-- 认证模板 + 认证方式(并排) -->
|
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
:style="{ flex: currentAuthTypes.length > 1 ? 1 : 'auto', width: currentAuthTypes.length > 1 ? undefined : '100%' }"
|
2026-01-17 19:50:35 +08:00
|
|
|
|
>
|
2026-02-15 16:32:23 +08:00
|
|
|
|
<Label>认证模板</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
v-model="selectedArchitectureId"
|
|
|
|
|
|
@update:model-value="handleArchitectureChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="选择认证模板" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="arch in architectures"
|
|
|
|
|
|
:key="arch.architecture_id"
|
|
|
|
|
|
:value="arch.architecture_id"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ arch.display_name }}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentAuthTypes.length > 1"
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
style="flex: 1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Label>认证方式</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
v-model="selectedAuthType"
|
|
|
|
|
|
@update:model-value="handleAuthTypeChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="选择认证方式" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="authType in currentAuthTypes"
|
|
|
|
|
|
:key="authType.type"
|
|
|
|
|
|
:value="authType.type"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ authType.display_name }}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 动态表单字段 -->
|
2026-02-13 16:41:30 +08:00
|
|
|
|
<template v-if="currentSchema">
|
2026-01-17 19:50:35 +08:00
|
|
|
|
<template
|
|
|
|
|
|
v-for="(group, groupIndex) in fieldGroups"
|
|
|
|
|
|
:key="groupIndex"
|
|
|
|
|
|
>
|
2026-02-07 14:30:46 +08:00
|
|
|
|
<!-- 可折叠的分组(代理配置 - 代理节点选择) -->
|
2026-01-17 19:50:35 +08:00
|
|
|
|
<div
|
2026-01-19 18:30:52 +08:00
|
|
|
|
v-if="group.collapsible && group.hasToggle && group.toggleKey"
|
2026-01-17 19:50:35 +08:00
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
<!-- 标题栏:标题在左,开关在右(在卡片外) -->
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<span class="text-sm font-medium text-foreground">{{ group.title }}</span>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">启用代理</span>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
:model-value="formData[group.toggleKey] || false"
|
2026-02-07 14:30:46 +08:00
|
|
|
|
@update:model-value="handleProxyToggle(group.toggleKey, $event)"
|
2026-01-19 18:30:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
2026-02-07 14:30:46 +08:00
|
|
|
|
<!-- 展开内容(卡片)- 代理节点选择 -->
|
2026-01-19 18:30:52 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="formData[group.toggleKey]"
|
|
|
|
|
|
class="rounded-lg border border-border bg-muted/30 px-4 py-3"
|
2026-01-17 19:50:35 +08:00
|
|
|
|
>
|
2026-02-07 14:30:46 +08:00
|
|
|
|
<ProxyNodeSelect
|
|
|
|
|
|
ref="proxyNodeSelectRef"
|
|
|
|
|
|
:model-value="formData.proxy_node_id || ''"
|
|
|
|
|
|
trigger-class="h-8"
|
|
|
|
|
|
@update:model-value="(v: string) => { formData.proxy_node_id = v; handleFieldChange('proxy_node_id', v) }"
|
|
|
|
|
|
/>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</div>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 普通分组(非折叠) -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<!-- 分组标题 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="group.title"
|
|
|
|
|
|
class="pt-2 text-sm font-medium text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ group.title }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-20 00:55:52 +08:00
|
|
|
|
<!-- inline 布局:字段横向排列 -->
|
2026-01-19 18:30:52 +08:00
|
|
|
|
<div
|
2026-01-20 00:55:52 +08:00
|
|
|
|
v-if="group.layout === 'inline'"
|
|
|
|
|
|
class="flex gap-3"
|
2026-01-19 18:30:52 +08:00
|
|
|
|
>
|
2026-01-20 00:55:52 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="field in group.fields"
|
|
|
|
|
|
:key="field.key"
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
:style="{ flex: field.flex || 1 }"
|
2026-01-19 18:30:52 +08:00
|
|
|
|
>
|
2026-01-20 00:55:52 +08:00
|
|
|
|
<Label>
|
|
|
|
|
|
{{ field.label }}
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="field.required"
|
|
|
|
|
|
class="text-muted-foreground/70"
|
|
|
|
|
|
>*</span>
|
|
|
|
|
|
</Label>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
|
2026-01-20 00:55:52 +08:00
|
|
|
|
<!-- 文本输入 -->
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-if="field.type === 'text'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
:placeholder="field.sensitive ? (sensitivePlaceholders[field.key] || field.placeholder) : field.placeholder"
|
|
|
|
|
|
:masked="field.sensitive"
|
|
|
|
|
|
disable-autofill
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
/>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
|
2026-01-20 00:55:52 +08:00
|
|
|
|
<!-- 密码/敏感输入 -->
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-else-if="field.type === 'password'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
|
|
|
|
|
masked
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
</div>
|
2026-01-20 00:55:52 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- vertical 布局(默认):字段垂直排列 -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="field in group.fields"
|
|
|
|
|
|
:key="field.key"
|
|
|
|
|
|
class="space-y-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Label>
|
|
|
|
|
|
{{ field.label }}
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="field.required"
|
|
|
|
|
|
class="text-muted-foreground/70"
|
|
|
|
|
|
>*</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文本输入 -->
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-if="field.type === 'text'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
:placeholder="field.sensitive ? (sensitivePlaceholders[field.key] || field.placeholder) : field.placeholder"
|
|
|
|
|
|
:masked="field.sensitive"
|
|
|
|
|
|
disable-autofill
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 密码/敏感输入 -->
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-else-if="field.type === 'password'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
|
|
|
|
|
masked
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 下拉选择 -->
|
|
|
|
|
|
<Select
|
|
|
|
|
|
v-else-if="field.type === 'select'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue :placeholder="field.placeholder || '请选择'" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="option in field.options"
|
|
|
|
|
|
:key="option.value"
|
|
|
|
|
|
:value="option.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 多行文本 -->
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
v-else-if="field.type === 'textarea'"
|
|
|
|
|
|
v-model="formData[field.key]"
|
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
@update:model-value="handleFieldChange(field.key, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 帮助文本 -->
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="field.helpText"
|
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ field.helpText }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-01-19 18:30:52 +08:00
|
|
|
|
</template>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
2026-05-18 23:51:14 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="rounded-lg border border-border bg-muted/20 px-4 py-3">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label class="text-sm font-medium">
|
|
|
|
|
|
额度提醒
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<p class="mt-1 text-xs text-muted-foreground">
|
2026-05-22 01:45:46 +08:00
|
|
|
|
余额低于阈值时通过通知服务发送提醒
|
2026-05-18 23:51:14 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Switch v-model="quotaAlert.enabled" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="quotaAlert.enabled"
|
|
|
|
|
|
class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<Label>提醒阈值</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="quotaAlert.threshold_amount"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
step="0.0001"
|
|
|
|
|
|
placeholder="0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<Label>获取频率(秒)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="quotaAlert.fetch_interval_seconds"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="30"
|
|
|
|
|
|
max="86400"
|
|
|
|
|
|
step="1"
|
|
|
|
|
|
placeholder="30"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2026-02-02 03:16:52 +08:00
|
|
|
|
<div class="flex w-full items-center justify-between">
|
|
|
|
|
|
<!-- 左侧:清除按钮(仅在已有配置时显示) -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="hasExistingConfig"
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
:disabled="isClearing"
|
|
|
|
|
|
@click="handleClear"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isClearing ? '清除中...' : '清除' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 右侧:验证、保存、取消按钮 -->
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
:disabled="isVerifying || !canVerify"
|
|
|
|
|
|
@click="handleVerify"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isVerifying ? '验证中...' : '验证' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
:disabled="isSaving || !canSave"
|
|
|
|
|
|
@click="handleSave"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isSaving ? '保存中...' : '保存' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="$emit('update:open', false)"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-15 16:32:23 +08:00
|
|
|
|
import { ref, computed, watch, nextTick } from 'vue'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
import { KeyRound } from 'lucide-vue-next'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Label,
|
|
|
|
|
|
Textarea,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
2026-01-19 18:30:52 +08:00
|
|
|
|
Switch,
|
2026-01-17 19:50:35 +08:00
|
|
|
|
} from '@/components/ui'
|
2026-02-13 16:41:30 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getArchitectures,
|
|
|
|
|
|
saveProviderOpsConfig,
|
|
|
|
|
|
verifyProviderAuth,
|
|
|
|
|
|
getProviderOpsConfig,
|
|
|
|
|
|
deleteProviderOpsConfig,
|
|
|
|
|
|
type ArchitectureInfo,
|
2026-05-18 23:51:14 +08:00
|
|
|
|
type QuotaAlertConfig,
|
2026-02-13 16:41:30 +08:00
|
|
|
|
} from '@/api/providerOps'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-02-02 03:16:52 +08:00
|
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
2026-02-13 16:41:30 +08:00
|
|
|
|
import type { AuthTemplateFieldGroup } from '../auth-templates/types'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
import {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
schemaToFieldGroups,
|
|
|
|
|
|
buildRequestFromSchema,
|
|
|
|
|
|
parseConfigFromSchema,
|
|
|
|
|
|
validateFromSchema,
|
|
|
|
|
|
formatQuotaFromSchema,
|
|
|
|
|
|
handleSchemaFieldChange,
|
|
|
|
|
|
type CredentialsSchema,
|
|
|
|
|
|
} from '../auth-templates/schema-utils'
|
2026-02-07 14:30:46 +08:00
|
|
|
|
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
|
|
|
|
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
providerId: string
|
|
|
|
|
|
providerWebsite?: string
|
2026-02-22 00:43:41 +08:00
|
|
|
|
currentConfig?: Record<string, unknown> | null
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'update:open', value: boolean): void
|
|
|
|
|
|
(e: 'saved'): void
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 敏感字段检测:根据 schema 动态判断
|
|
|
|
|
|
function isSensitiveField(key: string): boolean {
|
|
|
|
|
|
if (!currentSchema.value) return false
|
|
|
|
|
|
const prop = currentSchema.value.properties[key]
|
|
|
|
|
|
return prop?.['x-sensitive'] === true
|
|
|
|
|
|
}
|
2026-01-29 11:06:30 +08:00
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const { success: showSuccess, error: showError } = useToast()
|
2026-02-02 03:16:52 +08:00
|
|
|
|
const { confirmDanger } = useConfirm()
|
2026-02-07 14:30:46 +08:00
|
|
|
|
const proxyNodeSelectRef = ref<InstanceType<typeof ProxyNodeSelect> | null>(null)
|
|
|
|
|
|
const proxyNodesStore = useProxyNodesStore()
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
/** 启用代理时加载节点列表 */
|
2026-02-07 14:30:46 +08:00
|
|
|
|
function handleProxyToggle(toggleKey: string, value: boolean) {
|
|
|
|
|
|
formData.value[toggleKey] = value
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
proxyNodesStore.ensureLoaded()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
const isSaving = ref(false)
|
|
|
|
|
|
const isVerifying = ref(false)
|
|
|
|
|
|
const isLoadingConfig = ref(false)
|
2026-02-02 03:16:52 +08:00
|
|
|
|
const isClearing = ref(false)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const verifyStatus = ref<'success' | 'error' | null>(null)
|
|
|
|
|
|
const formChanged = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 敏感字段的 placeholder(存储脱敏后的已保存值)
|
|
|
|
|
|
const sensitivePlaceholders = ref<Record<string, string>>({})
|
|
|
|
|
|
// 是否有已保存的配置(编辑模式)
|
|
|
|
|
|
const hasExistingConfig = ref(false)
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 架构列表(从后端获取)
|
|
|
|
|
|
const architectures = ref<ArchitectureInfo[]>([])
|
|
|
|
|
|
const architecturesLoaded = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 当前选择
|
|
|
|
|
|
const selectedArchitectureId = ref('new_api')
|
2026-02-15 16:32:23 +08:00
|
|
|
|
const selectedAuthType = ref('')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const formData = ref<Record<string, unknown>>({})
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const quotaAlert = ref<QuotaAlertConfig>({
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
threshold_amount: 0,
|
|
|
|
|
|
fetch_interval_seconds: 30,
|
|
|
|
|
|
})
|
|
|
|
|
|
const savedQuotaAlertSignature = ref(quotaAlertSignature(quotaAlert.value))
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
2026-02-15 16:32:23 +08:00
|
|
|
|
// 当前架构支持的认证方式
|
|
|
|
|
|
const currentAuthTypes = computed(() => {
|
|
|
|
|
|
const arch = architectures.value.find((a) => a.architecture_id === selectedArchitectureId.value)
|
|
|
|
|
|
return arch?.supported_auth_types ?? []
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 当前架构的 schema(优先从选中的 auth_type 获取)
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const currentSchema = computed<CredentialsSchema | null>(() => {
|
|
|
|
|
|
const arch = architectures.value.find((a) => a.architecture_id === selectedArchitectureId.value)
|
2026-02-15 16:32:23 +08:00
|
|
|
|
if (!arch) return null
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有选中的 auth_type 且架构有多个 connector,从对应的 auth_type 获取 schema
|
|
|
|
|
|
if (selectedAuthType.value && arch.supported_auth_types.length > 1) {
|
|
|
|
|
|
const authType = arch.supported_auth_types.find((t) => t.type === selectedAuthType.value)
|
|
|
|
|
|
if (authType?.credentials_schema) {
|
|
|
|
|
|
return authType.credentials_schema as CredentialsSchema
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
return (arch?.credentials_schema as CredentialsSchema) ?? null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 表单是否可以验证(必填字段已填写)
|
|
|
|
|
|
const canVerify = computed(() => {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (!schema) return false
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 编辑模式下,敏感字段可以为空(使用已保存的值)
|
2026-02-13 16:41:30 +08:00
|
|
|
|
let dataToValidate = formData.value
|
2026-01-17 19:50:35 +08:00
|
|
|
|
if (hasExistingConfig.value) {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
dataToValidate = { ...formData.value }
|
|
|
|
|
|
for (const key of Object.keys(schema.properties)) {
|
|
|
|
|
|
if (isSensitiveField(key) && !dataToValidate[key] && sensitivePlaceholders.value[key]) {
|
|
|
|
|
|
dataToValidate[key] = 'placeholder'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const error = validateFromSchema(schema, dataToValidate)
|
|
|
|
|
|
if (error) return false
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
const effectiveBaseUrl = formData.value.base_url || props.providerWebsite
|
|
|
|
|
|
return !!effectiveBaseUrl
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 保存按钮是否可用:验证成功且表单未变动
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const quotaAlertChanged = computed(() => {
|
|
|
|
|
|
return quotaAlertSignature(quotaAlert.value) !== savedQuotaAlertSignature.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const canSave = computed(() => {
|
2026-05-18 23:51:14 +08:00
|
|
|
|
return (
|
|
|
|
|
|
(verifyStatus.value === 'success' && !formChanged.value)
|
|
|
|
|
|
|| (hasExistingConfig.value && quotaAlertChanged.value && !formChanged.value)
|
|
|
|
|
|
)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 字段分组
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const fieldGroups = computed<AuthTemplateFieldGroup[]>(() => {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
if (!currentSchema.value) return []
|
|
|
|
|
|
return schemaToFieldGroups(currentSchema.value, props.providerWebsite)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Methods
|
2026-02-13 16:41:30 +08:00
|
|
|
|
function handleArchitectureChange() {
|
2026-02-15 16:32:23 +08:00
|
|
|
|
// 切换架构时,默认选中第一个认证方式
|
|
|
|
|
|
const authTypes = currentAuthTypes.value
|
|
|
|
|
|
selectedAuthType.value = authTypes.length > 0 ? authTypes[0].type : ''
|
|
|
|
|
|
resetFormData()
|
|
|
|
|
|
verifyStatus.value = null
|
|
|
|
|
|
formChanged.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAuthTypeChange() {
|
2026-01-17 19:50:35 +08:00
|
|
|
|
resetFormData()
|
|
|
|
|
|
verifyStatus.value = null
|
|
|
|
|
|
formChanged.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
function handleFieldChange(fieldKey: string, value: unknown) {
|
2026-01-17 19:50:35 +08:00
|
|
|
|
formChanged.value = true
|
2026-01-20 00:55:52 +08:00
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 执行 schema 定义的字段钩子
|
|
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (schema) {
|
|
|
|
|
|
handleSchemaFieldChange(schema, fieldKey, value, formData.value)
|
2026-01-20 00:55:52 +08:00
|
|
|
|
}
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 formData 变化,验证成功后的修改需要重新验证
|
|
|
|
|
|
watch(
|
|
|
|
|
|
formData,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
if (verifyStatus.value === 'success') {
|
|
|
|
|
|
formChanged.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
function resetFormData() {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (!schema) {
|
2026-01-17 19:50:35 +08:00
|
|
|
|
formData.value = {}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 初始化表单数据
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const data: Record<string, unknown> = {}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
2026-02-22 00:43:41 +08:00
|
|
|
|
data[key] = (prop as Record<string, unknown>)['x-default-value'] ?? ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 代理相关默认值
|
|
|
|
|
|
data.proxy_enabled = false
|
|
|
|
|
|
data.proxy_node_id = ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
formData.value = data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatQuota(quota: number): string {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (schema) {
|
|
|
|
|
|
return formatQuotaFromSchema(schema, quota)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
return quota.toLocaleString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 23:51:14 +08:00
|
|
|
|
function finiteNumber(value: unknown): number | null {
|
|
|
|
|
|
const numberValue = Number(value)
|
|
|
|
|
|
return Number.isFinite(numberValue) ? numberValue : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
async function handleVerify() {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (!schema) return
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证表单(编辑模式下敏感字段可以为空)
|
|
|
|
|
|
let dataToValidate = formData.value
|
|
|
|
|
|
if (hasExistingConfig.value) {
|
|
|
|
|
|
dataToValidate = { ...formData.value }
|
2026-02-13 16:41:30 +08:00
|
|
|
|
for (const key of Object.keys(schema.properties)) {
|
|
|
|
|
|
if (isSensitiveField(key) && !dataToValidate[key] && sensitivePlaceholders.value[key]) {
|
|
|
|
|
|
dataToValidate[key] = 'placeholder'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const error = validateFromSchema(schema, dataToValidate)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
if (error) {
|
|
|
|
|
|
showError(error)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveBaseUrl = formData.value.base_url || props.providerWebsite
|
|
|
|
|
|
if (!effectiveBaseUrl) {
|
|
|
|
|
|
showError('请填写 API 地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isVerifying.value = true
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const request = buildRequestFromSchema(
|
|
|
|
|
|
schema,
|
|
|
|
|
|
selectedArchitectureId.value,
|
|
|
|
|
|
formData.value,
|
|
|
|
|
|
props.providerWebsite,
|
|
|
|
|
|
)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const verifyRequest = {
|
|
|
|
|
|
...request,
|
|
|
|
|
|
base_url: request.base_url || effectiveBaseUrl,
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await verifyProviderAuth(props.providerId, verifyRequest)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
2026-01-18 11:40:59 +08:00
|
|
|
|
const username = result.data?.username?.trim() || result.data?.display_name?.trim()
|
|
|
|
|
|
const quota = result.data?.quota
|
|
|
|
|
|
|
|
|
|
|
|
if (!username || quota === undefined || quota === null) {
|
|
|
|
|
|
verifyStatus.value = 'error'
|
|
|
|
|
|
const missing: string[] = []
|
|
|
|
|
|
if (!username) missing.push('用户信息')
|
|
|
|
|
|
if (quota === undefined || quota === null) missing.push('余额')
|
|
|
|
|
|
showError(`验证响应缺少: ${missing.join('、')}`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
verifyStatus.value = 'success'
|
2026-02-13 16:41:30 +08:00
|
|
|
|
formChanged.value = false
|
2026-02-15 16:32:23 +08:00
|
|
|
|
|
|
|
|
|
|
// Token Rotation: 验证过程中 refresh_token 可能已被轮换,用新值更新表单
|
|
|
|
|
|
// 必须在 formChanged=false 之后执行,避免 watch 将 formChanged 重新设为 true
|
|
|
|
|
|
if (result.updated_credentials) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(result.updated_credentials)) {
|
|
|
|
|
|
formData.value[key] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
// 凭据更新不算用户修改,保持可保存状态
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
formChanged.value = false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 11:40:59 +08:00
|
|
|
|
const displayName = result.data?.display_name || result.data?.username
|
2026-02-13 21:03:21 +08:00
|
|
|
|
const extra = result.data?.extra
|
|
|
|
|
|
let balanceText = `余额: ${formatQuota(quota)}`
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const extraBalance = finiteNumber(extra?.balance)
|
|
|
|
|
|
const extraPoints = finiteNumber(extra?.points)
|
|
|
|
|
|
if (extraBalance !== null && extraPoints !== null) {
|
|
|
|
|
|
balanceText = `余额: ${formatQuota(extraBalance)} | 积分: ${formatQuota(extraPoints)}`
|
2026-02-13 21:03:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
showSuccess(`用户: ${displayName} | ${balanceText}`, '验证成功')
|
2026-01-18 11:40:59 +08:00
|
|
|
|
}
|
2026-01-17 19:50:35 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
verifyStatus.value = 'error'
|
2026-02-15 16:32:23 +08:00
|
|
|
|
|
|
|
|
|
|
// Token Rotation: 即使验证失败,refresh_token 可能已被轮换(旧 token 已失效)
|
|
|
|
|
|
if (result.updated_credentials) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(result.updated_credentials)) {
|
|
|
|
|
|
formData.value[key] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
showError(result.message || '验证失败')
|
|
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
2026-01-17 19:50:35 +08:00
|
|
|
|
verifyStatus.value = 'error'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
showError(parseApiError(error, '验证失败'))
|
2026-01-17 19:50:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
isVerifying.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (!schema) return
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 验证表单
|
2026-01-17 19:50:35 +08:00
|
|
|
|
let dataToValidate = formData.value
|
|
|
|
|
|
if (hasExistingConfig.value) {
|
|
|
|
|
|
dataToValidate = { ...formData.value }
|
2026-02-13 16:41:30 +08:00
|
|
|
|
for (const key of Object.keys(schema.properties)) {
|
|
|
|
|
|
if (isSensitiveField(key) && !dataToValidate[key] && sensitivePlaceholders.value[key]) {
|
|
|
|
|
|
dataToValidate[key] = 'placeholder'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const error = validateFromSchema(schema, dataToValidate)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
if (error) {
|
|
|
|
|
|
showError(error)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveBaseUrl = formData.value.base_url || props.providerWebsite
|
|
|
|
|
|
if (!effectiveBaseUrl) {
|
|
|
|
|
|
showError('请填写 API 地址')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isSaving.value = true
|
|
|
|
|
|
try {
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const request = buildRequestFromSchema(
|
|
|
|
|
|
schema,
|
|
|
|
|
|
selectedArchitectureId.value,
|
|
|
|
|
|
formData.value,
|
|
|
|
|
|
props.providerWebsite,
|
|
|
|
|
|
)
|
2026-05-18 23:51:14 +08:00
|
|
|
|
request.quota_alert = normalizedQuotaAlert()
|
2026-01-17 19:50:35 +08:00
|
|
|
|
const result = await saveProviderOpsConfig(props.providerId, request)
|
|
|
|
|
|
if (result.success) {
|
2026-05-18 23:51:14 +08:00
|
|
|
|
savedQuotaAlertSignature.value = quotaAlertSignature(quotaAlert.value)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
showSuccess(result.message || '配置已保存', '保存成功')
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(result.message || '保存失败')
|
|
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '保存失败'), '保存失败')
|
2026-01-17 19:50:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
isSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 03:16:52 +08:00
|
|
|
|
async function handleClear() {
|
|
|
|
|
|
if (!props.providerId) return
|
|
|
|
|
|
|
|
|
|
|
|
const confirmed = await confirmDanger(
|
|
|
|
|
|
'确定要清除该提供商的认证配置吗?清除后将无法进行余额查询、签到等操作。',
|
|
|
|
|
|
'清除认证',
|
|
|
|
|
|
'清除'
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
isClearing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await deleteProviderOpsConfig(props.providerId)
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
showSuccess(result.message || '认证信息已清除', '清除成功')
|
|
|
|
|
|
hasExistingConfig.value = false
|
|
|
|
|
|
sensitivePlaceholders.value = {}
|
|
|
|
|
|
verifyStatus.value = null
|
|
|
|
|
|
formChanged.value = false
|
2026-02-13 16:41:30 +08:00
|
|
|
|
selectedArchitectureId.value = 'new_api'
|
2026-02-15 16:32:23 +08:00
|
|
|
|
selectedAuthType.value = ''
|
2026-05-18 23:51:14 +08:00
|
|
|
|
loadQuotaAlert(null)
|
2026-02-02 03:16:52 +08:00
|
|
|
|
resetFormData()
|
|
|
|
|
|
emit('saved')
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(result.message || '清除失败')
|
|
|
|
|
|
}
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
showError(parseApiError(error, '清除失败'), '清除失败')
|
2026-02-02 03:16:52 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
isClearing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 23:51:14 +08:00
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stringOrDefault(value: unknown, fallback: string): string {
|
|
|
|
|
|
return typeof value === 'string' && value.trim() ? value : fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
function loadFromConfig(config: Record<string, unknown>) {
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const connector = isRecord(config.connector) ? config.connector : null
|
|
|
|
|
|
if (!connector) return
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
hasExistingConfig.value = true
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 根据已保存的 architecture_id 选择对应架构
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const architectureId = stringOrDefault(config.architecture_id, 'new_api')
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const archExists = architectures.value.some((a) => a.architecture_id === architectureId)
|
|
|
|
|
|
selectedArchitectureId.value = archExists ? architectureId : 'new_api'
|
2026-01-19 17:18:03 +08:00
|
|
|
|
|
2026-02-15 16:32:23 +08:00
|
|
|
|
// 从已保存的 connector auth_type 恢复认证方式选择
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const savedAuthType = stringOrDefault(connector.auth_type, '')
|
2026-02-15 16:32:23 +08:00
|
|
|
|
const authTypes = currentAuthTypes.value
|
|
|
|
|
|
if (savedAuthType && authTypes.some((t) => t.type === savedAuthType)) {
|
|
|
|
|
|
selectedAuthType.value = savedAuthType
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedAuthType.value = authTypes.length > 0 ? authTypes[0].type : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
const schema = currentSchema.value
|
|
|
|
|
|
if (schema) {
|
2026-05-18 23:51:14 +08:00
|
|
|
|
const parsedData = parseConfigFromSchema(schema, {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
connector,
|
|
|
|
|
|
})
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 敏感字段:脱敏值放到 placeholder,表单值设为空
|
|
|
|
|
|
sensitivePlaceholders.value = {}
|
2026-02-13 16:41:30 +08:00
|
|
|
|
for (const key of Object.keys(schema.properties)) {
|
|
|
|
|
|
if (isSensitiveField(key) && parsedData[key]) {
|
|
|
|
|
|
sensitivePlaceholders.value[key] = `${parsedData[key]}`
|
|
|
|
|
|
parsedData[key] = ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
formData.value = parsedData
|
|
|
|
|
|
}
|
2026-05-18 23:51:14 +08:00
|
|
|
|
loadQuotaAlert(config.quota_alert)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function defaultQuotaAlert(): QuotaAlertConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
threshold_amount: 0,
|
|
|
|
|
|
fetch_interval_seconds: 30,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeQuotaAlert(value: unknown): QuotaAlertConfig {
|
|
|
|
|
|
if (!value || typeof value !== 'object') return defaultQuotaAlert()
|
|
|
|
|
|
const item = value as Record<string, unknown>
|
|
|
|
|
|
const threshold = Number(item.threshold_amount)
|
|
|
|
|
|
const interval = Number(item.fetch_interval_seconds)
|
|
|
|
|
|
return {
|
|
|
|
|
|
enabled: item.enabled === true,
|
|
|
|
|
|
threshold_amount: Number.isFinite(threshold) && threshold >= 0 ? threshold : 0,
|
|
|
|
|
|
fetch_interval_seconds: Number.isFinite(interval) && interval >= 30 ? Math.min(Math.floor(interval), 86400) : 30,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizedQuotaAlert(): QuotaAlertConfig {
|
|
|
|
|
|
return normalizeQuotaAlert(quotaAlert.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function quotaAlertSignature(value: QuotaAlertConfig): string {
|
|
|
|
|
|
const normalized = normalizeQuotaAlert(value)
|
|
|
|
|
|
return JSON.stringify([
|
|
|
|
|
|
normalized.enabled,
|
|
|
|
|
|
normalized.threshold_amount,
|
|
|
|
|
|
normalized.fetch_interval_seconds,
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadQuotaAlert(value: unknown) {
|
|
|
|
|
|
const normalized = normalizeQuotaAlert(value)
|
|
|
|
|
|
quotaAlert.value = normalized
|
|
|
|
|
|
savedQuotaAlertSignature.value = quotaAlertSignature(normalized)
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
/** 确保架构列表已加载 */
|
|
|
|
|
|
async function ensureArchitecturesLoaded(): Promise<void> {
|
|
|
|
|
|
if (architecturesLoaded.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
architectures.value = await getArchitectures()
|
|
|
|
|
|
architecturesLoaded.value = true
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
architectures.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 打开对话框时初始化
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
async (newVal) => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
verifyStatus.value = null
|
|
|
|
|
|
formChanged.value = false
|
|
|
|
|
|
|
2026-02-13 16:41:30 +08:00
|
|
|
|
// 确保架构列表已加载
|
|
|
|
|
|
await ensureArchitecturesLoaded()
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 如果传入了 currentConfig,直接使用
|
|
|
|
|
|
if (props.currentConfig?.connector) {
|
|
|
|
|
|
loadFromConfig(props.currentConfig)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 否则尝试从后端加载现有配置
|
|
|
|
|
|
if (props.providerId) {
|
|
|
|
|
|
isLoadingConfig.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const config = await getProviderOpsConfig(props.providerId)
|
|
|
|
|
|
if (config.is_configured && config.architecture_id) {
|
|
|
|
|
|
const configData = {
|
|
|
|
|
|
architecture_id: config.architecture_id,
|
|
|
|
|
|
base_url: config.base_url,
|
|
|
|
|
|
connector: config.connector,
|
2026-05-18 23:51:14 +08:00
|
|
|
|
quota_alert: config.quota_alert,
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
loadFromConfig(configData)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hasExistingConfig.value = false
|
|
|
|
|
|
sensitivePlaceholders.value = {}
|
2026-05-18 23:51:14 +08:00
|
|
|
|
loadQuotaAlert(null)
|
2026-02-13 16:41:30 +08:00
|
|
|
|
selectedArchitectureId.value = 'new_api'
|
2026-02-15 16:32:23 +08:00
|
|
|
|
selectedAuthType.value = ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
resetFormData()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
hasExistingConfig.value = false
|
|
|
|
|
|
sensitivePlaceholders.value = {}
|
2026-05-18 23:51:14 +08:00
|
|
|
|
loadQuotaAlert(null)
|
2026-02-13 16:41:30 +08:00
|
|
|
|
selectedArchitectureId.value = 'new_api'
|
2026-02-15 16:32:23 +08:00
|
|
|
|
selectedAuthType.value = ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
resetFormData()
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
isLoadingConfig.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hasExistingConfig.value = false
|
|
|
|
|
|
sensitivePlaceholders.value = {}
|
2026-05-18 23:51:14 +08:00
|
|
|
|
loadQuotaAlert(null)
|
2026-02-13 16:41:30 +08:00
|
|
|
|
selectedArchitectureId.value = 'new_api'
|
2026-02-15 16:32:23 +08:00
|
|
|
|
selectedAuthType.value = ''
|
2026-01-17 19:50:35 +08:00
|
|
|
|
resetFormData()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
</script>
|