feat(*): 插件中心与模型协议 - 完善声明式协议运行时并增加功能开放控制

This commit is contained in:
ddCat
2026-08-23 23:19:27 +08:00
parent 7cc6021f6e
commit 919037e5d9
106 changed files with 8817 additions and 500 deletions

View File

@@ -8,7 +8,8 @@ import { ModelIcon } from "@/components/model-picker";
import { ModelCapabilityEditor } from "@/components/model-capability-editor";
import { CapabilityCardPicker, ProtocolCardPicker, type ModelCapabilityChoice } from "@/components/model-protocol-picker";
import { defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ModelCapabilityConfig } from "@/lib/model-capabilities";
import { MODEL_PROTOCOLS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
import { modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
import { fetchProtocolCatalog } from "@/services/api/protocols";
import { createAdminChannelModel, deleteAdminChannelModel, fetchAdminChannelModels, listAdminChannelModels, testAdminChannelModel, updateAdminChannelModel, type ChannelModel, type ChannelModelPriceTier } from "@/services/api/wallet";
import type { ModelChannel } from "@/stores/use-config-store";
import { AdminPageFrame } from "./admin-shell";
@@ -21,7 +22,7 @@ type FormValues = {
providerModelKey?: string;
displayName?: string;
capability: EditableCapability;
protocol: ModelProtocol;
protocol?: ModelProtocol;
priceTiers: PriceTierFormValues[];
enabled: boolean;
capabilityConfig?: ModelCapabilityConfig;
@@ -58,6 +59,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
const [status, setStatus] = useState<"all" | "enabled" | "disabled">("all");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [availableProtocols, setAvailableProtocols] = useState<import("@/lib/model-protocols").ModelProtocolDefinition[]>([]);
const [form] = Form.useForm<FormValues>();
const modelCapability = Form.useWatch("capability", form);
const modelProtocol = Form.useWatch("protocol", form);
@@ -79,6 +81,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
useEffect(() => {
void reload();
void fetchProtocolCatalog("admin.system-channel").then(setAvailableProtocols).catch(() => setAvailableProtocols([]));
setEditing(null);
setEditorOpen(false);
setKeyword("");
@@ -111,10 +114,10 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
providerModelKey: "",
displayName: "",
capability: "text",
protocol: "chat-completion",
protocol: availableProtocols.find((item) => item.capability === "text")?.value,
priceTiers: [defaultPriceTier()],
enabled: true,
capabilityConfig: defaultModelCapabilityConfig("chat-completion", ""),
capabilityConfig: defaultModelCapabilityConfig(availableProtocols.find((item) => item.capability === "text")?.value, ""),
});
setEditorOpen(true);
};
@@ -220,8 +223,8 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
}
if (!changed.capability) return;
const current = form.getFieldValue("protocol") as ModelProtocol | undefined;
if (modelProtocolCapability(current) !== changed.capability) {
const nextProtocol = MODEL_PROTOCOLS.find((item) => item.capability === changed.capability)?.value;
if (modelProtocolCapability(current, availableProtocols) !== changed.capability) {
const nextProtocol = availableProtocols.find((item) => item.capability === changed.capability)?.value;
form.setFieldValue("protocol", nextProtocol);
form.setFieldValue("capabilityConfig", changed.capability === "text" || changed.capability === "image" || changed.capability === "video" ? defaultModelCapabilityConfig(nextProtocol, form.getFieldValue("modelKey")) : undefined);
}
@@ -262,8 +265,8 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
render: (value: ModelProtocol) =>
value ? (
<div>
<div className="text-xs font-medium">{modelProtocolLabel(value)}</div>
<div className="truncate text-[var(--fs-tiny)] text-foreground/45">{modelProtocolDefinition(value)?.create}</div>
<div className="text-xs font-medium">{modelProtocolLabel(value, availableProtocols)}</div>
<div className="truncate text-[var(--fs-tiny)] text-foreground/45">{modelProtocolDefinition(value, availableProtocols)?.create}</div>
</div>
) : (
<AdminStatusBadge label="待配置" tone="warning" />
@@ -423,9 +426,9 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
<Form.Item className="mb-0" name="capability" label="模型能力" rules={[{ required: true }]}>
<CapabilityCardPicker density="compact" />
</Form.Item>
<Form.Item className="mb-0" name="protocol" label="请求协议" rules={[{ required: true, message: "请选择模型请求协议" }]}>
<ProtocolCardPicker capability={modelCapability} density="compact" />
</Form.Item>
{availableProtocols.length ? <Form.Item className="mb-0" name="protocol" label="请求协议" rules={[{ required: true, message: "请选择模型请求协议" }]}>
<ProtocolCardPicker capability={modelCapability} density="compact" protocols={availableProtocols} />
</Form.Item> : null}
</div>
{modelCapability === "text" || modelCapability === "image" || modelCapability === "video" ? (
<Form.Item name="capabilityConfig" rules={[{ required: true, message: `请配置${capabilityLabel(modelCapability)}能力参数` }]}>

View File

@@ -1,18 +1,20 @@
import { useEffect, useState, type ReactNode } from "react";
import { App, Switch } from "antd";
import { Clapperboard, Coins, ListChecks, RadioTower, ToggleLeft, Sparkles, Settings } from "lucide-react";
import { Clapperboard, Coins, ListChecks, PlugZap, RadioTower, ToggleLeft, Sparkles, Settings } from "lucide-react";
import { getAdminFeatureAvailability, updateAdminFeatureAvailability } from "@/services/api/auth";
import { useUserStore, type FeatureAvailability } from "@/stores/use-user-store";
import { SettingsSectionCard } from "./admin-ui";
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled" | "frontendModelsEnabled";
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled" | "frontendModelsEnabled" | "pluginCenterEnabled" | "systemPluginsVisibleToUsers";
const userFeatureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
{ key: "shortDramaEnabled", title: "短剧创作", menu: "/projects", description: "关闭后隐藏短剧入口,并拦截项目列表、详情和项目 API。已有项目数据不会删除。", icon: <Clapperboard className="size-4" /> },
{ key: "taskCenterEnabled", title: "任务", menu: "/tasks", description: "关闭后仅隐藏并拦截任务中心页面;生成任务仍会创建、执行、记录和恢复。", icon: <ListChecks className="size-4" /> },
{ key: "creditsEnabled", title: "积分中心", menu: "/wallet", description: "关闭后隐藏用户积分入口,新创建的任务和系统渠道请求不再冻结或消费积分。", icon: <Coins className="size-4" /> },
{ key: "customChannelsEnabled", title: "自定义渠道", menu: "/settings?section=channels", description: "关闭后隐藏用户自定义渠道入口,并拦截模型目录拉取、渠道中转和新的生成任务。已有渠道配置不会删除。", icon: <RadioTower className="size-4" /> },
{ key: "pluginCenterEnabled", title: "插件中心", menu: "/plugins", description: "关闭后普通用户无法进入插件中心或调用插件中心相关接口;管理员仍可进入后台恢复开关。已有插件不会删除。", icon: <PlugZap className="size-4" /> },
{ key: "systemPluginsVisibleToUsers", title: "向普通用户显示系统插件(不可编辑)", menu: "/plugins", description: "仅在插件中心开启时生效。关闭后普通用户看不到系统插件,用户插件不受影响。", icon: <PlugZap className="size-4" /> },
];
const adminFeatureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
@@ -48,6 +50,8 @@ export default function FeatureAvailabilityPanel() {
creditsEnabled: next.creditsEnabled,
customChannelsEnabled: next.customChannelsEnabled,
frontendModelsEnabled: next.frontendModelsEnabled,
pluginCenterEnabled: next.pluginCenterEnabled,
systemPluginsVisibleToUsers: next.systemPluginsVisibleToUsers,
});
setFeatures(result.features);
setGlobalFeatures(result.features);
@@ -108,7 +112,7 @@ export default function FeatureAvailabilityPanel() {
<span className="rounded border border-border/70 px-1.5 py-0.5 text-[var(--fs-micro)] text-foreground/45">{item.menu}</span>
</div>
</div>
<Switch checked={features?.[item.key] === true} loading={!features || saving === item.key} disabled={Boolean(saving && saving !== item.key)} onChange={(checked) => toggle(item.key, checked)} aria-label={`开放${item.title}`} />
<Switch checked={features?.[item.key] === true} loading={!features || saving === item.key} disabled={Boolean(saving && saving !== item.key) || (item.key === "systemPluginsVisibleToUsers" && features?.pluginCenterEnabled !== true)} onChange={(checked) => toggle(item.key, checked)} aria-label={`开放${item.title}`} />
</div>
))}
</div>