mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge branch 'fawney19:main' into main
This commit is contained in:
@@ -58,6 +58,11 @@ ADMIN_USERNAME=admin
|
|||||||
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||||
|
|
||||||
# PostgreSQL 连接池配置(默认适合单实例/小型部署;高并发可按需调大)
|
# PostgreSQL 连接池配置(默认适合单实例/小型部署;高并发可按需调大)
|
||||||
|
# 推荐计算方式(单实例):
|
||||||
|
# MAX = CPU 核数 × 10(AI 网关偏 IO 等待,可激进些;纯 OLTP 用 × 4)
|
||||||
|
# MIN = MAX × 0.2(保留常驻连接应对突发流量,避免冷启动握手开销)
|
||||||
|
# 多实例部署时请按 实例数 × MAX 控制总和,PG 端 max_connections 至少为该总和 + 20 余量
|
||||||
# AETHER_GATEWAY_DATA_POSTGRES_MIN_CONNECTIONS=1
|
# AETHER_GATEWAY_DATA_POSTGRES_MIN_CONNECTIONS=1
|
||||||
# AETHER_GATEWAY_DATA_POSTGRES_MAX_CONNECTIONS=20
|
# AETHER_GATEWAY_DATA_POSTGRES_MAX_CONNECTIONS=20
|
||||||
# AETHER_GATEWAY_DATA_POSTGRES_IDLE_TIMEOUT_MS=30000
|
# AETHER_GATEWAY_DATA_POSTGRES_STATEMENT_CACHE_CAPACITY=100
|
||||||
|
# AETHER_GATEWAY_DATA_POSTGRES_ACQUIRE_TIMEOUT_MS=3000
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
import { PopoverTrigger } from 'radix-vue'
|
import { PopoverTrigger } from 'radix-vue'
|
||||||
import { useAttrs } from 'vue'
|
import { useAttrs } from 'vue'
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
inheritAttrs: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
as?: string
|
as?: string
|
||||||
@@ -14,6 +10,10 @@ withDefaults(defineProps<{
|
|||||||
as: 'button',
|
as: 'button',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,6 @@
|
|||||||
两次输入的密码不一致
|
两次输入的密码不一致
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- 登录链接 -->
|
<!-- 登录链接 -->
|
||||||
|
|||||||
@@ -17,6 +17,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue?: string
|
||||||
|
siteKey: string
|
||||||
|
action?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>(), {
|
||||||
|
modelValue: '',
|
||||||
|
action: undefined,
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
error: [message: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
const TURNSTILE_SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
||||||
|
|
||||||
type TurnstileWidgetId = string
|
type TurnstileWidgetId = string
|
||||||
@@ -46,22 +62,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
siteKey: string
|
|
||||||
action?: string
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
modelValue: '',
|
|
||||||
action: undefined,
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: string]
|
|
||||||
error: [message: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const widgetId = ref<TurnstileWidgetId | null>(null)
|
const widgetId = ref<TurnstileWidgetId | null>(null)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|||||||
@@ -28,7 +28,10 @@
|
|||||||
{{ manualGlobalModelMode ? '选择已有模型' : '手动添加' }}
|
{{ manualGlobalModelMode ? '选择已有模型' : '手动添加' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!manualGlobalModelMode" class="space-y-2">
|
<div
|
||||||
|
v-if="!manualGlobalModelMode"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
:model-value="form.global_model_id"
|
:model-value="form.global_model_id"
|
||||||
:disabled="loadingGlobalModels"
|
:disabled="loadingGlobalModels"
|
||||||
@@ -48,10 +51,16 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="manual-global-model-name" class="text-xs">模型ID *</Label>
|
<Label
|
||||||
|
for="manual-global-model-name"
|
||||||
|
class="text-xs"
|
||||||
|
>模型ID *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="manual-global-model-name"
|
id="manual-global-model-name"
|
||||||
v-model="form.manual_global_model_name"
|
v-model="form.manual_global_model_name"
|
||||||
@@ -60,7 +69,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="manual-global-model-display-name" class="text-xs">显示名称</Label>
|
<Label
|
||||||
|
for="manual-global-model-display-name"
|
||||||
|
class="text-xs"
|
||||||
|
>显示名称</Label>
|
||||||
<Input
|
<Input
|
||||||
id="manual-global-model-display-name"
|
id="manual-global-model-display-name"
|
||||||
v-model="form.manual_global_model_display_name"
|
v-model="form.manual_global_model_display_name"
|
||||||
@@ -79,7 +91,10 @@
|
|||||||
没有可选择的本地全局模型。可以切换到“手动添加”继续保存。
|
没有可选择的本地全局模型。可以切换到“手动添加”继续保存。
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="provider-model-name" class="text-xs">Provider 模型名 *</Label>
|
<Label
|
||||||
|
for="provider-model-name"
|
||||||
|
class="text-xs"
|
||||||
|
>Provider 模型名 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="provider-model-name"
|
id="provider-model-name"
|
||||||
v-model="form.provider_model_name"
|
v-model="form.provider_model_name"
|
||||||
|
|||||||
@@ -293,7 +293,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 桌面端表格视图 -->
|
<!-- 桌面端表格视图 -->
|
||||||
<Table :class="['hidden md:table table-fixed w-full', isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']">
|
<Table
|
||||||
|
class="hidden md:table table-fixed w-full"
|
||||||
|
:class="[isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']"
|
||||||
|
>
|
||||||
<colgroup v-if="isAdmin">
|
<colgroup v-if="isAdmin">
|
||||||
<col class="w-[8%]">
|
<col class="w-[8%]">
|
||||||
<col class="w-[12%]">
|
<col class="w-[12%]">
|
||||||
@@ -346,7 +349,8 @@
|
|||||||
密钥
|
密钥
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
:class="['h-12 font-semibold', isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
class="h-12 font-semibold"
|
||||||
|
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||||
column-key="model"
|
column-key="model"
|
||||||
:sortable="false"
|
:sortable="false"
|
||||||
:filter-active="filterModel !== '__all__'"
|
:filter-active="filterModel !== '__all__'"
|
||||||
@@ -383,7 +387,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</SortableTableHead>
|
</SortableTableHead>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
:class="['h-12 font-semibold', isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
class="h-12 font-semibold"
|
||||||
|
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||||
column-key="api_format"
|
column-key="api_format"
|
||||||
:sortable="false"
|
:sortable="false"
|
||||||
:filter-active="filterApiFormat !== '__all__'"
|
:filter-active="filterApiFormat !== '__all__'"
|
||||||
@@ -495,7 +500,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
:class="['font-medium py-4', isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
class="font-medium py-4"
|
||||||
|
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
|
||||||
:title="getModelTooltip(record)"
|
:title="getModelTooltip(record)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -582,7 +588,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
:class="['py-4', isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
class="py-4"
|
||||||
|
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
|
||||||
:title="getApiFormatTooltip(record)"
|
:title="getApiFormatTooltip(record)"
|
||||||
>
|
>
|
||||||
<!-- 有格式转换或同族格式差异:两行显示 -->
|
<!-- 有格式转换或同族格式差异:两行显示 -->
|
||||||
@@ -682,8 +689,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-x-1 text-xs leading-tight tabular-nums text-muted-foreground">
|
<div class="mt-0.5 grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-x-1 text-xs leading-tight tabular-nums text-muted-foreground">
|
||||||
<span
|
<span
|
||||||
|
class="justify-self-end whitespace-nowrap text-right"
|
||||||
:class="[
|
:class="[
|
||||||
'justify-self-end whitespace-nowrap text-right',
|
|
||||||
hasPositiveTokens(getRecordCacheReadTokens(record)) ? 'text-foreground/70' : ''
|
hasPositiveTokens(getRecordCacheReadTokens(record)) ? 'text-foreground/70' : ''
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -693,8 +700,8 @@
|
|||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
class="justify-self-start whitespace-nowrap text-left"
|
||||||
:class="[
|
:class="[
|
||||||
'justify-self-start whitespace-nowrap text-left',
|
|
||||||
hasPositiveTokens(getRecordCacheCreationTokens(record)) ? 'text-foreground/70' : ''
|
hasPositiveTokens(getRecordCacheCreationTokens(record)) ? 'text-foreground/70' : ''
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ describe('parseDateLike', () => {
|
|||||||
describe('datetime-local conversion', () => {
|
describe('datetime-local conversion', () => {
|
||||||
it('formats RFC3339 instants for datetime-local inputs using local clock fields', () => {
|
it('formats RFC3339 instants for datetime-local inputs using local clock fields', () => {
|
||||||
const date = new Date('2026-04-12T15:30:00Z')
|
const date = new Date('2026-04-12T15:30:00Z')
|
||||||
const expected = [
|
const expected = `${[
|
||||||
date.getFullYear(),
|
date.getFullYear(),
|
||||||
padDatePart(date.getMonth() + 1),
|
padDatePart(date.getMonth() + 1),
|
||||||
padDatePart(date.getDate()),
|
padDatePart(date.getDate()),
|
||||||
].join('-') + `T${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
].join('-') }T${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
||||||
|
|
||||||
expect(formatDateTimeLocalInput('2026-04-12T15:30:00Z')).toBe(expected)
|
expect(formatDateTimeLocalInput('2026-04-12T15:30:00Z')).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -511,7 +511,7 @@
|
|||||||
<span>用户: {{ selectedTask.username }}</span>
|
<span>用户: {{ selectedTask.username }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span class="opacity-40">|</span>
|
<span class="opacity-40">|</span>
|
||||||
<span>{{ displayTaskSource(selectedTask) }}</span>
|
<span>{{ displayTaskSource(selectedTask) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 进度条 -->
|
<!-- 进度条 -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -41,12 +41,19 @@
|
|||||||
:class="selectedType === '__new__' ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-muted'"
|
:class="selectedType === '__new__' ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-muted'"
|
||||||
@click="selectNewConfig()"
|
@click="selectNewConfig()"
|
||||||
>
|
>
|
||||||
<div class="w-7 h-7 rounded-md flex items-center justify-center text-xs font-semibold shrink-0"
|
<div
|
||||||
|
class="w-7 h-7 rounded-md flex items-center justify-center text-xs font-semibold shrink-0"
|
||||||
:class="selectedType === '__new__' ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'"
|
:class="selectedType === '__new__' ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'"
|
||||||
>+</div>
|
>
|
||||||
|
+
|
||||||
|
</div>
|
||||||
<div class="flex-1 min-w-0 text-left">
|
<div class="flex-1 min-w-0 text-left">
|
||||||
<div class="truncate font-medium text-sm">新配置</div>
|
<div class="truncate font-medium text-sm">
|
||||||
<div class="text-[10px] text-muted-foreground">未保存</div>
|
新配置
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-muted-foreground">
|
||||||
|
未保存
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -70,10 +77,12 @@
|
|||||||
src="https://cdn.linux.do/uploads/default/optimized/3X/9/d/9dd49731091ce8656243f3c2b6e5d5e5a7e3e3e3_2_32x32.png"
|
src="https://cdn.linux.do/uploads/default/optimized/3X/9/d/9dd49731091ce8656243f3c2b6e5d5e5a7e3e3e3_2_32x32.png"
|
||||||
class="absolute inset-0 w-full h-full object-cover"
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
@error="($event.target as HTMLImageElement).remove()"
|
@error="($event.target as HTMLImageElement).remove()"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0 text-left">
|
<div class="flex-1 min-w-0 text-left">
|
||||||
<div class="truncate font-medium text-sm">{{ item.display_name }}</div>
|
<div class="truncate font-medium text-sm">
|
||||||
|
{{ item.display_name }}
|
||||||
|
</div>
|
||||||
<div class="text-[10px] text-muted-foreground">
|
<div class="text-[10px] text-muted-foreground">
|
||||||
{{ item.configured ? (item.is_enabled ? '已启用' : '已禁用') : '未配置' }}
|
{{ item.configured ? (item.is_enabled ? '已启用' : '已禁用') : '未配置' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -96,262 +105,261 @@
|
|||||||
|
|
||||||
<!-- 右侧内容区 -->
|
<!-- 右侧内容区 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
|
<!-- 配置表单 -->
|
||||||
|
<CardSection
|
||||||
|
v-if="selectedType"
|
||||||
|
:title="selectedType === '__new__' ? '新建配置' : (selectedTypeMeta?.display_name || selectedType)"
|
||||||
|
:description="selectedType === '__new__' ? '填写后点击保存' : (configs[selectedType]?.is_enabled ? '已启用' : '已禁用')"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="saving || testing"
|
||||||
|
@click="handleTest"
|
||||||
|
>
|
||||||
|
{{ testing ? '测试中...' : '测试' }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 配置表单 -->
|
<div class="space-y-6">
|
||||||
<CardSection
|
<!-- 新建时的 Display Name -->
|
||||||
v-if="selectedType"
|
<div
|
||||||
:title="selectedType === '__new__' ? '新建配置' : (selectedTypeMeta?.display_name || selectedType)"
|
v-if="selectedType === '__new__'"
|
||||||
:description="selectedType === '__new__' ? '填写后点击保存' : (configs[selectedType]?.is_enabled ? '已启用' : '已禁用')"
|
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
:disabled="saving || testing"
|
|
||||||
@click="handleTest"
|
|
||||||
>
|
>
|
||||||
{{ testing ? '测试中...' : '测试' }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
:disabled="saving"
|
|
||||||
@click="handleSave"
|
|
||||||
>
|
|
||||||
{{ saving ? '保存中...' : '保存' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- 新建时的 Display Name -->
|
|
||||||
<div
|
|
||||||
v-if="selectedType === '__new__'"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">显示名称</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.new_display_name"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="例如:My OIDC Provider"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">配置标识</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.new_provider_type"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="custom_oidc_work"
|
|
||||||
autocomplete="off"
|
|
||||||
@blur="normalizeNewProviderType"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 凭证配置 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Client ID</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.client_id"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="client_id"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.client_secret"
|
|
||||||
masked
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 回调地址 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.redirect_uri"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.frontend_callback_url"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="http://localhost:5173/auth/callback"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- custom_oidc 必填端点 -->
|
|
||||||
<div
|
|
||||||
v-if="isSelectedCustomProvider"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.authorization_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/oauth/authorize"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Token URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.token_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/oauth/token"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.userinfo_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
placeholder="https://example.com/api/user"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 高级选项(折叠) -->
|
|
||||||
<details class="group">
|
|
||||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
高级选项
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
|
||||||
<div>
|
<div>
|
||||||
<Label class="block text-sm font-medium">Scopes</Label>
|
<Label class="block text-sm font-medium">显示名称</Label>
|
||||||
<Input
|
<Input
|
||||||
v-model="form.scopes_input"
|
v-model="form.new_display_name"
|
||||||
class="mt-1"
|
class="mt-1"
|
||||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
placeholder="例如:My OIDC Provider"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<p class="mt-1 text-xs text-muted-foreground">
|
|
||||||
空格/逗号分隔;留空使用默认值
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- linuxdo 的可选端点覆盖 -->
|
<Label class="block text-sm font-medium">配置标识</Label>
|
||||||
<div
|
<Input
|
||||||
v-if="!isSelectedCustomProvider"
|
v-model="form.new_provider_type"
|
||||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
class="mt-1"
|
||||||
>
|
placeholder="custom_oidc_work"
|
||||||
<div>
|
autocomplete="off"
|
||||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
@blur="normalizeNewProviderType"
|
||||||
<Input
|
/>
|
||||||
v-model="form.authorization_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Token URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.token_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="form.userinfo_url_override"
|
|
||||||
class="mt-1"
|
|
||||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- 凭证配置 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Client ID</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.client_id"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="client_id"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.client_secret"
|
||||||
|
masked
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回调地址 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.redirect_uri"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.frontend_callback_url"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="http://localhost:5173/auth/callback"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- custom_oidc 必填端点 -->
|
||||||
|
<div
|
||||||
|
v-if="isSelectedCustomProvider"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.authorization_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/oauth/authorize"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Token URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.token_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/oauth/token"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.userinfo_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
placeholder="https://example.com/api/user"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 高级选项(折叠) -->
|
||||||
|
<details class="group">
|
||||||
|
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
高级选项
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||||
<div>
|
<div>
|
||||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
<Label class="block text-sm font-medium">Scopes</Label>
|
||||||
<Textarea
|
<Input
|
||||||
v-model="form.attribute_mapping_json"
|
v-model="form.scopes_input"
|
||||||
class="mt-1 font-mono text-xs"
|
class="mt-1"
|
||||||
rows="3"
|
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||||
placeholder="{"id": "user_id", "username": "login"}"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
<div>
|
空格/逗号分隔;留空使用默认值
|
||||||
<Label class="block text-sm font-medium">
|
|
||||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
v-model="form.extra_config_json"
|
|
||||||
class="mt-1 font-mono text-xs"
|
|
||||||
rows="3"
|
|
||||||
:placeholder="extraConfigPlaceholder"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
v-if="isSelectedCustomProvider"
|
|
||||||
class="mt-1 text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 测试结果 -->
|
<!-- linuxdo 的可选端点覆盖 -->
|
||||||
<div
|
<div
|
||||||
v-if="lastTestResult"
|
v-if="!isSelectedCustomProvider"
|
||||||
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||||
>
|
>
|
||||||
<div class="font-medium mb-2">
|
<div>
|
||||||
测试结果
|
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.authorization_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Token URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.token_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||||
|
<Input
|
||||||
|
v-model="form.userinfo_url_override"
|
||||||
|
class="mt-1"
|
||||||
|
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||||
|
<Textarea
|
||||||
|
v-model="form.attribute_mapping_json"
|
||||||
|
class="mt-1 font-mono text-xs"
|
||||||
|
rows="3"
|
||||||
|
placeholder="{"id": "user_id", "username": "login"}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="block text-sm font-medium">
|
||||||
|
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
v-model="form.extra_config_json"
|
||||||
|
class="mt-1 font-mono text-xs"
|
||||||
|
rows="3"
|
||||||
|
:placeholder="extraConfigPlaceholder"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="isSelectedCustomProvider"
|
||||||
|
class="mt-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-4 text-xs">
|
|
||||||
<div class="flex items-center gap-2">
|
<!-- 测试结果 -->
|
||||||
<span
|
<div
|
||||||
class="w-2 h-2 rounded-full"
|
v-if="lastTestResult"
|
||||||
:class="lastTestResult.authorization_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
class="mt-6 rounded-lg border border-border p-4 text-sm"
|
||||||
/>
|
>
|
||||||
<span class="text-muted-foreground">Authorization URL</span>
|
<div class="font-medium mb-2">
|
||||||
|
测试结果
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap gap-4 text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="lastTestResult.authorization_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||||
|
/>
|
||||||
|
<span class="text-muted-foreground">Authorization URL</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="lastTestResult.token_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
||||||
|
/>
|
||||||
|
<span class="text-muted-foreground">Token URL</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="lastTestResult.secret_status === 'likely_valid' ? 'bg-green-500' : lastTestResult.secret_status === 'invalid' ? 'bg-red-500' : 'bg-yellow-500'"
|
||||||
|
/>
|
||||||
|
<span class="text-muted-foreground">Secret: {{ lastTestResult.secret_status }}</span>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
class="w-2 h-2 rounded-full"
|
v-if="lastTestResult.details"
|
||||||
:class="lastTestResult.token_url_reachable ? 'bg-green-500' : 'bg-red-500'"
|
class="text-muted-foreground"
|
||||||
/>
|
>
|
||||||
<span class="text-muted-foreground">Token URL</span>
|
{{ lastTestResult.details }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="w-2 h-2 rounded-full"
|
|
||||||
:class="lastTestResult.secret_status === 'likely_valid' ? 'bg-green-500' : lastTestResult.secret_status === 'invalid' ? 'bg-red-500' : 'bg-yellow-500'"
|
|
||||||
/>
|
|
||||||
<span class="text-muted-foreground">Secret: {{ lastTestResult.secret_status }}</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="lastTestResult.details"
|
|
||||||
class="text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ lastTestResult.details }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardSection>
|
||||||
</CardSection>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -822,7 +822,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
<div>API 端点</div>
|
<div>API 端点</div>
|
||||||
<div>推理程度</div>
|
<div>推理程度</div>
|
||||||
<div>映射参数</div>
|
<div>映射参数</div>
|
||||||
<div class="md:text-right">状态</div>
|
<div class="md:text-right">
|
||||||
|
状态
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y">
|
<div class="divide-y">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -50,15 +50,15 @@
|
|||||||
>
|
>
|
||||||
<UsageModelTable
|
<UsageModelTable
|
||||||
:data="enhancedModelStats"
|
:data="enhancedModelStats"
|
||||||
:is-admin="authStore.canAccessAdmin"
|
:is-admin="authStore.canAccessAdmin"
|
||||||
/>
|
/>
|
||||||
<UsageProviderTable
|
<UsageProviderTable
|
||||||
:data="providerStats"
|
:data="providerStats"
|
||||||
:is-admin="authStore.canAccessAdmin"
|
:is-admin="authStore.canAccessAdmin"
|
||||||
/>
|
/>
|
||||||
<UsageApiFormatTable
|
<UsageApiFormatTable
|
||||||
:data="apiFormatStats"
|
:data="apiFormatStats"
|
||||||
:is-admin="authStore.canAccessAdmin"
|
:is-admin="authStore.canAccessAdmin"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 用户:模型 + API格式(2列) -->
|
<!-- 用户:模型 + API格式(2列) -->
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
>
|
>
|
||||||
<UsageModelTable
|
<UsageModelTable
|
||||||
:data="enhancedModelStats"
|
:data="enhancedModelStats"
|
||||||
:is-admin="authStore.canAccessAdmin"
|
:is-admin="authStore.canAccessAdmin"
|
||||||
/>
|
/>
|
||||||
<UsageApiFormatTable
|
<UsageApiFormatTable
|
||||||
:data="apiFormatStats"
|
:data="apiFormatStats"
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<UsageRecordsTable
|
<UsageRecordsTable
|
||||||
:records="displayRecords"
|
:records="displayRecords"
|
||||||
:is-admin="isAdminPage"
|
:is-admin="isAdminPage"
|
||||||
:show-actual-cost="authStore.canAccessAdmin"
|
:show-actual-cost="authStore.canAccessAdmin"
|
||||||
:loading="isLoadingRecords"
|
:loading="isLoadingRecords"
|
||||||
:time-range="timeRange"
|
:time-range="timeRange"
|
||||||
:filter-search="filterSearch"
|
:filter-search="filterSearch"
|
||||||
|
|||||||
Reference in New Issue
Block a user