Files
sub2api/frontend/src/components/common/GroupBadge.vue
yangjianbo 13262a5698 feat(sora): 新增 Sora 平台支持并修复高危安全和性能问题
新增功能:
- 新增 Sora 账号管理和 OAuth 认证
- 新增 Sora 视频/图片生成 API 网关
- 新增 Sora 任务调度和缓存机制
- 新增 Sora 使用统计和计费支持
- 前端增加 Sora 平台配置界面

安全修复(代码审核):
- [SEC-001] 限制媒体下载响应体大小(图片 20MB、视频 200MB),防止 DoS 攻击
- [SEC-002] 限制 SDK API 响应大小(1MB),防止内存耗尽
- [SEC-003] 修复 SSRF 风险,添加 URL 验证并强制使用代理配置

BUG 修复(代码审核):
- [BUG-001] 修复 for 循环内 defer 累积导致的资源泄漏
- [BUG-002] 修复图片并发槽位获取失败时已持有锁未释放的永久泄漏

性能优化(代码审核):
- [PERF-001] 添加 Sentinel Token 缓存(3 分钟有效期),减少 PoW 计算开销

技术细节:
- 使用 io.LimitReader 限制所有外部输入的大小
- 添加 urlvalidator 验证防止 SSRF 攻击
- 使用 sync.Map 实现线程安全的包级缓存
- 优化并发槽位管理,添加 releaseAll 模式防止泄漏

影响范围:
- 后端:新增 Sora 相关数据模型、服务、网关和管理接口
- 前端:新增 Sora 平台配置、账号管理和监控界面
- 配置:新增 Sora 相关配置项和环境变量

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 16:18:38 +08:00

135 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<span
:class="[
'inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium transition-colors',
badgeClass
]"
>
<!-- Platform logo -->
<PlatformIcon v-if="platform" :platform="platform" size="sm" />
<!-- Group name -->
<span class="truncate">{{ name }}</span>
<!-- Right side label -->
<span v-if="showLabel" :class="labelClass">
{{ labelText }}
</span>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { SubscriptionType, GroupPlatform } from '@/types'
import PlatformIcon from './PlatformIcon.vue'
interface Props {
name: string
platform?: GroupPlatform
subscriptionType?: SubscriptionType
rateMultiplier?: number
showRate?: boolean
daysRemaining?: number | null // 剩余天数(订阅类型时使用)
}
const props = withDefaults(defineProps<Props>(), {
subscriptionType: 'standard',
showRate: true,
daysRemaining: null
})
const { t } = useI18n()
const isSubscription = computed(() => props.subscriptionType === 'subscription')
// 是否显示右侧标签
const showLabel = computed(() => {
if (!props.showRate) return false
// 订阅类型:显示天数或"订阅"
if (isSubscription.value) return true
// 标准类型:显示倍率
return props.rateMultiplier !== undefined
})
// Label text
const labelText = computed(() => {
if (isSubscription.value) {
// 如果有剩余天数,显示天数
if (props.daysRemaining !== null && props.daysRemaining !== undefined) {
if (props.daysRemaining <= 0) {
return t('admin.users.expired')
}
return t('admin.users.daysRemaining', { days: props.daysRemaining })
}
// 否则显示"订阅"
return t('groups.subscription')
}
return props.rateMultiplier !== undefined ? `${props.rateMultiplier}x` : ''
})
// Label style based on type and days remaining
const labelClass = computed(() => {
const base = 'px-1.5 py-0.5 rounded text-[10px] font-semibold'
if (!isSubscription.value) {
// Standard: subtle background
return `${base} bg-black/10 dark:bg-white/10`
}
// 订阅类型:根据剩余天数显示不同颜色
if (props.daysRemaining !== null && props.daysRemaining !== undefined) {
if (props.daysRemaining <= 0 || props.daysRemaining <= 3) {
// 已过期或紧急(<=3天红色
return `${base} bg-red-200/80 text-red-800 dark:bg-red-800/50 dark:text-red-300`
}
if (props.daysRemaining <= 7) {
// 警告(<=7天橙色
return `${base} bg-amber-200/80 text-amber-800 dark:bg-amber-800/50 dark:text-amber-300`
}
}
// 正常状态或无天数:根据平台显示主题色
if (props.platform === 'anthropic') {
return `${base} bg-orange-200/60 text-orange-800 dark:bg-orange-800/40 dark:text-orange-300`
}
if (props.platform === 'openai') {
return `${base} bg-emerald-200/60 text-emerald-800 dark:bg-emerald-800/40 dark:text-emerald-300`
}
if (props.platform === 'gemini') {
return `${base} bg-blue-200/60 text-blue-800 dark:bg-blue-800/40 dark:text-blue-300`
}
if (props.platform === 'sora') {
return `${base} bg-rose-200/60 text-rose-800 dark:bg-rose-800/40 dark:text-rose-300`
}
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
})
// Badge color based on platform and subscription type
const badgeClass = computed(() => {
if (props.platform === 'anthropic') {
// Claude: orange theme
return isSubscription.value
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400'
} else if (props.platform === 'openai') {
// OpenAI: green theme
return isSubscription.value
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
}
if (props.platform === 'gemini') {
return isSubscription.value
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
}
if (props.platform === 'sora') {
return isSubscription.value
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
: 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
}
// Fallback: original colors
return isSubscription.value
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
})
</script>