import { useState, useEffect } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { ArrowLeft, Download, ExternalLink, CheckCircle2, AlertCircle, Loader2, Trash2, RefreshCw, User, Package, Shield, Globe, Tag, GitBranch, Info, } from 'lucide-react' import { useToast } from '@/hooks/use-toast' import { fetchWithAuth } from '@/lib/fetch-with-auth' import type { PluginInfo } from '@/types/plugin' import { checkGitStatus, getMaimaiVersion, isPluginCompatible, installPlugin, uninstallPlugin, updatePlugin, checkPluginInstalled, getInstalledPluginVersion, getInstalledPlugins, type GitStatus, type MaimaiVersion, } from '@/lib/plugin-api' import { PluginStats } from '@/components/plugin-stats' import { MarkdownRenderer } from '@/components' import { recordPluginDownload } from '@/lib/plugin-stats' // 分类名称映射 const CATEGORY_NAMES: Record = { 'Group Management': '群组管理', 'Entertainment & Interaction': '娱乐互动', 'Utility Tools': '实用工具', 'Content Generation': '内容生成', Multimedia: '多媒体', 'External Integration': '外部集成', 'Data Analysis & Insights': '数据分析与洞察', Other: '其他', } export function PluginDetailPage() { const navigate = useNavigate() const search = useSearch({ strict: false }) as { pluginId?: string } const { toast } = useToast() const [plugin, setPlugin] = useState(null) const [readme, setReadme] = useState('') const [loading, setLoading] = useState(true) const [readmeLoading, setReadmeLoading] = useState(true) const [error, setError] = useState(null) const [gitStatus, setGitStatus] = useState(null) const [maimaiVersion, setMaimaiVersion] = useState(null) const [isInstalled, setIsInstalled] = useState(false) const [installedVersion, setInstalledVersion] = useState() const [operating, setOperating] = useState(false) // 加载插件信息 useEffect(() => { const loadPluginInfo = async () => { if (!search.pluginId) { setError('缺少插件 ID') setLoading(false) return } try { setLoading(true) setError(null) // 从插件列表 API 获取数据 const response = await fetchWithAuth('/api/webui/plugins/fetch-raw', { method: 'POST', body: JSON.stringify({ owner: 'Mai-with-u', repo: 'plugin-repo', branch: 'main', file_path: 'plugin_details.json', }), }) if (!response.ok) { throw new Error('获取插件列表失败') } const result = await response.json() if (!result.success || !result.data) { throw new Error(result.error || '获取插件列表失败') } const pluginList = JSON.parse(result.data) const foundPlugin = pluginList.find((p: any) => p.id === search.pluginId) if (!foundPlugin) { throw new Error('未找到该插件') } // 转换为 PluginInfo 格式 const pluginInfo: PluginInfo = { id: foundPlugin.id, manifest: foundPlugin.manifest, downloads: 0, rating: 0, review_count: 0, installed: false, published_at: new Date().toISOString(), updated_at: new Date().toISOString(), } setPlugin(pluginInfo) // 加载额外信息 const [gitStatusResult, versionResult, installedPlugins] = await Promise.all([ checkGitStatus(), getMaimaiVersion(), getInstalledPlugins(), ]) if (!gitStatusResult.success) { toast({ title: 'Git 状态检查失败', description: gitStatusResult.error, variant: 'destructive', }) } else { setGitStatus(gitStatusResult.data) } if (!versionResult.success) { toast({ title: '版本获取失败', description: versionResult.error, variant: 'destructive', }) } else { setMaimaiVersion(versionResult.data) } if (!installedPlugins.success) { toast({ title: '获取已安装插件失败', description: installedPlugins.error, variant: 'destructive', }) return } setIsInstalled(checkPluginInstalled(search.pluginId, installedPlugins.data)) setInstalledVersion(getInstalledPluginVersion(search.pluginId, installedPlugins.data)) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setLoading(false) } } loadPluginInfo() }, [search.pluginId]) // 加载 README useEffect(() => { const loadReadme = async () => { if (!plugin?.manifest?.repository_url) { setReadmeLoading(false) return } try { setReadmeLoading(true) // 如果插件已安装,优先尝试从本地读取 README if (isInstalled && search.pluginId) { try { const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${search.pluginId}`) if (localResponse.ok) { const localResult = await localResponse.json() if (localResult.success && localResult.data) { setReadme(localResult.data) setReadmeLoading(false) return // 成功获取本地 README,直接返回 } } } catch (err) { console.log('本地 README 获取失败,尝试远程获取:', err) // 继续执行远程获取逻辑 } } // 从 repository_url 解析仓库信息 // 格式: https://github.com/owner/repo const match = plugin.manifest.repository_url.match(/github\.com\/([^/]+)\/([^/\s]+)/) if (!match) { setReadme('无法解析仓库地址') return } const [, owner, repo] = match const cleanRepo = repo.replace(/\.git$/, '') // 使用后端代理获取 README.md const response = await fetchWithAuth('/api/webui/plugins/fetch-raw', { method: 'POST', body: JSON.stringify({ owner, repo: cleanRepo, branch: 'main', file_path: 'README.md', }), }) if (!response.ok) { throw new Error('获取 README 失败') } const result = await response.json() if (result.success && result.data) { setReadme(result.data) } else { setReadme('该插件暂无 README 文档') } } catch (err) { console.error('加载 README 失败:', err) setReadme('加载 README 失败') } finally { setReadmeLoading(false) } } loadReadme() }, [plugin, isInstalled, search.pluginId]) // 检查是否需要更新 const needsUpdate = () => { if (!plugin || !isInstalled || !installedVersion) return false return installedVersion !== plugin.manifest.version } // 检查兼容性 const checkCompatibility = () => { if (!plugin || !maimaiVersion) return true return isPluginCompatible( plugin.manifest.host_application.min_version, plugin.manifest.host_application.max_version, maimaiVersion ) } // 安装插件 const handleInstall = async () => { if (!plugin || !gitStatus?.installed) return try { setOperating(true) const installResult = await installPlugin(plugin.id, plugin.manifest.repository_url || '', 'main') if (!installResult.success) { toast({ title: '安装失败', description: installResult.error, variant: 'destructive', }) return } // 记录下载统计 recordPluginDownload(plugin.id).catch((err) => { console.warn('Failed to record download:', err) }) toast({ title: '安装成功', description: `${plugin.manifest.name} 已成功安装`, }) // 重新加载安装状态 const installedPluginsResult = await getInstalledPlugins() if (!installedPluginsResult.success) { toast({ title: '获取已安装插件失败', description: installedPluginsResult.error, variant: 'destructive', }) return } setIsInstalled(checkPluginInstalled(plugin.id, installedPluginsResult.data)) setInstalledVersion(getInstalledPluginVersion(plugin.id, installedPluginsResult.data)) } catch (error) { toast({ title: '安装失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } finally { setOperating(false) } } // 卸载插件 const handleUninstall = async () => { if (!plugin) return try { setOperating(true) const uninstallResult = await uninstallPlugin(plugin.id) if (!uninstallResult.success) { toast({ title: '卸载失败', description: uninstallResult.error, variant: 'destructive', }) return } toast({ title: '卸载成功', description: `${plugin.manifest.name} 已成功卸载`, }) // 重新加载安装状态 const installedPluginsResult = await getInstalledPlugins() if (!installedPluginsResult.success) { toast({ title: '获取已安装插件失败', description: installedPluginsResult.error, variant: 'destructive', }) return } setIsInstalled(checkPluginInstalled(plugin.id, installedPluginsResult.data)) setInstalledVersion(getInstalledPluginVersion(plugin.id, installedPluginsResult.data)) } catch (error) { toast({ title: '卸载失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } finally { setOperating(false) } } // 更新插件 const handleUpdate = async () => { if (!plugin || !gitStatus?.installed) return try { setOperating(true) const updateResult = await updatePlugin(plugin.id, plugin.manifest.repository_url || '', 'main') if (!updateResult.success) { toast({ title: '更新失败', description: updateResult.error, variant: 'destructive', }) return } toast({ title: '更新成功', description: `${plugin.manifest.name} 已从 ${updateResult.data.old_version} 更新到 ${updateResult.data.new_version}`, }) // 重新加载安装状态 const installedPluginsResult = await getInstalledPlugins() if (!installedPluginsResult.success) { toast({ title: '获取已安装插件失败', description: installedPluginsResult.error, variant: 'destructive', }) return } setIsInstalled(checkPluginInstalled(plugin.id, installedPluginsResult.data)) setInstalledVersion(getInstalledPluginVersion(plugin.id, installedPluginsResult.data)) } catch (error) { toast({ title: '更新失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } finally { setOperating(false) } } if (loading) { return (

