refactor(core): 模型路由与画布 - 收敛渠道模型映射并优化交互
This commit is contained in:
@@ -29,13 +29,13 @@ export function CreditOperationsPage() {
|
||||
}
|
||||
|
||||
export function AccessSettingsPage() {
|
||||
return <AdminPageFrame title="登录与注册" description="注册策略与 Linux.do"><Suspense fallback={<PageFallback label="登录与注册配置" />}><AccessSettingsPanel /></Suspense></AdminPageFrame>;
|
||||
return <AdminPageFrame title="登录与注册" description="注册策略与 Linux.do" scroll><Suspense fallback={<PageFallback label="登录与注册配置" />}><AccessSettingsPanel /></Suspense></AdminPageFrame>;
|
||||
}
|
||||
|
||||
export function EmailSettingsPage() {
|
||||
return <AdminPageFrame title="邮件服务" description="注册验证码 SMTP"><div className="pt-4"><Suspense fallback={<PageFallback label="邮件配置" />}><EmailSettingsPanel /></Suspense></div></AdminPageFrame>;
|
||||
return <AdminPageFrame title="邮件服务" description="注册验证码 SMTP" scroll><div className="pt-4"><Suspense fallback={<PageFallback label="邮件配置" />}><EmailSettingsPanel /></Suspense></div></AdminPageFrame>;
|
||||
}
|
||||
|
||||
export function FeatureAvailabilityPage() {
|
||||
return <AdminPageFrame title="功能开放" description="控制用户工作台入口、渠道与计费模式"><Suspense fallback={<PageFallback label="功能开放配置" />}><FeatureAvailabilityPanel /></Suspense></AdminPageFrame>;
|
||||
return <AdminPageFrame title="功能开放" description="控制用户工作台入口、渠道与计费模式" scroll><Suspense fallback={<PageFallback label="功能开放配置" />}><FeatureAvailabilityPanel /></Suspense></AdminPageFrame>;
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ export function AdminShell() {
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminPageFrame({ title, actions, back, children }: { title: string; description?: string; actions?: ReactNode; back?: { label: string; onClick: () => void }; children: ReactNode }) {
|
||||
export function AdminPageFrame({ title, actions, back, scroll = false, children }: { title: string; description?: string; actions?: ReactNode; back?: { label: string; onClick: () => void }; scroll?: boolean; children: ReactNode }) {
|
||||
return (
|
||||
<WorkspacePage scroll={false} fluid className="admin-page-root">
|
||||
<div className="admin-page-frame">
|
||||
<WorkspacePage scroll={scroll} fluid className={cn("admin-page-root", scroll && "admin-page-root-scrollable")}>
|
||||
<div className={cn("admin-page-frame", scroll && "admin-page-frame-scrollable")}>
|
||||
<header className="admin-page-header flex min-h-12 shrink-0 flex-col gap-2 pb-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
{back ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { App, Button, Drawer, Form, Input, InputNumber, Popconfirm, Segmented, Select, Space, Switch } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { FlaskConical, Plus, RefreshCw, Search, Trash2 } from "lucide-react";
|
||||
@@ -7,12 +7,10 @@ import { PaginationBar } from "@/components/layout/workspace-page";
|
||||
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, type ModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { MODEL_PROTOCOLS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
|
||||
import { createAdminPhysicalVariant, listAdminPhysicalVariants, updateAdminPhysicalVariant, type AdminPhysicalVariant } from "@/services/api/logical-models";
|
||||
import { createAdminChannelModel, deleteAdminChannelModel, fetchAdminChannelModels, listAdminChannelModels, testAdminChannelModel, updateAdminChannelModel, type ChannelModel } from "@/services/api/wallet";
|
||||
import type { ModelChannel } from "@/stores/use-config-store";
|
||||
import { CapabilitySummary, capabilitySpecFromChannelModel } from "../logical-models/model-routing-capabilities";
|
||||
import { AdminPageFrame } from "./admin-shell";
|
||||
import { AdminDataTable, AdminFilterChip, AdminStatusBadge } from "./admin-ui";
|
||||
|
||||
@@ -32,11 +30,6 @@ type FormValues = {
|
||||
capabilityConfig?: ModelCapabilityConfig;
|
||||
};
|
||||
|
||||
type ConfigFormValues = {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export function ChannelModelManager({ channel, onClose, onChanged }: { channel: ModelChannel; onClose: () => void; onChanged: () => void | Promise<void> }) {
|
||||
const { message } = App.useApp();
|
||||
const [items, setItems] = useState<ChannelModel[]>([]);
|
||||
@@ -46,24 +39,16 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [configDrawerOpen, setConfigDrawerOpen] = useState(false);
|
||||
const [config, setConfig] = useState<AdminPhysicalVariant | null>(null);
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [capability, setCapability] = useState<ChannelModel["capability"] | "all">("all");
|
||||
const [status, setStatus] = useState<"all" | "enabled" | "disabled">("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [configForm] = Form.useForm<ConfigFormValues>();
|
||||
const configRequestRef = useRef(0);
|
||||
const billingMode = Form.useWatch("billingMode", form) || "fixed_request";
|
||||
const modelCapability = Form.useWatch("capability", form);
|
||||
const modelProtocol = Form.useWatch("protocol", form);
|
||||
const modelKey = Form.useWatch("modelKey", form) || "";
|
||||
const editingCapabilitySpec = capabilitySpecFromChannelModel(editing || undefined);
|
||||
const currentConfig = editing && config?.channelModelId === editing.id ? config : null;
|
||||
|
||||
const reload = async () => {
|
||||
if (!channel) return;
|
||||
@@ -77,69 +62,9 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
}
|
||||
};
|
||||
|
||||
const reloadConfig = async (channelModelID: string, defaultName = "") => {
|
||||
const requestID = ++configRequestRef.current;
|
||||
setConfigLoading(true);
|
||||
try {
|
||||
const variants = (await listAdminPhysicalVariants(channelModelID)).variants;
|
||||
if (requestID !== configRequestRef.current) return;
|
||||
const nextConfig = variants.find((item) => item.channelModelId === channelModelID) || null;
|
||||
setConfig(nextConfig);
|
||||
if (nextConfig) {
|
||||
configForm.setFieldsValue({ name: nextConfig.name, enabled: nextConfig.enabled });
|
||||
} else {
|
||||
configForm.setFieldsValue({ name: defaultName, enabled: true });
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestID === configRequestRef.current) message.error(error instanceof Error ? error.message : "读取可用配置失败");
|
||||
} finally {
|
||||
if (requestID === configRequestRef.current) setConfigLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!editing) {
|
||||
message.info("请先保存渠道模型,再配置唯一可用配置");
|
||||
return;
|
||||
}
|
||||
const values = await configForm.validateFields();
|
||||
if (values.enabled && editing.capability !== "audio" && !editingCapabilitySpec) {
|
||||
message.error("请先完成渠道模型的供应能力配置");
|
||||
return;
|
||||
}
|
||||
if (values.enabled && !editing.enabled) {
|
||||
message.error("渠道模型已停用,不能启用可用配置");
|
||||
return;
|
||||
}
|
||||
if (config && !currentConfig) {
|
||||
message.error("可用配置与当前渠道模型不匹配,请重新打开后再试");
|
||||
return;
|
||||
}
|
||||
setConfigSaving(true);
|
||||
try {
|
||||
// 能力范围只维护在渠道模型能力参数中;可用配置只是路由开关和显示名称。
|
||||
// 后端会从渠道模型能力生成 variant 快照,避免同一尺寸/比例被填写两次。
|
||||
const payload = { channelModelId: editing.id, name: values.name.trim(), enabled: values.enabled };
|
||||
if (currentConfig) await updateAdminPhysicalVariant(currentConfig.id, payload);
|
||||
else await createAdminPhysicalVariant(payload);
|
||||
await reloadConfig(editing.id, editing.displayName || editing.modelKey);
|
||||
await onChanged();
|
||||
message.success(currentConfig ? "可用配置已更新" : "已创建唯一可用配置");
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "保存可用配置失败");
|
||||
} finally {
|
||||
setConfigSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
configRequestRef.current += 1;
|
||||
void reload();
|
||||
setEditing(null);
|
||||
setConfig(null);
|
||||
setConfigLoading(false);
|
||||
setConfigDrawerOpen(false);
|
||||
configForm.resetFields();
|
||||
setEditorOpen(false);
|
||||
setKeyword("");
|
||||
setCapability("all");
|
||||
@@ -165,12 +90,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
};
|
||||
|
||||
const startCreate = () => {
|
||||
configRequestRef.current += 1;
|
||||
setEditing(null);
|
||||
setConfig(null);
|
||||
setConfigLoading(false);
|
||||
setConfigDrawerOpen(false);
|
||||
configForm.resetFields();
|
||||
form.setFieldsValue({
|
||||
modelKey: "",
|
||||
displayName: "",
|
||||
@@ -189,10 +109,6 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
|
||||
const startEdit = (item: ChannelModel) => {
|
||||
setEditing(item);
|
||||
setConfig(null);
|
||||
setConfigDrawerOpen(false);
|
||||
configForm.setFieldsValue({ name: item.displayName || item.modelKey, enabled: true });
|
||||
void reloadConfig(item.id, item.displayName || item.modelKey);
|
||||
form.setFieldsValue({
|
||||
modelKey: item.modelKey,
|
||||
displayName: item.displayName,
|
||||
@@ -204,7 +120,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
outputTokenPrice: item.outputTokenPriceMicrocredits / 1_000_000,
|
||||
cachedTokenPrice: item.cachedTokenPriceMicrocredits / 1_000_000,
|
||||
enabled: item.enabled,
|
||||
capabilityConfig: item.capability === "text" || item.capability === "image" || item.capability === "video" ? item.capabilityConfig || defaultModelCapabilityConfig(item.protocol, item.modelKey) : undefined,
|
||||
capabilityConfig: item.capability === "text" || item.capability === "image" || item.capability === "video" ? normalizeModelCapabilityConfig(item.capabilityConfig || defaultModelCapabilityConfig(item.protocol, item.modelKey)) : undefined,
|
||||
});
|
||||
setEditorOpen(true);
|
||||
};
|
||||
@@ -225,7 +141,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
cachedTokenPriceMicrocredits: Math.round((values.cachedTokenPrice || 0) * 1_000_000),
|
||||
priceConfigured: true,
|
||||
enabled: values.enabled !== false,
|
||||
capabilityConfig: values.capability === "text" || values.capability === "image" || values.capability === "video" ? values.capabilityConfig : undefined,
|
||||
capabilityConfig: values.capability === "text" || values.capability === "image" || values.capability === "video" ? normalizeModelCapabilityConfig(values.capabilityConfig!) : undefined,
|
||||
};
|
||||
if (editing) await updateAdminChannelModel(channel.id, editing.id, payload);
|
||||
else await createAdminChannelModel(channel.id, payload);
|
||||
@@ -249,7 +165,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
modelKey: values.modelKey.trim(),
|
||||
capability: values.capability,
|
||||
protocol: values.protocol,
|
||||
capabilityConfig: values.capabilityConfig,
|
||||
capabilityConfig: values.capabilityConfig ? normalizeModelCapabilityConfig(values.capabilityConfig) : undefined,
|
||||
});
|
||||
message.success(`模型测试通过,耗时 ${(result.durationMs / 1000).toFixed(2)} 秒`);
|
||||
} catch (error) {
|
||||
@@ -330,7 +246,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
<Button size="small" onClick={() => startEdit(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="删除模型" description="删除后模型不再显示,历史账单仍会保留。该操作不能在页面恢复。" okText="删除" cancelText="取消" onConfirm={() => void remove(item)}>
|
||||
<Popconfirm title="删除模型" description="已被前台供应线路或进行中任务使用的模型不能删除;删除后模型不再显示,且不能在页面恢复。" okText="删除" cancelText="取消" onConfirm={() => void remove(item)}>
|
||||
<Button size="small" danger title="删除模型" aria-label="删除模型" icon={<Trash2 className="size-3.5" />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
@@ -421,7 +337,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
title={editing ? `编辑模型 / ${editing.displayName || editing.modelKey}` : "新增模型"}
|
||||
open={editorOpen}
|
||||
size="min(1080px, 100vw)"
|
||||
onClose={() => !configSaving && setEditorOpen(false)}
|
||||
onClose={() => !saving && setEditorOpen(false)}
|
||||
rootClassName="admin-drawer"
|
||||
footer={
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -481,34 +397,6 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="admin-form-section">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-sm font-semibold">可用配置</h2>
|
||||
</div>
|
||||
{!editing ? (
|
||||
<div className="rounded-md bg-muted/20 px-4 py-4 text-sm text-foreground/60">请先保存模型基础信息,保存后即可配置唯一的路由入口。</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg bg-muted/15 p-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{currentConfig?.name || "尚未创建路由配置"}</span>
|
||||
{currentConfig ? <AdminStatusBadge label={currentConfig.enabled ? "启用" : "停用"} tone={currentConfig.enabled ? "success" : "neutral"} /> : null}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-foreground/45">{currentConfig?.usageCount ? `已被 ${currentConfig.usageCount} 条前台供应配置引用` : "尚未被前台供应配置引用"}</div>
|
||||
</div>
|
||||
<Button
|
||||
loading={configLoading}
|
||||
onClick={() => {
|
||||
setConfigDrawerOpen(true);
|
||||
void reloadConfig(editing.id, editing.displayName || editing.modelKey);
|
||||
}}
|
||||
>
|
||||
编辑配置
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-form-section">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-sm font-semibold">计费</h2>
|
||||
@@ -558,32 +446,6 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
</section>
|
||||
</Form>
|
||||
</Drawer>
|
||||
<Drawer
|
||||
title="可用配置"
|
||||
open={configDrawerOpen}
|
||||
size="min(560px, 100vw)"
|
||||
onClose={() => !configSaving && setConfigDrawerOpen(false)}
|
||||
rootClassName="admin-secondary-drawer"
|
||||
footer={<div className="flex justify-end gap-2"><Button onClick={() => setConfigDrawerOpen(false)}>取消</Button><Button type="primary" loading={configSaving} onClick={() => void saveConfig()}>保存路由配置</Button></div>}
|
||||
>
|
||||
<Form form={configForm} layout="vertical" requiredMark={false}>
|
||||
<div className="mb-5 rounded-lg bg-muted/15 p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">唯一可用配置</span>
|
||||
<AdminStatusBadge label={currentConfig ? (currentConfig.enabled ? "启用" : "停用") : "未创建"} tone={currentConfig?.enabled ? "success" : "neutral"} />
|
||||
</div>
|
||||
<span className="text-xs text-foreground/45">{currentConfig?.usageCount ? `引用 ${currentConfig.usageCount}` : "未引用"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item name="name" label="配置名称" rules={[{ required: true, message: "请填写配置名称" }]}><Input placeholder="例如:标准能力、高清能力、多参考图" /></Form.Item>
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
|
||||
<div className="rounded-lg bg-muted/10 px-4 py-4">
|
||||
<div className="mb-2 text-xs font-semibold text-foreground/65">继承的模型能力</div>
|
||||
{editingCapabilitySpec ? <CapabilitySummary spec={editingCapabilitySpec} /> : <span className="text-xs text-foreground/45">请先在“能力与协议”中完成模型能力参数。</span>}
|
||||
</div>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</AdminPageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useDeferredValue, useEffect, useMemo, useState, type ReactNode } from "
|
||||
|
||||
import { PaginationBar } from "@/components/layout/workspace-page";
|
||||
import { ModelIconPicker, ModelLogo } from "@/components/model-logo";
|
||||
import { CapabilityCardPicker } from "@/components/model-protocol-picker";
|
||||
import { formatCredits } from "@/constant/credits";
|
||||
import { AdminPageFrame } from "@/pages/admin/components/admin-shell";
|
||||
import { AdminDataTable, AdminFilterChip, AdminRowActions, AdminStatusBadge, AdminTableEmpty } from "@/pages/admin/components/admin-ui";
|
||||
import { listAdminChannels } from "@/services/api/auth";
|
||||
import { listAdminChannelModels, type ChannelModel } from "@/services/api/wallet";
|
||||
import {
|
||||
createAdminLogicalModel,
|
||||
listAdminLogicalModels,
|
||||
listAdminPhysicalVariants,
|
||||
simulateAdminLogicalModel,
|
||||
updateAdminLogicalModel,
|
||||
type AdminLogicalModel,
|
||||
type AdminPhysicalVariant,
|
||||
type CapabilitySpec,
|
||||
type LogicalModelMutation,
|
||||
type ModelRequestIntent,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
CapabilitySummary,
|
||||
DefaultOptionsEditor,
|
||||
capabilityLabel,
|
||||
capabilitySpecFromChannelModel,
|
||||
capabilitySourceError,
|
||||
emptyCapabilitySpec,
|
||||
mergeCapabilitySpecs,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
type CapabilityKind,
|
||||
} from "./model-routing-capabilities";
|
||||
|
||||
type RouteRuleRow = { physicalVariantId: string; enabled: boolean; priority: number; weight: number };
|
||||
type RouteRuleRow = { channelModelId: string; enabled: boolean; priority: number; weight: number };
|
||||
type LogicalModelFormValues = {
|
||||
code: string;
|
||||
name: string;
|
||||
@@ -57,13 +58,6 @@ type LogicalModelFormValues = {
|
||||
defaultOptions: Record<string, unknown>;
|
||||
routes: RouteRuleRow[];
|
||||
};
|
||||
const capabilityOptions = [
|
||||
{ label: "文本", value: "text" },
|
||||
{ label: "图片", value: "image" },
|
||||
{ label: "视频", value: "video" },
|
||||
{ label: "音频", value: "audio" },
|
||||
];
|
||||
|
||||
export default function LogicalModelsPage() {
|
||||
const { message } = App.useApp();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
@@ -72,7 +66,7 @@ export default function LogicalModelsPage() {
|
||||
const deferredKeyword = useDeferredValue(keyword.trim().toLowerCase());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [models, setModels] = useState<AdminLogicalModel[]>([]);
|
||||
const [variants, setVariants] = useState<AdminPhysicalVariant[]>([]);
|
||||
const [channelModels, setChannelModels] = useState<ChannelModel[]>([]);
|
||||
const [channelNames, setChannelNames] = useState<Record<string, string>>({});
|
||||
const [channelEnabled, setChannelEnabled] = useState<Record<string, boolean>>({});
|
||||
const [editingModel, setEditingModel] = useState<AdminLogicalModel | null | undefined>();
|
||||
@@ -89,11 +83,16 @@ export default function LogicalModelsPage() {
|
||||
const reload = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [modelResult, variantResult, channelResult] = await Promise.all([listAdminLogicalModels(), listAdminPhysicalVariants(), listAdminChannels({ page: 1, limit: 100 })]);
|
||||
const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, limit: 100 })]);
|
||||
const remainingChannelPages = await Promise.all(
|
||||
Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.limit) - 1) }, (_, index) => listAdminChannels({ page: index + 2, limit: firstChannelPage.limit })),
|
||||
);
|
||||
const channels = [firstChannelPage, ...remainingChannelPages].flatMap((result) => result.channels);
|
||||
const channelModelResults = await Promise.all(channels.map((channel) => listAdminChannelModels(channel.id)));
|
||||
setModels(modelResult.models);
|
||||
setVariants(variantResult.variants);
|
||||
setChannelNames(Object.fromEntries(channelResult.channels.map((channel) => [channel.id, channel.name])));
|
||||
setChannelEnabled(Object.fromEntries(channelResult.channels.map((channel) => [channel.id, channel.enabled !== false])));
|
||||
setChannelModels(channelModelResults.flatMap((result) => result.models));
|
||||
setChannelNames(Object.fromEntries(channels.map((channel) => [channel.id, channel.name])));
|
||||
setChannelEnabled(Object.fromEntries(channels.map((channel) => [channel.id, channel.enabled !== false])));
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "读取前台模型配置失败");
|
||||
} finally {
|
||||
@@ -107,14 +106,15 @@ export default function LogicalModelsPage() {
|
||||
|
||||
const filteredModels = useMemo(() => models.filter((item) => !deferredKeyword || [item.name, item.code, item.capability].some((value) => value.toLowerCase().includes(deferredKeyword))), [models, deferredKeyword]);
|
||||
const paginatedModels = useMemo(() => filteredModels.slice((page - 1) * pageSize, page * pageSize), [filteredModels, page, pageSize]);
|
||||
const modelVariants = useMemo(() => variants.filter((item) => item.capability === modelCapability), [variants, modelCapability]);
|
||||
const modelChannelModels = useMemo(() => channelModels.filter((item) => item.capability === modelCapability), [channelModels, modelCapability]);
|
||||
const modelSourceSpecs = useMemo(
|
||||
() =>
|
||||
modelRoutes
|
||||
.filter((route) => route.enabled && route.weight > 0)
|
||||
.map((route) => variants.find((item) => item.id === route.physicalVariantId && item.enabled && item.modelEnabled && channelEnabled[item.channelId] !== false)?.capabilitySpec)
|
||||
.map((route) => channelModels.find((item) => item.id === route.channelModelId && item.enabled && channelEnabled[item.channelId] !== false))
|
||||
.map((item) => (item ? capabilitySpecFromChannelModel(item) : undefined))
|
||||
.filter((item): item is CapabilitySpec => Boolean(item)),
|
||||
[channelEnabled, modelRoutes, variants],
|
||||
[channelEnabled, channelModels, modelRoutes],
|
||||
);
|
||||
|
||||
const openModel = (item?: AdminLogicalModel) => {
|
||||
@@ -148,7 +148,7 @@ export default function LogicalModelsPage() {
|
||||
const saveModel = async () => {
|
||||
const values = await modelForm.validateFields();
|
||||
if (values.enabled && !values.routes.length) {
|
||||
message.error("请至少添加一条供应配置");
|
||||
message.error("请至少添加一条供应线路");
|
||||
return;
|
||||
}
|
||||
const sourceError = capabilitySourceError(values.capability, modelSourceSpecs, values.capabilitySpec);
|
||||
@@ -218,9 +218,9 @@ export default function LogicalModelsPage() {
|
||||
),
|
||||
},
|
||||
{ title: "类型", dataIndex: "capability", width: 90, render: (value: CapabilityKind) => capabilityLabel(value) },
|
||||
{ title: "创作端能力", render: (_, item) => <CapabilitySummary spec={item.capabilitySpec} /> },
|
||||
{ title: "创作端能力", width: 360, render: (_, item) => <CapabilitySummary spec={item.capabilitySpec} /> },
|
||||
{
|
||||
title: "供应配置",
|
||||
title: "供应线路",
|
||||
width: 110,
|
||||
render: (_, item) => (
|
||||
<div className="text-xs">
|
||||
@@ -239,7 +239,7 @@ export default function LogicalModelsPage() {
|
||||
<AdminRowActions
|
||||
primary={{ label: "编辑", icon: <Pencil className="size-3.5" />, onClick: () => openModel(item) }}
|
||||
actions={[
|
||||
{ key: "simulate", label: "模拟供应配置匹配", icon: <FlaskConical className="size-3.5" />, onClick: () => openSimulation(item) },
|
||||
{ key: "simulate", label: "模拟供应线路匹配", icon: <FlaskConical className="size-3.5" />, onClick: () => openSimulation(item) },
|
||||
{ key: "toggle", label: item.enabled ? "停用" : "启用", onClick: () => void toggleModel(item) },
|
||||
]}
|
||||
/>
|
||||
@@ -276,13 +276,28 @@ export default function LogicalModelsPage() {
|
||||
rootClassName="admin-drawer"
|
||||
footer={<div className="flex justify-end gap-2"><Button disabled={saving} onClick={() => setEditingModel(undefined)}>取消</Button><Button type="primary" loading={saving} onClick={() => void saveModel()}>保存</Button></div>}
|
||||
>
|
||||
<Form form={modelForm} layout="vertical" requiredMark={false} className="space-y-3">
|
||||
<Form
|
||||
form={modelForm}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
className="space-y-3"
|
||||
onValuesChange={(changedValues: Partial<LogicalModelFormValues>) => {
|
||||
const capability = changedValues.capability;
|
||||
if (!capability) return;
|
||||
modelForm.setFieldsValue({
|
||||
routes: [],
|
||||
capabilitySpec: emptyCapabilitySpec(capability),
|
||||
defaultOptions: {},
|
||||
billingMode: capability === "text" ? "token" : capability === "video" ? "per_second" : "fixed_request",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{editingModel?.configurationError || editingModel?.availabilityError ? (
|
||||
<Alert
|
||||
className="mb-4"
|
||||
type="warning"
|
||||
showIcon
|
||||
message={editingModel.configurationError ? "当前供应配置无法覆盖全部创作端能力" : "当前供应配置暂不可结算"}
|
||||
message={editingModel.configurationError ? "当前供应线路无法覆盖全部创作端能力" : "当前供应线路暂不可结算"}
|
||||
description={editingModel.configurationError || editingModel.availabilityError}
|
||||
/>
|
||||
) : null}
|
||||
@@ -301,15 +316,10 @@ export default function LogicalModelsPage() {
|
||||
<Form.Item name="description" label="简短说明">
|
||||
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} placeholder="说明适合的创作场景,不描述供应渠道。" />
|
||||
</Form.Item>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Form.Item name="capability" label="类型">
|
||||
<Select
|
||||
options={capabilityOptions}
|
||||
onChange={(capability: CapabilityKind) =>
|
||||
modelForm.setFieldsValue({ routes: [], capabilitySpec: emptyCapabilitySpec(capability), defaultOptions: {}, billingMode: capability === "text" ? "token" : capability === "video" ? "per_second" : "fixed_request" })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="capability" label="类型">
|
||||
<CapabilityCardPicker density="compact" />
|
||||
</Form.Item>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Form.Item name="sortOrder" label="前台排序">
|
||||
<InputNumber className="w-full" precision={0} />
|
||||
</Form.Item>
|
||||
@@ -318,8 +328,8 @@ export default function LogicalModelsPage() {
|
||||
</Form.Item>
|
||||
</div>
|
||||
</DrawerSection>
|
||||
<DrawerSection icon={<GitBranch className="size-4" />} title="供应配置">
|
||||
<RouteFields variants={modelVariants} channelNames={channelNames} channelEnabled={channelEnabled} form={modelForm} capability={modelCapability} />
|
||||
<DrawerSection icon={<GitBranch className="size-4" />} title="供应线路">
|
||||
<RouteFields channelModels={modelChannelModels} channelNames={channelNames} channelEnabled={channelEnabled} form={modelForm} capability={modelCapability} />
|
||||
</DrawerSection>
|
||||
<DrawerSection title="创作端可选能力">
|
||||
<Form.Item name="capabilitySpec" noStyle>
|
||||
@@ -338,7 +348,7 @@ export default function LogicalModelsPage() {
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title={simulatingModel ? `供应配置匹配模拟 - ${simulatingModel.name}` : "供应配置匹配模拟"}
|
||||
title={simulatingModel ? `供应线路匹配模拟 - ${simulatingModel.name}` : "供应线路匹配模拟"}
|
||||
open={Boolean(simulatingModel)}
|
||||
className="workspace-modal workspace-modal-wide admin-simulation-modal"
|
||||
rootClassName="admin-modal-root"
|
||||
@@ -382,7 +392,7 @@ export default function LogicalModelsPage() {
|
||||
</Tag>
|
||||
</div>
|
||||
{simulationResult.productMatch.reasons?.length ? <p className="mb-4 text-sm text-error">{simulationResult.productMatch.reasons.join(";")}</p> : null}
|
||||
<Table size="small" pagination={false} rowKey="routeId" dataSource={simulationResult.candidates} columns={simulationColumns(simulatingModel)} />
|
||||
<Table size="small" pagination={false} rowKey="routeId" dataSource={simulationResult.candidates} columns={simulationColumns()} />
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -408,74 +418,75 @@ function DrawerSection({ icon, title, description, children }: { icon?: ReactNod
|
||||
}
|
||||
|
||||
function RouteFields({
|
||||
variants,
|
||||
channelModels,
|
||||
channelNames,
|
||||
channelEnabled,
|
||||
form,
|
||||
capability,
|
||||
}: {
|
||||
variants: AdminPhysicalVariant[];
|
||||
channelModels: ChannelModel[];
|
||||
channelNames: Record<string, string>;
|
||||
channelEnabled: Record<string, boolean>;
|
||||
form: FormInstance<LogicalModelFormValues>;
|
||||
capability: CapabilityKind;
|
||||
}) {
|
||||
const selectOptions = variants.map((item) => {
|
||||
const unavailableReason = channelEnabled[item.channelId] === false ? "渠道已停用" : !item.modelEnabled ? "渠道模型已停用" : !item.enabled ? "规格已停用" : "";
|
||||
const selectOptions = channelModels.map((item) => {
|
||||
const unavailableReason = channelEnabled[item.channelId] === false ? "渠道已停用" : !item.enabled ? "渠道模型已停用" : "";
|
||||
return {
|
||||
value: item.id,
|
||||
label: `${item.name} · ${channelNames[item.channelId] || item.channelId} / ${item.modelName || item.modelKey}${unavailableReason ? `(${unavailableReason})` : ""}`,
|
||||
label: `${channelNames[item.channelId]} / ${item.displayName || item.modelKey}${unavailableReason ? `(${unavailableReason})` : ""}`,
|
||||
disabled: Boolean(unavailableReason),
|
||||
};
|
||||
});
|
||||
const availableVariantCount = selectOptions.filter((item) => !item.disabled).length;
|
||||
const availableChannelModelCount = selectOptions.filter((item) => !item.disabled).length;
|
||||
return (
|
||||
<Form.List name="routes">
|
||||
{(fields, { add, remove }) => {
|
||||
const currentRoutes = (form.getFieldValue("routes") || []) as RouteRuleRow[];
|
||||
const selectedVariantCount = new Set(currentRoutes.map((route) => route?.physicalVariantId).filter(Boolean)).size;
|
||||
const canAdd = selectedVariantCount < availableVariantCount;
|
||||
const selectedChannelModelCount = new Set(currentRoutes.map((route) => route?.channelModelId).filter(Boolean)).size;
|
||||
const canAdd = selectedChannelModelCount < availableChannelModelCount;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-foreground/50">共 {fields.length} 条供应配置</span>
|
||||
<Button size="small" icon={<Plus className="size-3.5" />} disabled={!canAdd} onClick={() => add({ physicalVariantId: "", enabled: true, priority: 100, weight: 100 })}>
|
||||
添加供应配置
|
||||
<span className="text-xs text-foreground/50">共 {fields.length} 条供应线路</span>
|
||||
<Button size="small" icon={<Plus className="size-3.5" />} disabled={!canAdd} onClick={() => add({ channelModelId: "", enabled: true, priority: 100, weight: 100 })}>
|
||||
添加供应线路
|
||||
</Button>
|
||||
</div>
|
||||
{fields.length ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{fields.map((field) => {
|
||||
const routes = (form.getFieldValue("routes") || []) as RouteRuleRow[];
|
||||
const selectedByOthers = new Set(routes.map((route, index) => (index === field.name ? "" : route?.physicalVariantId)).filter(Boolean));
|
||||
const selectedByOthers = new Set(routes.map((route, index) => (index === field.name ? "" : route?.channelModelId)).filter(Boolean));
|
||||
const options = selectOptions.map((option) => ({ ...option, disabled: option.disabled || selectedByOthers.has(option.value) }));
|
||||
const selected = variants.find((item) => item.id === routes[field.name]?.physicalVariantId);
|
||||
const selected = channelModels.find((item) => item.id === routes[field.name]?.channelModelId);
|
||||
return (
|
||||
<div key={field.key} className="rounded-lg border border-border bg-muted/5 p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold">供应配置 {fields.indexOf(field) + 1}</div>
|
||||
<div className="text-xs font-semibold">供应线路 {fields.indexOf(field) + 1}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-foreground/50">
|
||||
{selected ? `${selected.name} · ${channelNames[selected.channelId] || selected.channelId} / ${selected.modelName || selected.modelKey}` : "选择一个可承接请求的配置"}
|
||||
{selected ? `${channelNames[selected.channelId]} / ${selected.displayName || selected.modelKey}` : "选择一个可承接请求的渠道模型"}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="text" size="small" danger onClick={() => remove(field.name)}>
|
||||
移除
|
||||
</Button>
|
||||
</div>
|
||||
<Form.Item name={[field.name, "physicalVariantId"]} rules={[{ required: true, message: "请选择供应配置" }]} className="mb-3">
|
||||
<Form.Item name={[field.name, "channelModelId"]} rules={[{ required: true, message: "请选择渠道模型" }]} className="mb-3">
|
||||
<Select
|
||||
aria-label={`供应配置 ${fields.indexOf(field) + 1}`}
|
||||
aria-label={`供应线路 ${fields.indexOf(field) + 1}`}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择供应配置"
|
||||
placeholder="选择渠道模型"
|
||||
options={options}
|
||||
onChange={(physicalVariantId) => {
|
||||
onChange={(channelModelId) => {
|
||||
const nextRoutes = [...(form.getFieldValue("routes") || [])];
|
||||
nextRoutes[field.name] = { ...nextRoutes[field.name], physicalVariantId };
|
||||
nextRoutes[field.name] = { ...nextRoutes[field.name], channelModelId };
|
||||
const specs = nextRoutes
|
||||
.filter((route) => route.enabled && route.weight > 0)
|
||||
.map((route) => variants.find((item) => item.id === route.physicalVariantId && item.enabled && item.modelEnabled && channelEnabled[item.channelId] !== false)?.capabilitySpec)
|
||||
.map((route) => channelModels.find((item) => item.id === route.channelModelId && item.enabled && channelEnabled[item.channelId] !== false))
|
||||
.map((item) => (item ? capabilitySpecFromChannelModel(item) : undefined))
|
||||
.filter((item): item is CapabilitySpec => Boolean(item));
|
||||
form.setFieldValue("routes", nextRoutes);
|
||||
if (!hasCapabilityRules(form.getFieldValue("capabilitySpec"))) form.setFieldValue("capabilitySpec", mergeCapabilitySpecs(capability, specs));
|
||||
@@ -498,7 +509,7 @@ function RouteFields({
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{!fields.length ? <div className="rounded-md bg-muted/20 px-3 py-4 text-center text-xs text-foreground/50">尚未添加供应配置</div> : null}
|
||||
{!fields.length ? <div className="rounded-md bg-muted/20 px-3 py-4 text-center text-xs text-foreground/50">尚未添加供应线路</div> : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -569,7 +580,7 @@ function PricingFields() {
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-md bg-muted/20 px-3 py-3 text-xs leading-5 text-foreground/55">用户费用按实际命中的供应配置价格计算。故障切换到更高价格配置时会重新校验余额。</div>
|
||||
<div className="rounded-md bg-muted/20 px-3 py-3 text-xs leading-5 text-foreground/55">用户费用按实际命中的供应线路价格计算。故障切换到更高价格线路时会重新校验余额。</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -613,7 +624,7 @@ function logicalModelToForm(item: AdminLogicalModel): LogicalModelFormValues {
|
||||
cachedPriceMicrocredits: item.cachedPriceMicrocredits,
|
||||
capabilitySpec: item.capabilitySpec,
|
||||
defaultOptions: item.defaultOptions,
|
||||
routes: item.routes.map((route) => ({ physicalVariantId: route.physicalVariantId, enabled: route.enabled, priority: route.priority, weight: route.weight })),
|
||||
routes: item.routes.map((route) => ({ channelModelId: route.channelModelId, enabled: route.enabled, priority: route.priority, weight: route.weight })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -643,9 +654,12 @@ function hasCapabilityRules(spec?: CapabilitySpec) {
|
||||
return Boolean(spec && ((spec.operations?.length || 0) > 0 || Object.keys(spec.inputs || {}).length > 0 || Object.keys(spec.options || {}).length > 0));
|
||||
}
|
||||
|
||||
function simulationColumns(model: AdminLogicalModel): ColumnsType<RouteSimulationResult["candidates"][number]> {
|
||||
function simulationColumns(): ColumnsType<RouteSimulationResult["candidates"][number]> {
|
||||
return [
|
||||
{ title: "供应配置", render: (_, candidate) => model.routes.find((route) => route.id === candidate.routeId)?.physicalVariantName || "未知配置" },
|
||||
{
|
||||
title: "供应线路",
|
||||
render: (_, candidate) => `${candidate.channelModelName}(${candidate.channelModelKey})`,
|
||||
},
|
||||
{ title: "优先级", dataIndex: "priority", width: 80 },
|
||||
{ title: "权重", dataIndex: "weight", width: 70 },
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ export function emptyCapabilitySpec(capability: CapabilityKind): CapabilitySpec
|
||||
}
|
||||
|
||||
function imageSizeOptionValues(values: string[], allowCustom: boolean) {
|
||||
const concreteValues = Array.from(new Set(values.map((value) => value.trim()).filter((value) => value && value !== "*")));
|
||||
const concreteValues = Array.from(new Set(values.map((value) => String(normalizeScalar(value)).trim()).filter((value) => value && value !== "*")));
|
||||
const result = concreteValues.length || !allowCustom ? concreteValues : [...STANDARD_IMAGE_SIZE_VALUES];
|
||||
if (allowCustom) result.push("*");
|
||||
return result;
|
||||
@@ -437,11 +437,13 @@ export function CapabilitySummary({ spec }: { spec: CapabilitySpec }) {
|
||||
if (spec.operations?.length) labels.unshift(spec.operations.map(operationLabel).join("/"));
|
||||
if (!labels.length) return <span className="text-xs text-foreground/45">基础能力</span>;
|
||||
return (
|
||||
<div className="flex max-w-lg flex-wrap gap-1">
|
||||
<div className="flex min-w-0 max-w-full flex-wrap gap-1">
|
||||
{labels.slice(0, 4).map((label) => (
|
||||
<Tag key={label}>{label}</Tag>
|
||||
<Tag key={label} className="max-w-full whitespace-normal break-all text-left leading-5">
|
||||
{label}
|
||||
</Tag>
|
||||
))}
|
||||
{labels.length > 4 ? <Tag>+{labels.length - 4}</Tag> : null}
|
||||
{labels.length > 4 ? <Tag className="shrink-0">+{labels.length - 4}</Tag> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function DrawingEngineSettingsPage() {
|
||||
|
||||
const dirty = engine !== current.defaultEngine || tldrawLicenseKey !== (current.tldrawLicenseKey || "");
|
||||
return (
|
||||
<AdminPageFrame title="绘图工具" description="设置新建绘图节点使用的默认编辑器">
|
||||
<AdminPageFrame title="绘图工具" description="设置新建绘图节点使用的默认编辑器" scroll>
|
||||
<div className="pt-4">
|
||||
<SettingsSectionCard
|
||||
icon={<Brush className="size-4" />}
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function LibTVSettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminPageFrame title="第三方参数配置" description="集中维护外部平台的服务端凭证和连接状态,后续平台可在此继续扩展。">
|
||||
<AdminPageFrame title="第三方参数配置" description="集中维护外部平台的服务端凭证和连接状态,后续平台可在此继续扩展。" scroll>
|
||||
<div className="pt-4">
|
||||
<SettingsSectionCard
|
||||
icon={<KeyRound className="size-4" />}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function ResponseInterceptionSettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminPageFrame title="模型响应拦截">
|
||||
<AdminPageFrame title="模型响应拦截" scroll>
|
||||
<div className="pt-4">
|
||||
<SettingsSectionCard
|
||||
layout="stacked"
|
||||
|
||||
@@ -162,6 +162,7 @@ export default function RuntimePolicySettingsPage() {
|
||||
<AdminPageFrame
|
||||
title="资源与策略"
|
||||
description="账号配额、任务调度与请求安全策略"
|
||||
scroll
|
||||
actions={<div className="flex items-center gap-2"><Button icon={<RotateCcw className="size-4" />} disabled={loading || saving} onClick={reset}>重置</Button><Button icon={<InfinityIcon className="size-4" />} disabled={loading || saving} onClick={() => void useSelfMode()}>自用模式</Button></div>}
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false} disabled={loading} onValuesChange={() => setDirty(true)}>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function StorageSettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminPageFrame title="存储服务" description="配置新增资源的默认存储位置">
|
||||
<AdminPageFrame title="存储服务" description="配置新增资源的默认存储位置" scroll>
|
||||
<div className="space-y-4 pt-4">
|
||||
<SettingsSectionCard
|
||||
layout="stacked"
|
||||
|
||||
Reference in New Issue
Block a user