feat: Provider 异步删除、可配置密码策略、Hub 超时优化及多项改进

- 新增 Provider 异步删除任务系统,后台分阶段删除子资源并清理残留引用
- 新增可配置密码策略等级(weak/medium/strong),支持系统设置面板调整
- aether-hub 升级至 0.1.4,idle timeout 支持禁用(设为 0),worker 默认超时调整为 120s
- OAuth 手动续期增加 Redis 分布式锁,防止并发刷新冲突
- ProxyNode 心跳检测改为 asyncio.to_thread,避免阻塞事件循环
- 删除 ModelMultiSelect 和 useInvalidModels,MultiSelect 组件通用化
- 明确 allowed_providers/allowed_api_formats 的 NULL 与空数组语义
- 前端 StandaloneKeyFormDialog、UserFormDialog 等多处 UI 优化
- 新增 Alembic 迁移脚本清理 Provider 删除后的残留引用
- 补充相关测试用例
This commit is contained in:
fawney19
2026-03-12 01:11:35 +08:00
parent 0d770d1c4d
commit 6e51a3f45d
55 changed files with 3219 additions and 862 deletions

View File

@@ -1,6 +1,6 @@
import type { OAuthOrganizationInfo } from '@/api/endpoints/types/provider'
export function formatOAuthIdentityShort(
function formatOAuthIdentityShort(
value: string | null | undefined,
head = 8,
tail = 6,
@@ -11,33 +11,24 @@ export function formatOAuthIdentityShort(
return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`
}
export function getPrimaryOAuthOrganizationTitle(
function getPrimaryOAuthOrganizationId(
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
): string | null {
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
const defaultOrg = organizations.find(
(org) => org?.is_default && typeof org?.title === 'string' && org.title.trim(),
(org) => org?.is_default && typeof org?.id === 'string' && org.id.trim(),
)
if (defaultOrg?.title) return defaultOrg.title.trim()
const firstWithTitle = organizations.find(
(org) => typeof org?.title === 'string' && org.title.trim(),
if (defaultOrg?.id) return defaultOrg.id.trim()
const firstWithId = organizations.find(
(org) => typeof org?.id === 'string' && org.id.trim(),
)
return firstWithTitle?.title?.trim() || null
return firstWithId?.id?.trim() || null
}
export function getOAuthOrganizationsTooltip(
export function getOAuthOrgBadge(
value: { oauth_organizations?: OAuthOrganizationInfo[] | null } | null | undefined,
): string {
const organizations = Array.isArray(value?.oauth_organizations) ? value.oauth_organizations : []
if (organizations.length === 0) return ''
return organizations
.map((org) => {
const title =
typeof org?.title === 'string' && org.title.trim() ? org.title.trim() : '未命名组织'
const role =
typeof org?.role === 'string' && org.role.trim() ? ` (${org.role.trim()})` : ''
const suffix = org?.is_default ? ' [default]' : ''
return `${title}${role}${suffix}`
})
.join('\n')
): { id: string; label: string } | null {
const id = getPrimaryOAuthOrganizationId(value)
if (!id) return null
return { id, label: formatOAuthIdentityShort(id) }
}

View File

@@ -0,0 +1,98 @@
export type PasswordPolicyLevel = 'weak' | 'medium' | 'strong'
export const PASSWORD_POLICY_OPTIONS: Array<{
value: PasswordPolicyLevel
label: string
description: string
}> = [
{
value: 'weak',
label: '弱密码',
description: '至少 6 个字符',
},
{
value: 'medium',
label: '中等密码',
description: '至少 8 个字符,且包含字母和数字',
},
{
value: 'strong',
label: '强密码',
description: '至少 8 个字符,且包含大小写字母、数字和特殊字符',
},
]
export function normalizePasswordPolicyLevel(value: unknown): PasswordPolicyLevel {
if (value === 'medium' || value === 'strong') {
return value
}
return 'weak'
}
export function getPasswordPolicyHint(level: unknown): string {
switch (normalizePasswordPolicyLevel(level)) {
case 'medium':
return '至少 8 个字符,且需包含字母和数字'
case 'strong':
return '至少 8 个字符,且需包含大写字母、小写字母、数字和特殊字符'
case 'weak':
default:
return '至少 6 个字符'
}
}
export function getPasswordPolicyPlaceholder(level: unknown): string {
switch (normalizePasswordPolicyLevel(level)) {
case 'medium':
return '至少 8 位,含字母和数字'
case 'strong':
return '至少 8 位,含大小写字母、数字和特殊字符'
case 'weak':
default:
return '至少 6 个字符'
}
}
export function validatePasswordByPolicy(password: string, level: unknown): string {
if (!password) {
return ''
}
const normalized = normalizePasswordPolicyLevel(level)
if (password.length < 6) {
return '密码长度至少为6个字符'
}
if (normalized === 'medium') {
if (password.length < 8) {
return '密码长度至少为8个字符'
}
if (!/[A-Za-z]/.test(password)) {
return '密码必须包含至少一个字母'
}
if (!/[0-9]/.test(password)) {
return '密码必须包含至少一个数字'
}
}
if (normalized === 'strong') {
if (password.length < 8) {
return '密码长度至少为8个字符'
}
if (!/[A-Z]/.test(password)) {
return '密码必须包含至少一个大写字母'
}
if (!/[a-z]/.test(password)) {
return '密码必须包含至少一个小写字母'
}
if (!/[0-9]/.test(password)) {
return '密码必须包含至少一个数字'
}
if (!/[!@#$%^&*()_+\-=[\]{};:'",.<>?/\\|`~]/.test(password)) {
return '密码必须包含至少一个特殊字符'
}
}
return ''
}