import { useState, useEffect } from 'react' import { useNavigate } from '@tanstack/react-router' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Checkbox } from '@/components/ui/checkbox' import { Search, Download, Star, CheckCircle2, AlertCircle, Loader2, AlertTriangle, RefreshCw, Trash2, Settings2, RotateCw, Info } from 'lucide-react' import type { PluginInfo } from '@/types/plugin' import { RestartProvider, useRestart } from '@/lib/restart-context' import { RestartOverlay } from '@/components/restart-overlay' import { fetchPluginList, checkGitStatus, connectPluginProgressWebSocket, installPlugin, uninstallPlugin, updatePlugin, getMaimaiVersion, isPluginCompatible, getInstalledPlugins, checkPluginInstalled, getInstalledPluginVersion, type GitStatus, type PluginLoadProgress, type MaimaiVersion, type InstalledPlugin } from '@/lib/plugin-api' import { useToast } from '@/hooks/use-toast' import { Progress } from '@/components/ui/progress' import { recordPluginDownload, getPluginStats, type PluginStatsData } from '@/lib/plugin-stats' // 分类名称映射 const CATEGORY_NAMES: Record = { 'Group Management': '群组管理', 'Entertainment & Interaction': '娱乐互动', 'Utility Tools': '实用工具', 'Content Generation': '内容生成', 'Multimedia': '多媒体', 'External Integration': '外部集成', 'Data Analysis & Insights': '数据分析与洞察', 'Other': '其他', } // 主导出组件:包装 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 [selectedBranch, setSelectedBranch] = useState('main') const [customBranch, setCustomBranch] = useState('') const [branchInputMode, setBranchInputMode] = useState<'preset' | 'custom'>('preset') const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) 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 ws: WebSocket | null = null let isUnmounted = false const init = async () => { // 1. 先连接 WebSocket(异步获取 token) ws = 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. 等待 WebSocket 连接建立 await new Promise((resolve) => { if (!ws) { resolve() return } const checkConnection = () => { if (ws && ws.readyState === WebSocket.OPEN) { console.log('WebSocket connected, starting to load plugins') resolve() } else if (ws && ws.readyState === WebSocket.CLOSED) { console.warn('WebSocket closed before loading plugins') resolve() } else { setTimeout(checkConnection, 100) } } checkConnection() }) // 3. 检查 Git 状态 if (!isUnmounted) { const status = await checkGitStatus() setGitStatus(status) if (!status.installed) { toast({ title: 'Git 未安装', description: status.error || '请先安装 Git 才能使用插件安装功能', variant: 'destructive', }) } } // 4. 获取麦麦版本 if (!isUnmounted) { const version = await getMaimaiVersion() setMaimaiVersion(version) } // 5. 加载插件列表(包含已安装信息) if (!isUnmounted) { try { setLoading(true) setError(null) const data = await fetchPluginList() if (!isUnmounted) { // 获取已安装插件列表 const installed = await getInstalledPlugins() 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) } } catch (err) { if (!isUnmounted) { const errorMessage = err instanceof Error ? err.message : '加载插件列表失败' setError(errorMessage) toast({ title: '加载失败', description: errorMessage, variant: 'destructive', }) } } finally { if (!isUnmounted) { setLoading(false) } } } } init() return () => { isUnmounted = true if (ws) { ws.close() } } }, [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) { // console.log(`[Plugin ${plugin.id}] 版本不一致:`, { // installed: installedVer, // market: marketVer, // installedType: typeof plugin.installed_version, // marketType: typeof plugin.manifest.version // }) // 简单的版本比较:只有当市场版本比已安装版本新时才显示"可更新" // 如果本地版本更新(比如手动更新或市场数据过期),则显示"已安装" 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 filteredPlugins = plugins.filter(plugin => { // 跳过没有 manifest 的插件 if (!plugin.manifest) { console.warn('[过滤] 跳过无 manifest 的插件:', plugin.id) 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)) // 标签页过滤 let matchesTab = true if (activeTab === 'installed') { matchesTab = plugin.installed === true } else if (activeTab === 'updates') { matchesTab = plugin.installed === true && needsUpdate(plugin) } // 兼容性过滤 const matchesCompatibility = !showCompatibleOnly || !maimaiVersion || checkPluginCompatibility(plugin) return matchesSearch && matchesCategory && matchesTab && matchesCompatibility }) // 打开安装对话框 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) setSelectedBranch('main') setCustomBranch('') setBranchInputMode('preset') setShowAdvancedOptions(false) setInstallDialogOpen(true) } // 安装插件处理 const handleInstall = async () => { if (!installingPlugin) return const branch = branchInputMode === 'custom' ? customBranch : selectedBranch if (!branch || branch.trim() === '') { toast({ title: '分支名称不能为空', variant: 'destructive', }) return } try { setInstallDialogOpen(false) await installPlugin( installingPlugin.id, installingPlugin.manifest.repository_url || '', branch ) // 记录下载统计 recordPluginDownload(installingPlugin.id).catch(err => { console.warn('Failed to record download:', err) }) toast({ title: '安装成功', description: `${installingPlugin.manifest.name} 已成功安装`, }) // 重新加载已安装插件列表 const installed = await getInstalledPlugins() 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 { await uninstallPlugin(plugin.id) toast({ title: '卸载成功', description: `${plugin.manifest.name} 已成功卸载`, }) // 重新加载已安装插件列表 const installed = await getInstalledPlugins() 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 result = await updatePlugin( plugin.id, plugin.manifest.repository_url || '', 'main' ) toast({ title: '更新成功', description: `${plugin.manifest.name} 已从 ${result.old_version} 更新到 ${result.new_version}`, }) // 重新加载已安装插件列表 const installed = await getInstalledPlugins() 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', }) } } return (
{/* 标题 */}

