/** * Pack 详情页面 * * 查看 Pack 详情并应用到本地配置 */ import { useState, useEffect, useCallback } from 'react' import { useNavigate } from '@tanstack/react-router' import { packDetailRoute } from '@/router' import { Package, ArrowLeft, Download, Heart, Clock, User, Server, Layers, ListChecks, Tag, Check, AlertTriangle, Info, ChevronRight, Key, Settings, Loader2, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Skeleton } from '@/components/ui/skeleton' import { ScrollArea } from '@/components/ui/scroll-area' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Separator } from '@/components/ui/separator' import { toast } from '@/hooks/use-toast' import { getPack, recordPackDownload, togglePackLike, checkPackLike, detectPackConflicts, applyPack, getPackUserId, type ModelPack, type ApplyPackOptions, type ApplyPackConflicts, } from '@/lib/pack-api' // 任务类型名称映射 const TASK_TYPE_NAMES: Record = { utils: '通用工具', utils_small: '轻量工具', tool_use: '工具调用', replyer: '回复生成', planner: '规划推理', vlm: '视觉模型', voice: '语音处理', embedding: '向量嵌入', lpmm_entity_extract: '实体提取', lpmm_rdf_build: 'RDF构建', lpmm_qa: '问答模型', } export default function PackDetailPage() { const { packId } = packDetailRoute.useParams() const navigate = useNavigate() const [pack, setPack] = useState(null) const [loading, setLoading] = useState(true) const [liked, setLiked] = useState(false) const [liking, setLiking] = useState(false) // 应用向导状态 const [showApplyDialog, setShowApplyDialog] = useState(false) const [applyStep, setApplyStep] = useState(1) const [conflicts, setConflicts] = useState(null) const [detectingConflicts, setDetectingConflicts] = useState(false) const [applying, setApplying] = useState(false) // 应用选项 const [applyOptions, setApplyOptions] = useState({ apply_providers: true, apply_models: true, apply_task_config: true, task_mode: 'append', selected_providers: undefined, selected_models: undefined, selected_tasks: undefined, }) // 提供商映射和 API Key const [providerMapping, setProviderMapping] = useState>({}) const [newProviderApiKeys, setNewProviderApiKeys] = useState>({}) const userId = getPackUserId() // 加载 Pack const loadPack = useCallback(async () => { if (!packId) return setLoading(true) try { const data = await getPack(packId) setPack(data) const isLiked = await checkPackLike(packId, userId) setLiked(isLiked) } catch (error) { console.error('加载 Pack 失败:', error) toast({ title: '加载模板失败', variant: 'destructive' }) } finally { setLoading(false) } }, [packId, userId]) useEffect(() => { loadPack() }, [loadPack]) // 点赞 const handleLike = async () => { if (!packId || liking) return setLiking(true) try { const result = await togglePackLike(packId, userId) setLiked(result.liked) if (pack) { setPack({ ...pack, likes: result.likes }) } } catch (error) { console.error('点赞失败:', error) toast({ title: '点赞失败', variant: 'destructive' }) } finally { setLiking(false) } } // 开始应用流程 const startApply = async () => { if (!pack) return setShowApplyDialog(true) setApplyStep(1) setDetectingConflicts(true) try { const detected = await detectPackConflicts(pack) setConflicts(detected) // 初始化提供商映射(已存在的提供商默认使用第一个匹配的本地提供商) const mapping: Record = {} for (const c of detected.existing_providers) { mapping[c.pack_provider.name] = c.local_providers[0].name } setProviderMapping(mapping) // 初始化新提供商的 API Key const keys: Record = {} for (const p of detected.new_providers) { keys[p.name] = '' } setNewProviderApiKeys(keys) } catch (error) { console.error('检测冲突失败:', error) toast({ title: '检测配置冲突失败', variant: 'destructive' }) setShowApplyDialog(false) } finally { setDetectingConflicts(false) } } // 执行应用 const executeApply = async () => { if (!pack) return // 验证新提供商都有 API Key if (applyOptions.apply_providers && conflicts) { for (const p of conflicts.new_providers) { if (!newProviderApiKeys[p.name]) { toast({ title: `请填写提供商 "${p.name}" 的 API Key`, variant: 'destructive' }) return } } } setApplying(true) try { await applyPack(pack, applyOptions, providerMapping, newProviderApiKeys) // 记录下载 await recordPackDownload(pack.id, userId) // 更新下载数 setPack({ ...pack, downloads: pack.downloads + 1 }) toast({ title: '配置模板应用成功!' }) setShowApplyDialog(false) } catch (error) { console.error('应用 Pack 失败:', error) toast({ title: error instanceof Error ? error.message : '应用配置失败', variant: 'destructive' }) } finally { setApplying(false) } } // 格式化日期 const formatDate = (dateStr: string) => { const date = new Date(dateStr) return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', }) } if (loading) { return } if (!pack) { return (

模板不存在

