import { useEffect, useMemo, useRef, useState } from "react"; import { api, pluginText, type PluginAddMethod, type PluginDescriptor, type PluginOAuthBegin, type PluginProviderDescriptor, type PluginResourceDescriptor, type PluginResourceView, } from "../../shared/api"; import { useI18n } from "../../i18n/store"; import { appStore } from "../../shared/store/appStore"; import { Button } from "../../shared/ui/Button"; import { Card } from "../../shared/ui/Card"; import { FormField, TextInput } from "../../shared/ui/FormControls"; import styles from "./PluginResourcePanels.module.scss"; const PAGE_SIZE = 10; export function PluginAddPanel({ plugin, onConfigured }: { plugin: PluginDescriptor; onConfigured: () => void }) { return
{plugin.resources.map((resource) => )} {plugin.resources.length === 0 && {t("该插件不需要添加资源")}}
; } function ResourceAddSection({ plugin, resource, onConfigured }: { plugin: PluginDescriptor; resource: PluginResourceDescriptor; onConfigured: () => void; }) { return <> {resource.add.map((method) => )} ; } function OAuthMethodCard({ pluginId, resourceType, method, onConfigured }: { pluginId: string; resourceType: string; method: PluginAddMethod; onConfigured: () => void; }) { const { locale } = useI18n(); const [status, setStatus] = useState<"idle" | "starting" | "polling" | "success" | "error">("idle"); const [begun, setBegun] = useState(null); const [error, setError] = useState(null); const stopped = useRef(false); useEffect(() => () => { stopped.current = true; }, []); useEffect(() => { if (!begun || status !== "polling") return; let timer = 0; const poll = async (intervalMs: number) => { if (stopped.current) return; try { const result = await api.pluginOAuthPoll(begun.sessionId); if (stopped.current) return; if (result.status === "pending") { timer = window.setTimeout(() => void poll(result.pollIntervalMs), Math.max(1000, result.pollIntervalMs)); return; } if (result.status === "completed") { await appStore.refreshPlugins(); if (result.modelSyncError) { setStatus("error"); setError(t("账号已保存,但同步模型失败:{error}", { error: result.modelSyncError })); return; } setStatus("success"); onConfigured(); return; } setStatus("error"); setError(result.message || t("授权被拒绝或已失败。")); } catch (cause) { if (stopped.current) return; setError(errorText(cause)); timer = window.setTimeout(() => void poll(intervalMs), Math.max(1000, intervalMs)); } }; timer = window.setTimeout(() => void poll(begun.pollIntervalMs), Math.max(1000, begun.pollIntervalMs)); return () => window.clearTimeout(timer); }, [begun, onConfigured, status]); const start = async () => { setStatus("starting"); setError(null); try { const next = await api.pluginOAuthBegin(pluginId, resourceType, method.id); setBegun(next); setStatus("polling"); await api.copyCursorText(next.userCode).catch(() => undefined); await api.openExternalUrl(next.verificationUrlComplete || next.verificationUrl); } catch (cause) { setStatus("error"); setError(errorText(cause)); } }; return {pluginText(method.displayName, locale)} {method.description && {pluginText(method.description, locale)}} {begun && status === "polling" &&
{t("设备验证码")}
}
{begun && status === "polling" && }
{status === "success" && {t("账号已保存,模型目录已同步。")}} {error && {error}}
; } export function PluginSettingsPanel({ plugin }: { plugin: PluginDescriptor }) { const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const run = async (key: string, task: () => Promise) => { setBusy(key); setError(null); try { await task(); await appStore.refreshPlugins(); } catch (cause) { setError(errorText(cause)); } finally { setBusy(null); } }; return
{plugin.providers.map((provider) => void run(`sync:${provider.id}`, async () => { await api.syncPluginModels(plugin.id, provider.id); })} />)} {plugin.resources.map((resource) => void run(`refresh:${item.id}`, async () => { await api.refreshPluginResource(plugin.id, resource.type, item.id); })} onDelete={(item) => void run(`delete:${item.id}`, async () => { await api.deletePluginResource(plugin.id, resource.type, item.id); })} />)} {error && {error}}
; } function ProviderRow({ provider, busy, syncing, onSync }: { provider: PluginProviderDescriptor; busy: boolean; syncing: boolean; onSync: () => void; }) { const { locale } = useI18n(); return
{pluginText(provider.displayName, locale)} {provider.providerType} {" · "} {provider.models.length > 0 ? t("{count} 个模型", { count: provider.models.length }) : t("尚未同步模型")} {" · "} {provider.configured ? t("可调用") : t("未就绪")}
{provider.hasModels && }
; } function ResourceList({ resource, busy, onRefresh, onDelete }: { resource: PluginResourceDescriptor; busy: boolean; onRefresh: (item: PluginResourceView) => void; onDelete: (item: PluginResourceView) => void; }) { const { locale } = useI18n(); const [query, setQuery] = useState(""); const [page, setPage] = useState(1); const filtered = useMemo( () => resource.resources.filter((item) => item.displayName.toLowerCase().includes(query.trim().toLowerCase())), [resource.resources, query], ); const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const visible = filtered.slice((Math.min(page, pageCount) - 1) * PAGE_SIZE, Math.min(page, pageCount) * PAGE_SIZE); useEffect(() => setPage(1), [query]); return
{resource.resources.length > PAGE_SIZE &&
setQuery(event.target.value)} />
}
{visible.map((item) => onRefresh(item)} onDelete={() => onDelete(item)} />)} {visible.length === 0 && {t("还没有资源,请先添加。")}}
{pageCount > 1 &&
{t("第 {page} / {total} 页", { page: Math.min(page, pageCount), total: pageCount })}
}
; } function ResourceRow({ item, canRefresh, disabled, onRefresh, onDelete }: { item: PluginResourceView; canRefresh: boolean; disabled: boolean; onRefresh: () => void; onDelete: () => void; }) { const { locale } = useI18n(); return
{item.displayName} {item.description && {pluginText(item.description, locale)}} {item.metrics.map((metric) => {metric.unit === "percent" ? t("{label} 剩余 {percent}%", { label: pluginText(metric.label, locale), percent: Math.round(metric.value) }) : `${pluginText(metric.label, locale)}: ${metric.value}`} )}
{canRefresh && }
; } function StateBadge({ state }: { state: PluginResourceView["state"] }) { if (state.status === "cooling") { return {t("冷却中")}; } if (state.status === "invalid") { return {t("已失效")}; } return {t("可用")}; } function errorText(cause: unknown) { return cause instanceof Error ? cause.message : String(cause); }