插件市场

浏览和管理麦麦的插件

{/* 安装提示 */}

安装、卸载或更新插件后,需要重启麦麦才能使更改生效

{/* Git 状态警告 */} {gitStatus && !gitStatus.installed && (
Git 未安装 {gitStatus.error || '请先安装 Git 才能使用插件安装功能'}

您可以从 git-scm.com 下载并安装 Git。 安装完成后,请重启麦麦应用。

)} {/* 搜索和筛选栏 */}
{/* 搜索框 */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* 分类筛选 */}
{/* 兼容性筛选 */}
setShowCompatibleOnly(checked === true)} />
{/* 标签页 */} 全部插件 ({ 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) return matchesSearch && matchesCategory && matchesCompatibility }).length }) 已安装 ({ 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) return p.installed && matchesSearch && matchesCategory && matchesCompatibility }).length }) 可更新 ({ 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) return p.installed && needsUpdate(p) && matchesSearch && matchesCategory && matchesCompatibility }).length }) {/* 进度条 - 仅显示插件清单加载进度 */} {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}

) : filteredPlugins.length === 0 ? (

未找到插件

{searchQuery || categoryFilter !== 'all' ? '尝试调整搜索条件或筛选器' : '暂无可用插件'}

) : (
{filteredPlugins.map((plugin) => (
{plugin.manifest?.name || plugin.id}
{plugin.manifest?.categories && plugin.manifest.categories[0] && ( {CATEGORY_NAMES[plugin.manifest.categories[0]] || plugin.manifest.categories[0]} )} {getStatusBadge(plugin)}
{plugin.manifest?.description || '无描述'}
{/* 统计信息 */}
{(pluginStats[plugin.id]?.downloads ?? plugin.downloads ?? 0).toLocaleString()}
{(pluginStats[plugin.id]?.rating ?? plugin.rating ?? 0).toFixed(1)}
{/* 标签 */}
{plugin.manifest?.keywords && plugin.manifest.keywords.slice(0, 3).map((keyword) => ( {keyword} ))} {plugin.manifest?.keywords && plugin.manifest.keywords.length > 3 && ( +{plugin.manifest.keywords.length - 3} )}
{/* 版本和作者 */}
v{plugin.manifest?.version || 'unknown'} · {plugin.manifest?.author?.name || 'Unknown'}
{/* 支持版本 */} {plugin.manifest?.host_application && (
支持: {plugin.manifest.host_application.min_version} {plugin.manifest.host_application.max_version ? ` - ${plugin.manifest.host_application.max_version}` : ' - 最新版本' }
)}
{plugin.installed ? ( needsUpdate(plugin) ? ( ) : ( ) ) : ( )}
{/* 安装/卸载/更新进度显示 - 在卡片下方 */} {loadProgress && (loadProgress.stage === 'loading' || loadProgress.stage === 'success' || loadProgress.stage === 'error') && loadProgress.operation !== 'fetch' && loadProgress.plugin_id === plugin.id && (
{loadProgress.stage === 'loading' ? ( ) : loadProgress.stage === 'success' ? ( ) : ( )} {loadProgress.stage === 'loading' ? ( <> {loadProgress.operation === 'install' && '正在安装'} {loadProgress.operation === 'uninstall' && '正在卸载'} {loadProgress.operation === 'update' && '正在更新'} ) : loadProgress.stage === 'success' ? ( <> {loadProgress.operation === 'install' && '安装完成'} {loadProgress.operation === 'uninstall' && '卸载完成'} {loadProgress.operation === 'update' && '更新完成'} ) : ( <> {loadProgress.operation === 'install' && '安装失败'} {loadProgress.operation === 'uninstall' && '卸载失败'} {loadProgress.operation === 'update' && '更新失败'} )}
{loadProgress.stage !== 'error' && ( {loadProgress.progress}% )}
{loadProgress.stage !== 'error' && ( div]:bg-green-500' : ''}`} /> )}
{loadProgress.stage === 'error' ? (loadProgress.error || loadProgress.message || '操作失败') : loadProgress.message}
)}
))}
)} {/* 安装对话框 */} 安装插件 安装 {installingPlugin?.manifest.name}
{/* 基本信息 */}

版本: {installingPlugin?.manifest.version}

作者: {typeof installingPlugin?.manifest.author === 'string' ? installingPlugin.manifest.author : installingPlugin?.manifest.author?.name}

{/* 高级选项开关 */}
setShowAdvancedOptions(checked as boolean)} />
{/* 高级选项内容 */} {showAdvancedOptions && (
setBranchInputMode(value as 'preset' | 'custom')}> 预设分支 自定义分支 {/* 预设分支选择 */} {branchInputMode === 'preset' && (
)} {/* 自定义分支输入 */} {branchInputMode === 'custom' && (
setCustomBranch(e.target.value)} />

输入 Git 分支名称、标签或提交哈希

)}
)} {!showAdvancedOptions && (

将从默认分支 (main) 安装插件

)}
{/* 重启遮罩层 */}
) }