插件详情

加载插件信息中...
) } if (error || !plugin) { return (

插件详情

加载失败

{error}

) } const isCompatible = checkCompatibility() return (
{/* 页面标题和返回按钮 */}

插件详情

{plugin.manifest.name}

{/* 操作按钮 */}
{isInstalled ? ( <> {needsUpdate() ? ( ) : null} ) : ( )}
{/* 插件头部信息卡片 */}
{plugin.manifest.name} v{plugin.manifest.version} {isInstalled && ( 已安装 {installedVersion && `(v${installedVersion})`} )} {needsUpdate() && ( 可更新 )} {!isCompatible && ( 不兼容 )}
{plugin.manifest.description}
{/* 左侧 - 详细信息 */}
{/* 统计信息 */} 统计信息 {/* 基本信息 */} 基本信息
作者: {plugin.manifest.author?.name || 'Unknown'} {plugin.manifest.author?.url && ( )}
版本: v{plugin.manifest.version}
许可证: {plugin.manifest.license}
{plugin.manifest.homepage_url && (
主页: 访问
)} {plugin.manifest.repository_url && (
仓库: GitHub
)}
支持版本:
{plugin.manifest.host_application.min_version} {plugin.manifest.host_application.max_version ? ` - ${plugin.manifest.host_application.max_version}` : ' - 最新版本'}
{/* 分类和标签 */} {(plugin.manifest.categories || plugin.manifest.keywords) && ( 分类与标签 {plugin.manifest.categories && plugin.manifest.categories.length > 0 && (

分类

{plugin.manifest.categories.map((category) => ( {CATEGORY_NAMES[category] || category} ))}
)} {plugin.manifest.keywords && plugin.manifest.keywords.length > 0 && (

标签

{plugin.manifest.keywords.map((keyword) => ( {keyword} ))}
)}
)}
{/* 右侧 - README */} 插件说明 {readmeLoading ? (
加载说明文档中...
) : readme ? ( ) : (
暂无说明文档
)}
) }