该配置模板可能已被删除或尚未通过审核

) } return (
{/* 返回按钮 */} {/* 头部信息 */}

{pack.name} v{pack.version}

{pack.description}

{/* 元信息 */}
{pack.author} {formatDate(pack.created_at)} {pack.downloads} 次下载 {pack.likes} 赞
{/* 标签 */} {pack.tags && pack.tags.length > 0 && (
{pack.tags.map(tag => ( {tag} ))}
)}
{/* 操作按钮 */}
{/* 内容统计 */}

{pack.providers.length}

API 提供商

{pack.models.length}

模型配置

{Object.keys(pack.task_config).length}

任务配置

{/* 详细内容 */} 提供商 提供商 ({pack.providers.length}) 模型 模型 ({pack.models.length}) 任务配置 任务 ({Object.keys(pack.task_config).length}) API 提供商 模板中包含的 API 提供商配置(不含 API Key)
名称 Base URL 类型 {pack.providers.map(provider => ( {provider.name} {provider.base_url} {provider.client_type} ))}
模型配置 模板中包含的模型配置
模型名称 标识符 提供商 价格 (入/出) {pack.models.map(model => ( {model.name} {model.model_identifier} {model.api_provider} ¥{model.price_in} / ¥{model.price_out} ))}
任务配置 模板中各任务类型的模型分配 {Object.entries(pack.task_config).map(([taskKey, config]) => (
{TASK_TYPE_NAMES[taskKey] || taskKey} {config.model_list.length} 个模型
分配的模型:
{config.model_list.map((model: string) => ( {model} ))}
{config.temperature !== undefined && (
Temperature: {config.temperature}
)} {config.max_tokens !== undefined && (
Max Tokens: {config.max_tokens}
)}
))}
{/* 应用向导对话框 */}
) } // 应用向导对话框 function ApplyDialog({ open, onOpenChange, pack, step, setStep, conflicts, detectingConflicts, applying, options, setOptions, _providerMapping, _setProviderMapping, newProviderApiKeys, setNewProviderApiKeys, onApply, }: { open: boolean onOpenChange: (open: boolean) => void pack: ModelPack step: number setStep: (step: number) => void conflicts: ApplyPackConflicts | null detectingConflicts: boolean applying: boolean options: ApplyPackOptions setOptions: (options: ApplyPackOptions) => void _providerMapping: Record _setProviderMapping: (mapping: Record) => void newProviderApiKeys: Record setNewProviderApiKeys: (keys: Record) => void onApply: () => void }) { const totalSteps = 3 return ( 应用配置模板 步骤 {step} / {totalSteps}: {step === 1 && '选择要应用的内容'} {step === 2 && '配置提供商映射'} {step === 3 && '确认并应用'} {detectingConflicts ? (

正在检测配置冲突...

) : ( <> {/* 步骤 1: 选择内容 */} {step === 1 && (
setOptions({ ...options, apply_providers: checked as boolean }) } />
setOptions({ ...options, apply_models: checked as boolean }) } />
setOptions({ ...options, apply_task_config: checked as boolean }) } />
{options.apply_task_config && (
setOptions({ ...options, task_mode: value as 'replace' | 'append' }) } >
)}
)} {/* 步骤 2: 提供商映射 */} {step === 2 && conflicts && (
{/* 已存在的提供商 */} {options.apply_providers && conflicts.existing_providers.length > 0 && (
发现已有的提供商 以下提供商的 URL 与您本地配置中的提供商匹配,将自动使用本地提供商:
{conflicts.existing_providers.map(({ pack_provider, local_providers }) => (
{pack_provider.name} {local_providers.length === 1 ? ( <> {local_providers[0].name} URL 匹配 ) : ( <> {local_providers.length} 个匹配 )}
))}
)} {/* 新提供商 */} {options.apply_providers && conflicts.new_providers.length > 0 && (
需要配置 API Key 以下提供商在您的本地配置中不存在,需要填写 API Key:
{conflicts.new_providers.map(provider => (
{provider.name} ({provider.base_url})
setNewProviderApiKeys({ ...newProviderApiKeys, [provider.name]: e.target.value, }) } />
))}
)} {(!options.apply_providers || (conflicts.existing_providers.length === 0 && conflicts.new_providers.length === 0)) && ( 无需配置 模板中没有提供商配置,或您选择不应用提供商。 )}
)} {/* 步骤 3: 确认 */} {step === 3 && (
确认应用 请确认以下将要应用的内容:
{options.apply_providers && (
应用 {pack.providers.length} 个提供商配置
)} {options.apply_models && (
应用 {pack.models.length} 个模型配置
)} {options.apply_task_config && (
{options.task_mode === 'append' ? '追加' : '替换'} {Object.keys(pack.task_config).length} 个任务配置
)}
{conflicts && conflicts.new_providers.length > 0 && ( 将添加 {conflicts.new_providers.length} 个新提供商,请确保已填写正确的 API Key。 )}
)} )}
{step > 1 && !detectingConflicts && ( )}
{step < totalSteps ? ( ) : ( )}
) } // 加载骨架 function PackDetailSkeleton() { return (
{/* 返回按钮 */} {/* 头部信息 */}
{/* 元信息 */}
{/* 标签 */}
{/* 操作按钮 */}
{/* 内容统计卡片 */}
{/* Tabs */}
) }