import { useState, useEffect } from 'react' import { useNavigate } from '@tanstack/react-router' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { AlertCircle, AlertTriangle, CheckCircle2, Info, Loader2, RotateCw, Search, Settings2 } from 'lucide-react' import { RestartOverlay } from '@/components/restart-overlay' import { useToast } from '@/hooks/use-toast' import { RestartProvider, useRestart } from '@/lib/restart-context' import { checkGitStatus, checkPluginInstalled, connectPluginProgressWebSocket, fetchPluginList, getInstalledPluginVersion, getInstalledPlugins, getMaimaiVersion, installPlugin, isPluginCompatible, uninstallPlugin, updatePlugin, type InstalledPlugin, } from '@/lib/plugin-api' import { getPluginStats, recordPluginDownload, type PluginStatsData } from '@/lib/plugin-stats' import { InstallDialog } from './InstallDialog' import { InstalledTab } from './InstalledTab' import { MarketplaceTab } from './MarketplaceTab' import type { GitStatus, MaimaiVersion, PluginInfo, PluginLoadProgress } from './types' // 主导出组件:包装 RestartProvider export function PluginsPage() { return ( ) } // 内部组件:实际内容 function PluginsPageContent() { const navigate = useNavigate() const { triggerRestart, isRestarting } = useRestart() const [searchQuery, setSearchQuery] = useState('') const [categoryFilter, setCategoryFilter] = useState('all') const [activeTab, setActiveTab] = useState('all') // all | installed | updates const [showCompatibleOnly, setShowCompatibleOnly] = useState(true) // 默认只显示兼容的 const [plugins, setPlugins] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [gitStatus, setGitStatus] = useState(null) const [loadProgress, setLoadProgress] = useState(null) const [maimaiVersion, setMaimaiVersion] = useState(null) const [, setInstalledPlugins] = useState([]) const [pluginStats, setPluginStats] = useState>({}) // 安装对话框状态 const [installDialogOpen, setInstallDialogOpen] = useState(false) const [installingPlugin, setInstallingPlugin] = useState(null) const { toast } = useToast() // 加载插件统计数据 const loadPluginStats = async (pluginList: PluginInfo[]) => { const statsPromises = pluginList.map(async (plugin) => { try { const stats = await getPluginStats(plugin.id) return { id: plugin.id, stats } } catch (error) { console.warn(`Failed to load stats for ${plugin.id}:`, error) return { id: plugin.id, stats: null } } }) const results = await Promise.all(statsPromises) const statsMap: Record = {} results.forEach(({ id, stats }) => { if (stats) { statsMap[id] = stats } }) setPluginStats(statsMap) } // 统一管理 WebSocket 和数据加载 useEffect(() => { let unsubscribeProgress: (() => Promise) | null = null let isUnmounted = false const init = async () => { // 1. 先连接 WebSocket(异步获取 token) unsubscribeProgress = await connectPluginProgressWebSocket( (progress) => { if (isUnmounted) return setLoadProgress(progress) // 如果加载完成,清除进度 if (progress.stage === 'success') { setTimeout(() => { if (!isUnmounted) { setLoadProgress(null) } }, 2000) } else if (progress.stage === 'error') { setLoading(false) setError(progress.error || '加载失败') } }, (error) => { console.error('WebSocket error:', error) if (!isUnmounted) { toast({ title: 'WebSocket 连接失败', description: '无法实时显示加载进度', variant: 'destructive', }) } } ) // 2. 检查 Git 状态 if (!isUnmounted) { const statusResult = await checkGitStatus() if (!statusResult.success) { toast({ title: 'Git 状态检查失败', description: statusResult.error, variant: 'destructive', }) setGitStatus({ installed: false, error: statusResult.error }) } else { setGitStatus(statusResult.data) if (!statusResult.data.installed) { toast({ title: 'Git 未安装', description: statusResult.data.error || '请先安装 Git 才能使用插件安装功能', variant: 'destructive', }) } } } // 3. 获取麦麦版本 if (!isUnmounted) { const versionResult = await getMaimaiVersion() if (!versionResult.success) { toast({ title: '版本获取失败', description: versionResult.error, variant: 'destructive', }) } else { setMaimaiVersion(versionResult.data) } } // 4. 加载插件列表(包含已安装信息) if (!isUnmounted) { try { setLoading(true) setError(null) const apiResult = await fetchPluginList() if (!apiResult.success) { if (!isUnmounted) { setError(apiResult.error) toast({ title: '加载失败', description: apiResult.error, variant: 'destructive', }) } return } const data = apiResult.data if (!isUnmounted) { // 获取已安装插件列表 const installedResult = await getInstalledPlugins() if (!installedResult.success) { toast({ title: '获取已安装插件失败', description: installedResult.error, variant: 'destructive', }) return } const installed = installedResult.data setInstalledPlugins(installed) // 将已安装信息合并到插件数据中 const mergedData = data.map(plugin => { const isInstalled = checkPluginInstalled(plugin.id, installed) const installedVersion = getInstalledPluginVersion(plugin.id, installed) return { ...plugin, installed: isInstalled, installed_version: installedVersion } }) // 添加本地安装但不在市场的插件 for (const installedPlugin of installed) { const existsInMarket = mergedData.some(p => p.id === installedPlugin.id) if (!existsInMarket && installedPlugin.manifest) { // 添加本地插件到列表 mergedData.push({ id: installedPlugin.id, manifest: { manifest_version: installedPlugin.manifest.manifest_version || 1, name: installedPlugin.manifest.name, version: installedPlugin.manifest.version, description: installedPlugin.manifest.description || '', author: installedPlugin.manifest.author, license: installedPlugin.manifest.license || 'Unknown', host_application: installedPlugin.manifest.host_application, homepage_url: installedPlugin.manifest.homepage_url, repository_url: installedPlugin.manifest.repository_url, keywords: installedPlugin.manifest.keywords || [], categories: installedPlugin.manifest.categories || [], default_locale: (installedPlugin.manifest.default_locale as string) || 'zh-CN', locales_path: installedPlugin.manifest.locales_path as string | undefined, }, downloads: 0, rating: 0, review_count: 0, installed: true, installed_version: installedPlugin.manifest.version, published_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) } } setPlugins(mergedData) // 6. 加载所有插件的统计数据 loadPluginStats(mergedData) } } finally { if (!isUnmounted) { setLoading(false) } } } } init() return () => { isUnmounted = true if (unsubscribeProgress) { void unsubscribeProgress() } } }, [toast]) // 获取插件状态徽章 const getStatusBadge = (plugin: PluginInfo) => { // 优先显示兼容性状态 if (!plugin.installed && maimaiVersion && !checkPluginCompatibility(plugin)) { return ( 不兼容 ) } if (plugin.installed) { // 版本比较:去除两边空格并进行比较 const installedVer = plugin.installed_version?.trim() const marketVer = plugin.manifest.version?.trim() if (installedVer !== marketVer) { // 简单的版本比较:只有当市场版本比已安装版本新时才显示"可更新" // 如果本地版本更新(比如手动更新或市场数据过期),则显示"已安装" const installedParts = installedVer?.split('.').map(Number) || [0, 0, 0] const marketParts = marketVer?.split('.').map(Number) || [0, 0, 0] // 比较主版本号、次版本号、修订号 for (let i = 0; i < 3; i++) { if ((marketParts[i] || 0) > (installedParts[i] || 0)) { // 市场版本更新 return ( 可更新 ) } else if ((marketParts[i] || 0) < (installedParts[i] || 0)) { // 本地版本更新 break } } } return ( 已安装 ) } return null } // 检查插件兼容性 const checkPluginCompatibility = (plugin: PluginInfo): boolean => { if (!maimaiVersion || !plugin.manifest?.host_application) return true return isPluginCompatible( plugin.manifest.host_application.min_version, plugin.manifest.host_application.max_version, maimaiVersion ) } // 检查是否需要更新(市场版本比已安装版本新) const needsUpdate = (plugin: PluginInfo): boolean => { if (!plugin.installed || !plugin.installed_version || !plugin.manifest?.version) { return false } const installedVer = plugin.installed_version.trim() const marketVer = plugin.manifest.version.trim() if (installedVer === marketVer) return false const installedParts = installedVer.split('.').map(Number) const marketParts = marketVer.split('.').map(Number) // 比较主版本号、次版本号、修订号 for (let i = 0; i < 3; i++) { if ((marketParts[i] || 0) > (installedParts[i] || 0)) { return true // 市场版本更新 } else if ((marketParts[i] || 0) < (installedParts[i] || 0)) { return false // 本地版本更新 } } return false } // 打开安装对话框 const openInstallDialog = (plugin: PluginInfo) => { if (!gitStatus?.installed) { toast({ title: '无法安装', description: 'Git 未安装', variant: 'destructive', }) return } // 检查插件兼容性 if (maimaiVersion && !checkPluginCompatibility(plugin)) { toast({ title: '无法安装', description: '插件与当前麦麦版本不兼容', variant: 'destructive', }) return } setInstallingPlugin(plugin) setInstallDialogOpen(true) } // 安装插件处理 const handleInstall = async (branch: string) => { if (!installingPlugin) return if (!branch || branch.trim() === '') { toast({ title: '分支名称不能为空', variant: 'destructive', }) return } try { setInstallDialogOpen(false) const installResult = await installPlugin( installingPlugin.id, installingPlugin.manifest.repository_url || '', branch ) if (!installResult.success) { toast({ title: '安装失败', description: installResult.error, variant: 'destructive', }) return } // 记录下载统计 recordPluginDownload(installingPlugin.id).catch(err => { console.warn('Failed to record download:', err) }) toast({ title: '安装成功', description: `${installingPlugin.manifest.name} 已成功安装`, }) // 重新加载已安装插件列表 const installedResult = await getInstalledPlugins() if (!installedResult.success) { toast({ title: '获取已安装插件失败', description: installedResult.error, variant: 'destructive', }) return } const installed = installedResult.data setInstalledPlugins(installed) // 重新合并已安装信息到插件列表 setPlugins(prevPlugins => prevPlugins.map(p => { if (p.id === installingPlugin.id) { const isInstalled = checkPluginInstalled(p.id, installed) const installedVersion = getInstalledPluginVersion(p.id, installed) return { ...p, installed: isInstalled, installed_version: installedVersion } } return p }) ) } catch (error) { toast({ title: '安装失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } finally { setInstallingPlugin(null) } } // 卸载插件处理 const handleUninstall = async (plugin: PluginInfo) => { try { const uninstallResult = await uninstallPlugin(plugin.id) if (!uninstallResult.success) { toast({ title: '卸载失败', description: uninstallResult.error, variant: 'destructive', }) return } toast({ title: '卸载成功', description: `${plugin.manifest.name} 已成功卸载`, }) // 重新加载已安装插件列表 const installedResult = await getInstalledPlugins() if (!installedResult.success) { toast({ title: '获取已安装插件失败', description: installedResult.error, variant: 'destructive', }) return } const installed = installedResult.data setInstalledPlugins(installed) // 重新合并已安装信息到插件列表 setPlugins(prevPlugins => prevPlugins.map(p => { if (p.id === plugin.id) { const isInstalled = checkPluginInstalled(p.id, installed) const installedVersion = getInstalledPluginVersion(p.id, installed) return { ...p, installed: isInstalled, installed_version: installedVersion } } return p }) ) } catch (error) { toast({ title: '卸载失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } } // 更新插件处理 const handleUpdate = async (plugin: PluginInfo) => { if (!gitStatus?.installed) { toast({ title: '无法更新', description: 'Git 未安装', variant: 'destructive', }) return } try { 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 installedResult = await getInstalledPlugins() if (!installedResult.success) { toast({ title: '获取已安装插件失败', description: installedResult.error, variant: 'destructive', }) return } const installed = installedResult.data setInstalledPlugins(installed) // 重新合并已安装信息到插件列表 setPlugins(prevPlugins => prevPlugins.map(p => { if (p.id === plugin.id) { const isInstalled = checkPluginInstalled(p.id, installed) const installedVersion = getInstalledPluginVersion(p.id, installed) return { ...p, installed: isInstalled, installed_version: installedVersion } } return p }) ) } catch (error) { toast({ title: '更新失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive', }) } } // 过滤插件用于标签页统计 const getFilteredPluginCount = (tab: 'all' | 'installed' | 'updates') => { return plugins.filter(p => { if (!p.manifest) return false const matchesSearch = searchQuery === '' || p.manifest.name?.toLowerCase().includes(searchQuery.toLowerCase()) || p.manifest.description?.toLowerCase().includes(searchQuery.toLowerCase()) || (p.manifest.keywords && p.manifest.keywords.some(k => k.toLowerCase().includes(searchQuery.toLowerCase()))) const matchesCategory = categoryFilter === 'all' || (p.manifest.categories && p.manifest.categories.includes(categoryFilter)) const matchesCompatibility = !showCompatibleOnly || !maimaiVersion || checkPluginCompatibility(p) let matchesTab = true if (tab === 'installed') { matchesTab = p.installed === true } else if (tab === 'updates') { matchesTab = p.installed === true && needsUpdate(p) } return matchesSearch && matchesCategory && matchesCompatibility && matchesTab }).length } // 过滤插件用于可更新标签页 const filteredUpdatablePlugins = plugins.filter(plugin => { if (!plugin.manifest) return false const matchesSearch = searchQuery === '' || plugin.manifest.name?.toLowerCase().includes(searchQuery.toLowerCase()) || plugin.manifest.description?.toLowerCase().includes(searchQuery.toLowerCase()) || (plugin.manifest.keywords && plugin.manifest.keywords.some(k => k.toLowerCase().includes(searchQuery.toLowerCase()))) const matchesCategory = categoryFilter === 'all' || (plugin.manifest.categories && plugin.manifest.categories.includes(categoryFilter)) const matchesCompatibility = !showCompatibleOnly || !maimaiVersion || checkPluginCompatibility(plugin) return plugin.installed && needsUpdate(plugin) && matchesSearch && matchesCategory && matchesCompatibility }) return ( {/* 标题 */} 插件市场 浏览和管理麦麦的插件 triggerRestart()} disabled={isRestarting} > 重启麦麦 navigate({ to: '/plugin-mirrors' })}> 配置镜像源 {/* 安装提示 */} 安装、卸载或更新插件后,需要重启麦麦才能使更改生效 {/* Git 状态警告 */} {gitStatus && !gitStatus.installed && ( Git 未安装 {gitStatus.error || '请先安装 Git 才能使用插件安装功能'} 您可以从 git-scm.com 下载并安装 Git。 安装完成后,请重启麦麦应用。 )} {/* 搜索和筛选栏 */} {/* 搜索框 */} setSearchQuery(e.target.value)} className="pl-9" /> {/* 分类筛选 */} 全部分类 群组管理 娱乐互动 实用工具 内容生成 多媒体 外部集成 数据分析与洞察 其他 {/* 兼容性筛选 */} setShowCompatibleOnly(checked === true)} /> 只显示兼容当前版本的插件 {/* 标签页 */} 全部插件 ({getFilteredPluginCount('all')}) 已安装 ({getFilteredPluginCount('installed')}) 可更新 ({getFilteredPluginCount('updates')}) {/* 进度条 - 仅显示插件清单加载进度 */} {loadProgress && loadProgress.stage === 'loading' && loadProgress.operation === 'fetch' && ( 加载插件列表 {loadProgress.progress}% {loadProgress.message} {loadProgress.total_plugins > 0 && ( 已加载 {loadProgress.loaded_plugins} / {loadProgress.total_plugins} 个插件 )} )} {/* 加载错误显示 */} {loadProgress && loadProgress.stage === 'error' && loadProgress.error && ( 加载失败 {loadProgress.error} )} {/* 插件卡片网格 */} {loading ? ( 加载插件列表中... ) : error ? ( 加载失败 {error} window.location.reload()}> 重新加载 ) : activeTab === 'all' ? ( ) : activeTab === 'installed' ? ( ) : ( {filteredUpdatablePlugins.map((plugin) => ( {/* PluginCard would go here */} ))} )} {/* 安装对话框 */} {/* 重启遮罩层 */} ) }
浏览和管理麦麦的插件
安装、卸载或更新插件后,需要重启麦麦才能使更改生效
您可以从 git-scm.com 下载并安装 Git。 安装完成后,请重启麦麦应用。
{error}