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("确定")}
>
| {t("模型名称")} | {t("启用")} |
{provider.models.map((model) =>
{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);
}