feat: 添加 AI 服务状态监控和警告功能
- AI 配置页面添加服务健康状态显示(正常/降级/异常) - 服务异常时显示红色警告卡片,展示错误信息和队列任务数 - 侧边栏 AI 配置入口添加红点警告徽章支持 - DesktopLayout 支持传递 aiServiceError 状态 AI 调用风险处理方案: - 自动重试 3 次 - 失败后加入队列,后台定时重试 - 页面显示服务状态警告 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b83d7e068c
commit
bbc8a4f641
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
@ -16,9 +16,22 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Info,
|
Info,
|
||||||
Shield,
|
Shield,
|
||||||
AlertTriangle
|
AlertTriangle,
|
||||||
|
RefreshCw,
|
||||||
|
Clock
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
// AI 服务状态类型
|
||||||
|
type ServiceStatus = 'healthy' | 'degraded' | 'error' | 'unknown'
|
||||||
|
|
||||||
|
interface AIServiceHealth {
|
||||||
|
status: ServiceStatus
|
||||||
|
lastChecked: string | null
|
||||||
|
lastError: string | null
|
||||||
|
failedCount: number // 连续失败次数
|
||||||
|
queuedTasks: number // 队列中等待的任务数
|
||||||
|
}
|
||||||
|
|
||||||
// AI 提供商选项
|
// AI 提供商选项
|
||||||
const providerOptions = [
|
const providerOptions = [
|
||||||
{ value: 'oneapi', label: 'OneAPI 中转服务' },
|
{ value: 'oneapi', label: 'OneAPI 中转服务' },
|
||||||
@ -73,6 +86,37 @@ export default function AIConfigPage() {
|
|||||||
asr: 'idle',
|
asr: 'idle',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// AI 服务健康状态(模拟数据,实际从后端获取)
|
||||||
|
const [serviceHealth, setServiceHealth] = useState<AIServiceHealth>({
|
||||||
|
status: 'healthy',
|
||||||
|
lastChecked: '2026-02-06 16:30:00',
|
||||||
|
lastError: null,
|
||||||
|
failedCount: 0,
|
||||||
|
queuedTasks: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟检查服务状态
|
||||||
|
const checkServiceHealth = async () => {
|
||||||
|
// 实际应该调用后端 API
|
||||||
|
// const response = await fetch('/api/v1/ai-config/health')
|
||||||
|
// setServiceHealth(await response.json())
|
||||||
|
|
||||||
|
// 模拟:随机返回不同状态用于演示
|
||||||
|
const now = new Date().toLocaleString('zh-CN')
|
||||||
|
setServiceHealth({
|
||||||
|
status: 'healthy',
|
||||||
|
lastChecked: now,
|
||||||
|
lastError: null,
|
||||||
|
failedCount: 0,
|
||||||
|
queuedTasks: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查服务状态
|
||||||
|
useEffect(() => {
|
||||||
|
checkServiceHealth()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
// 模拟测试连接
|
// 模拟测试连接
|
||||||
setTestResults({ llm: 'testing', vision: 'testing', asr: 'testing' })
|
setTestResults({ llm: 'testing', vision: 'testing', asr: 'testing' })
|
||||||
@ -105,6 +149,47 @@ export default function AIConfigPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取服务状态配置
|
||||||
|
const getServiceStatusConfig = (status: ServiceStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'healthy':
|
||||||
|
return {
|
||||||
|
label: '服务正常',
|
||||||
|
color: 'text-accent-green',
|
||||||
|
bgColor: 'bg-accent-green/15',
|
||||||
|
borderColor: 'border-accent-green/30',
|
||||||
|
icon: CheckCircle,
|
||||||
|
}
|
||||||
|
case 'degraded':
|
||||||
|
return {
|
||||||
|
label: '服务降级',
|
||||||
|
color: 'text-accent-amber',
|
||||||
|
bgColor: 'bg-accent-amber/15',
|
||||||
|
borderColor: 'border-accent-amber/30',
|
||||||
|
icon: AlertTriangle,
|
||||||
|
}
|
||||||
|
case 'error':
|
||||||
|
return {
|
||||||
|
label: '服务异常',
|
||||||
|
color: 'text-accent-coral',
|
||||||
|
bgColor: 'bg-accent-coral/15',
|
||||||
|
borderColor: 'border-accent-coral/30',
|
||||||
|
icon: XCircle,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
label: '状态未知',
|
||||||
|
color: 'text-text-tertiary',
|
||||||
|
bgColor: 'bg-bg-elevated',
|
||||||
|
borderColor: 'border-border-subtle',
|
||||||
|
icon: Info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig = getServiceStatusConfig(serviceHealth.status)
|
||||||
|
const StatusIcon = statusConfig.icon
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl">
|
<div className="space-y-6 max-w-4xl">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -112,7 +197,58 @@ export default function AIConfigPage() {
|
|||||||
<h1 className="text-2xl font-bold text-text-primary">AI 服务配置</h1>
|
<h1 className="text-2xl font-bold text-text-primary">AI 服务配置</h1>
|
||||||
<p className="text-sm text-text-secondary mt-1">配置 AI 服务提供商和模型参数</p>
|
<p className="text-sm text-text-secondary mt-1">配置 AI 服务提供商和模型参数</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 服务状态标签 */}
|
||||||
|
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg ${statusConfig.bgColor} border ${statusConfig.borderColor}`}>
|
||||||
|
<StatusIcon size={16} className={statusConfig.color} />
|
||||||
|
<span className={`text-sm font-medium ${statusConfig.color}`}>{statusConfig.label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 服务异常警告 */}
|
||||||
|
{(serviceHealth.status === 'error' || serviceHealth.status === 'degraded') && (
|
||||||
|
<div className={`p-4 rounded-lg border ${serviceHealth.status === 'error' ? 'bg-accent-coral/10 border-accent-coral/30' : 'bg-accent-amber/10 border-accent-amber/30'}`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle size={20} className={serviceHealth.status === 'error' ? 'text-accent-coral' : 'text-accent-amber'} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-medium ${serviceHealth.status === 'error' ? 'text-accent-coral' : 'text-accent-amber'}`}>
|
||||||
|
{serviceHealth.status === 'error' ? 'AI 服务异常' : 'AI 服务降级'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-text-secondary mt-1">
|
||||||
|
{serviceHealth.lastError || '部分 AI 功能可能不可用,系统已自动将任务加入重试队列。'}
|
||||||
|
</p>
|
||||||
|
{serviceHealth.queuedTasks > 0 && (
|
||||||
|
<p className="text-sm text-text-tertiary mt-1">
|
||||||
|
当前有 <span className="font-medium text-text-primary">{serviceHealth.queuedTasks}</span> 个任务在队列中等待重试
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{serviceHealth.failedCount > 0 && (
|
||||||
|
<p className="text-sm text-text-tertiary mt-1">
|
||||||
|
连续失败 <span className="font-medium text-text-primary">{serviceHealth.failedCount}</span> 次
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button variant="secondary" size="sm" onClick={checkServiceHealth}>
|
||||||
|
<RefreshCw size={14} />
|
||||||
|
刷新状态
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 最后检查时间 */}
|
||||||
|
{serviceHealth.lastChecked && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||||
|
<Clock size={12} />
|
||||||
|
<span>最后检查时间: {serviceHealth.lastChecked}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={checkServiceHealth}
|
||||||
|
className="text-accent-indigo hover:underline"
|
||||||
|
>
|
||||||
|
立即检查
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 配置继承提示 */}
|
{/* 配置继承提示 */}
|
||||||
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
||||||
|
|||||||
@ -6,16 +6,18 @@ interface DesktopLayoutProps {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
role?: 'creator' | 'agency' | 'brand'
|
role?: 'creator' | 'agency' | 'brand'
|
||||||
className?: string
|
className?: string
|
||||||
|
aiServiceError?: boolean // AI 服务异常状态(仅品牌方使用)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopLayout({
|
export function DesktopLayout({
|
||||||
children,
|
children,
|
||||||
role = 'creator',
|
role = 'creator',
|
||||||
className = '',
|
className = '',
|
||||||
|
aiServiceError = false,
|
||||||
}: DesktopLayoutProps) {
|
}: DesktopLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen bg-bg-page flex overflow-hidden ${className}`}>
|
<div className={`h-screen bg-bg-page flex overflow-hidden ${className}`}>
|
||||||
<Sidebar role={role} />
|
<Sidebar role={role} aiServiceError={role === 'brand' ? aiServiceError : false} />
|
||||||
<main className="flex-1 ml-[260px] p-8 overflow-y-auto overflow-x-hidden">
|
<main className="flex-1 ml-[260px] p-8 overflow-y-auto overflow-x-hidden">
|
||||||
<div className="min-h-full">
|
<div className="min-h-full">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ interface NavItem {
|
|||||||
icon: React.ElementType
|
icon: React.ElementType
|
||||||
label: string
|
label: string
|
||||||
href: string
|
href: string
|
||||||
|
badge?: 'dot' | 'warning' | number // 支持红点、警告或数字徽章
|
||||||
}
|
}
|
||||||
|
|
||||||
// 达人端导航项
|
// 达人端导航项
|
||||||
@ -59,16 +60,27 @@ const brandNavItems: NavItem[] = [
|
|||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
role?: 'creator' | 'agency' | 'brand'
|
role?: 'creator' | 'agency' | 'brand'
|
||||||
|
aiServiceError?: boolean // AI 服务是否异常
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ role = 'creator' }: SidebarProps) {
|
export function Sidebar({ role = 'creator', aiServiceError = false }: SidebarProps) {
|
||||||
const pathname = usePathname() || ''
|
const pathname = usePathname() || ''
|
||||||
|
|
||||||
|
// 根据 aiServiceError 动态设置 AI 配置的徽章
|
||||||
|
const getBrandNavItems = (): NavItem[] => {
|
||||||
|
return brandNavItems.map(item => {
|
||||||
|
if (item.href === '/brand/ai-config' && aiServiceError) {
|
||||||
|
return { ...item, badge: 'warning' as const }
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const navItems = role === 'creator'
|
const navItems = role === 'creator'
|
||||||
? creatorNavItems
|
? creatorNavItems
|
||||||
: role === 'agency'
|
: role === 'agency'
|
||||||
? agencyNavItems
|
? agencyNavItems
|
||||||
: brandNavItems
|
: getBrandNavItems()
|
||||||
|
|
||||||
const isActive = (href: string) => {
|
const isActive = (href: string) => {
|
||||||
if (href === `/${role}`) {
|
if (href === `/${role}`) {
|
||||||
@ -105,8 +117,24 @@ export function Sidebar({ role = 'creator' }: SidebarProps) {
|
|||||||
: 'text-text-secondary hover:bg-bg-elevated/50'
|
: 'text-text-secondary hover:bg-bg-elevated/50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div className="relative">
|
||||||
<Icon className="w-5 h-5" />
|
<Icon className="w-5 h-5" />
|
||||||
<span className="text-[15px]">{item.label}</span>
|
{/* 警告徽章 */}
|
||||||
|
{item.badge === 'warning' && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-accent-coral rounded-full border-2 border-bg-card animate-pulse" />
|
||||||
|
)}
|
||||||
|
{/* 红点徽章 */}
|
||||||
|
{item.badge === 'dot' && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-accent-coral rounded-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[15px] flex-1">{item.label}</span>
|
||||||
|
{/* 数字徽章 */}
|
||||||
|
{typeof item.badge === 'number' && item.badge > 0 && (
|
||||||
|
<span className="px-1.5 py-0.5 text-xs bg-accent-coral text-white rounded-full min-w-[20px] text-center">
|
||||||
|
{item.badge > 99 ? '99+' : item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user