import { useEffect, useMemo, useRef, useState } from "react"; import { api, pluginText, type PluginAddMethod, type PluginDescriptor, type PluginOAuthBegin, type PluginProviderDescriptor, type PluginResourceAction, type PluginResourceActionCard, type PluginResourceActionResult, 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 { ConfirmDialog } from "../../shared/ui/ConfirmDialog"; import { FormField, TextInput } from "../../shared/ui/FormControls"; import { Modal } from "../../shared/ui/Modal"; import { Switch } from "../../shared/ui/Switch"; 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 [copied, setCopied] = useState(false); const stopped = useRef(false); const copyCode = async (code: string) => { await api.copyCursorText(code).catch(() => undefined); setCopied(true); window.setTimeout(() => setCopied(false), 2000); }; 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"); if (next.userCode) await api.copyCursorText(next.userCode).catch(() => undefined); await api.openExternalUrl(next.verificationUrlComplete || next.verificationUrl); } catch (cause) { setStatus("error"); setError(errorText(cause)); } }; const userCode = begun?.userCode; return {pluginText(method.displayName, locale)} {method.description && {pluginText(method.description, locale)}} {userCode && 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 [modelProviderId, setModelProviderId] = useState(null); const [resourceAction, setResourceAction] = useState<{ resource: PluginResourceDescriptor; item: PluginResourceView; } | null>(null); const [resourceActionResult, setResourceActionResult] = useState(null); const [resourceActionError, setResourceActionError] = useState(null); const modelProvider = modelProviderId ? plugin.providers.find((provider) => provider.id === modelProviderId) ?? null : 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); } }; const executeResourceAction = async ( target: { resource: PluginResourceDescriptor; item: PluginResourceView }, action: PluginResourceAction, input: unknown = {}, ) => { const key = `action:${target.item.id}:${action.id}`; setBusy(key); setResourceActionError(null); try { const result = await api.pluginResourceAction( plugin.id, target.resource.type, target.item.id, action.id, input, ); setResourceActionResult(result); await appStore.refreshPlugins(); } catch (cause) { setResourceActionError(errorText(cause)); } finally { setBusy(null); } }; const openResourceAction = (resource: PluginResourceDescriptor, item: PluginResourceView, action: PluginResourceAction) => { setResourceAction({ resource, item }); setResourceActionResult(null); setResourceActionError(null); void executeResourceAction({ resource, item }, action); }; return
{plugin.providers.map((provider) => setModelProviderId(provider.id)} onSync={() => void run(`sync:${provider.id}`, async () => { await api.syncPluginModels(plugin.id, provider.id); })} />)} {plugin.resources.map((resource) => openResourceAction(resource, item, action)} onRefresh={(item) => 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}} {modelProvider && setModelProviderId(null)} onSubmit={(enabledByModel) => void run("models", async () => { for (const model of modelProvider.models) { const enabled = enabledByModel[model.id] ?? model.enabled; if (model.enabled !== enabled) await api.setPluginModelEnabled(plugin.id, modelProvider.id, model.modelId, enabled); } })} />} {resourceAction && item.target === "resource") ?? null} cardAction={resourceAction.resource.actions.find((item) => item.target === "card") ?? null} result={resourceActionResult} busy={busy !== null} error={resourceActionError} onClose={() => setResourceAction(null)} onCardAction={(action, card) => void executeResourceAction(resourceAction, action, { cardId: card.id })} />}
; } function ProviderRow({ provider, busy, syncing, onManageModels, onSync }: { provider: PluginProviderDescriptor; busy: boolean; syncing: boolean; onManageModels: () => void; 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 ModelManagementModal({ provider, busy, onClose, onSubmit }: { provider: PluginProviderDescriptor; busy: boolean; onClose: () => void; onSubmit: (enabledByModel: Record) => void; }) { const { locale } = useI18n(); const [enabledByModel, setEnabledByModel] = useState>({}); useEffect(() => { setEnabledByModel(Object.fromEntries(provider.models.map((model) => [model.id, model.enabled]))); }, [provider.models]); const setAll = (enabled: boolean) => { setEnabledByModel(Object.fromEntries(provider.models.map((model) => [model.id, enabled]))); }; return onSubmit(enabledByModel)} submitLabel={t("确定")} >
{provider.models.map((model) => )}
{t("模型名称")}{t("启用")}
{model.displayName} {model.description && {model.description}}
setEnabledByModel((current) => ({ ...current, [model.id]: enabled }))} />
{provider.models.length === 0 && {t("尚未同步模型")}}
; } function ResourceList({ resource, busy, onAction, onRefresh, onDelete }: { resource: PluginResourceDescriptor; busy: boolean; onAction: (item: PluginResourceView, action: PluginResourceAction) => void; 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) => action.target === "resource")} canRefresh={resource.canRefresh} disabled={busy} onAction={(action) => onAction(item, action)} onRefresh={() => onRefresh(item)} onDelete={() => onDelete(item)} />)} {visible.length === 0 && {t("还没有资源,请先添加。")}}
{pageCount > 1 &&
{t("第 {page} / {total} 页", { page: Math.min(page, pageCount), total: pageCount })}
}
; } function ResourceRow({ item, actions, canRefresh, disabled, onAction, onRefresh, onDelete }: { item: PluginResourceView; actions: PluginResourceAction[]; canRefresh: boolean; disabled: boolean; onAction: (action: PluginResourceAction) => void; 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}`} )}
{actions.map((action) => )} {canRefresh && }
; } function ResourceActionModal({ action, cardAction, result, busy, error, onClose, onCardAction }: { action: PluginResourceAction | null; cardAction: PluginResourceAction | null; result: PluginResourceActionResult | null; busy: boolean; error: string | null; onClose: () => void; onCardAction: (action: PluginResourceAction, card: PluginResourceActionCard) => void; }) { const { locale } = useI18n(); const [pendingCard, setPendingCard] = useState(null); const title = result ? pluginText(result.title, locale) : action ? pluginText(action.displayName, locale) : t("资源详情"); const cardActions = cardAction ? [cardAction] : []; return <>
{result?.description && {pluginText(result.description, locale)}} {busy && {t("正在加载…")}} {error && {error}} {!busy && !error && result && result.cards.length === 0 && {t("没有可用的重置卡。")}} {!busy && !error && result &&
{result.cards.map((card) =>
{pluginText(card.title, locale)} {card.status && {formatActionStatus(card.status, locale)}} {card.grantedAtMs !== null && card.grantedAtMs !== undefined && {t("发放时间:{time}", { time: formatActionDate(card.grantedAtMs, locale) })}} {card.expiresAtMs !== null && card.expiresAtMs !== undefined && {t("到期时间:{time}", { time: formatActionDate(card.expiresAtMs, locale) })}} {card.fields.map((field) => {pluginText(field.label, locale)}: {field.value})}
{cardActions.length > 0 &&
{cardActions.map((cardActionItem) => )}
}
)}
}
{pendingCard && cardAction && setPendingCard(null)} onConfirm={() => { const card = pendingCard; setPendingCard(null); onCardAction(cardAction, card); }} >

{t("使用后会立即消耗这张重置卡,且无法恢复。确定继续吗?")}

{pluginText(pendingCard.title, locale)}
} ; } function formatActionStatus(status: PluginResourceActionCard["status"], locale: string) { const value = typeof status === "string" ? status : pluginText(status, locale); switch (value.toLowerCase()) { case "available": return t("可用"); case "redeemed": case "used": return t("已使用"); case "expired": return t("已过期"); default: return value; } } function formatActionDate(value: number, locale: string) { return new Date(value).toLocaleString(locale); } 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); }