import { useEffect, useRef, useState } from "react"; import { api, pluginText, type PluginDescriptor, type PluginImportFile, type PluginRuntimePhase, type PluginRuntimeStatus } from "../../shared/api"; import { useI18n } from "../../i18n/store"; import { PageContent } from "../../shell/layout/PageContent"; import { appStore, useAppStore } from "../../shared/store/appStore"; import { ActionMenu } from "../../shared/ui/ActionMenu"; import { Button } from "../../shared/ui/Button"; import { Card } from "../../shared/ui/Card"; import { Modal } from "../../shared/ui/Modal"; import { useMessage } from "../../shared/ui/message"; import { TruncatedButton } from "../../shared/ui/TruncatedButton"; import { PluginAddPanel, PluginSettingsPanel } from "./PluginResourcePanels"; import styles from "./PluginManagementPage.module.scss"; export function PluginManagementPage() { const { pluginRuntime, plugins } = useAppStore(); const [progressOpen, setProgressOpen] = useState(false); const [starting, setStarting] = useState(false); const [selected, setSelected] = useState<{ pluginId: string; mode: "add" | "settings" } | null>(null); const cancelRequested = useRef(false); const selectedPlugin = selected ? plugins.find((plugin) => plugin.id === selected.pluginId) ?? null : null; useEffect(() => { if (!pluginRuntime) void appStore.refreshPluginRuntime(); }, [pluginRuntime]); useEffect(() => { if (pluginRuntime?.state !== "initializing") return; if (!cancelRequested.current) setProgressOpen(true); const timer = window.setInterval(() => void appStore.refreshPluginRuntime(), 300); return () => window.clearInterval(timer); }, [pluginRuntime?.state]); const initialize = async () => { if (starting) return; cancelRequested.current = false; setStarting(true); setProgressOpen(true); const status = await appStore.initializePluginRuntime(); setStarting(false); if (!status) { setProgressOpen(false); } else if (cancelRequested.current && status.state === "initializing") { void appStore.cancelPluginRuntimeInitialization(); } }; const closeProgress = () => { setProgressOpen(false); cancelRequested.current = true; if (pluginRuntime?.state === "initializing") { void appStore.cancelPluginRuntimeInitialization(); } }; const content = pluginRuntime?.state === "ready" ? setSelected({ pluginId, mode })} /> : void initialize()} />; const estimatedHeight = plugins.length > 0 ? Math.max(320, Math.ceil(plugins.length / 3) * 180) : 320; return <> setSelected(null)} > {selected?.mode === "add" && selectedPlugin && setSelected(null)} />} {selected?.mode === "settings" && selectedPlugin && } ; } function RuntimeGate({ status, starting, onInitialize }: { status: PluginRuntimeStatus | null; starting: boolean; onInitialize: () => void }) { const checking = status === null; const initializing = starting || status?.state === "initializing"; const failed = status?.state === "failed"; const unsupported = status?.state === "unsupported"; const title = checking ? t("正在检查插件运行时") : failed ? t("插件运行时初始化失败") : unsupported ? t("当前系统不支持插件运行时") : t("需要先初始化插件运行时"); const description = failed ? t("请重试初始化") : unsupported ? status.error ?? t("当前操作系统或 CPU 架构暂不受支持") : t("初始化将下载并安装插件运行时。"); return
{title} {description} {!unsupported && }
; } function PluginCards({ plugins, onOpen }: { plugins: PluginDescriptor[]; onOpen: (pluginId: string, mode: "add" | "settings") => void; }) { if (plugins.length === 0) { return
{t("还没有安装插件")} {t("安装插件后会显示在这里。")}
; } return
{plugins.map((plugin) => )}
; } function PluginCard({ plugin, onOpen }: { plugin: PluginDescriptor; onOpen: (pluginId: string, mode: "add" | "settings") => void; }) { const { locale } = useI18n(); const { ports } = useAppStore(); const message = useMessage(); const importInput = useRef(null); const [importing, setImporting] = useState(false); const configured = plugin.providers.some((provider) => provider.configured); const accountCount = plugin.resources.reduce((count, resource) => count + resource.resources.length, 0); const modelCount = plugin.providers.reduce((count, provider) => count + provider.models.length, 0); const subtitle = plugin.providers.map((provider) => pluginText(provider.displayName, locale)).join(" · ") || plugin.id; const importResource = plugin.resources.find((resource) => resource.import); const exportResource = plugin.resources.find((resource) => resource.resources.length > 0); const importFiles = async (files: FileList | null) => { if (!files?.length || !importResource) return; setImporting(true); try { const entries: PluginImportFile[] = await Promise.all( [...files].map(async (file) => ({ name: file.name, content: await file.text() })), ); const result = await api.importPluginResources(plugin.id, importResource.type, entries); await appStore.refreshPlugins(); const summary = t("导入完成:新增 {added},更新 {updated}", { added: result.added, updated: result.updated }); if (result.modelSyncError) { message(t("账号已保存,但同步模型失败:{error}", { error: result.modelSyncError }), { duration: 5000 }); } else if (result.warnings.length > 0) { message(`${summary} · ${result.warnings.join("; ")}`, { duration: 5000 }); } else { message(summary); } } catch (cause) { message(cause instanceof Error ? cause.message : String(cause), { duration: 5000 }); } finally { setImporting(false); if (importInput.current) importInput.current.value = ""; } }; return (
{plugin.name} {subtitle}
{configured ? t("已配置") : t("未配置")}
{t("{accounts} 个账号 · {models} 个模型", { accounts: accountCount, models: modelCount, })} {[`v${plugin.version}`, plugin.author].filter(Boolean).join(" · ")}
onOpen(plugin.id, "add")} /> {configured && ( onOpen(plugin.id, "settings")} /> )} {(importResource || exportResource) && ( importInput.current?.click(), }, ] : []), ...(exportResource ? [ { id: "export", label: t("批量导出"), onSelect: () => void api.openExternalUrl( api.pluginResourceExportUrl( ports.service_port, plugin.id, exportResource.type, ), ), }, ] : []), ]} /> )} {importResource && ( void importFiles(event.target.files)} /> )}
); } function RuntimeProgressModal({ open, status, starting, onClose }: { open: boolean; status: PluginRuntimeStatus | null; starting: boolean; onClose: () => void }) { const initializing = starting || status?.state === "initializing"; const downloaded = status?.downloaded_bytes ?? 0; const total = status?.total_bytes ?? null; const percent = total && total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null; const stage = status?.state === "ready" ? t("插件运行时初始化完成") : status?.state === "failed" ? t("插件运行时初始化失败") : phaseText(status?.phase ?? null); return
{stage} {status?.phase === "downloading" && <>
{total ? t("已下载 {downloaded} / {total}", { downloaded: formatBytes(downloaded), total: formatBytes(total) }) : t("已下载 {downloaded}", { downloaded: formatBytes(downloaded) })} } {status?.state === "failed" && {t("请重试初始化")}} {status?.state === "ready" && {t("插件运行时 {version} 已安装,可以开始使用插件。", { version: status.version })}}
; } function phaseText(phase: PluginRuntimePhase | null) { switch (phase) { case "checking": return t("正在检查插件运行时"); case "downloading": return t("正在下载插件运行时"); case "verifying": return t("正在验证插件运行时下载文件"); case "installing": return t("正在安装插件运行时"); case "validating": return t("正在验证插件运行时"); default: return t("正在准备插件运行时"); } } function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; const units = ["KB", "MB", "GB"]; let value = bytes / 1024; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value < 10 ? value.toFixed(1) : value.toFixed(0)} ${units[unit]}`; }