fix(canvas): 渠道模型配置 - 将能力与计费编辑收进单模型抽屉

This commit is contained in:
ddCat
2026-08-06 16:46:17 +08:00
parent f17a0cb77a
commit 7960983a0c

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import { App, Button, InputNumber, Segmented } from "antd";
import { FlaskConical } from "lucide-react";
import { App, Button, Drawer, InputNumber, Segmented, Tag } from "antd";
import { ChevronRight, FlaskConical, Settings2 } from "lucide-react";
import { testChannelModelConnection } from "@/lib/model-connection-test";
import { ModelCapabilityEditor } from "@/components/model-capability-editor";
@@ -14,6 +14,7 @@ type ModelCost = NonNullable<ModelChannel["modelCosts"]>[number];
export function ChannelModelSettings({ channel, onChange }: { channel: ModelChannel; onChange: (costs: ModelCost[]) => void }) {
const { message } = App.useApp();
const [testingModel, setTestingModel] = useState("");
const [activeModel, setActiveModel] = useState<string | null>(null);
if (!channel.models.length) return null;
const updateCost = (model: string, patch: Partial<ModelCost>) => {
@@ -41,10 +42,18 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
}
};
const activeModelCost = activeModel ? channel.modelCosts?.find((item) => item.model === activeModel) : undefined;
const activeProtocol = activeModel ? activeModelCost?.protocol || defaultProtocolForModel(channel, activeModel) : undefined;
const activeCapability = activeModel ? activeModelCost?.capability || modelProtocolCapability(activeProtocol) || "text" : undefined;
const activeBillingMode = activeModelCost?.billingMode || "fixed_request";
return (
<div className="mt-3 border-t border-border/70 pt-3">
<div className="mb-2 flex items-center justify-between gap-3">
<div><div className="text-xs font-medium">模型能力与请求协议</div><div className="mt-0.5 text-[var(--fs-tiny)] text-foreground/42">与运营后台使用同一能力目录;测试会发起真实请求并可能产生供应商费用</div></div>
<div>
<div className="text-xs font-medium">模型能力与请求协议</div>
<div className="mt-0.5 text-[var(--fs-tiny)] text-foreground/42">与运营后台使用同一能力目录;测试会发起真实请求并可能产生供应商费用</div>
</div>
<span className="text-[var(--fs-tiny)] text-foreground/35">{channel.models.length} 个模型</span>
</div>
<div className="space-y-2">
@@ -55,23 +64,102 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
const capability = cost?.capability || modelProtocolCapability(protocol) || "text";
const billingMode = cost?.billingMode || "fixed_request";
return (
<div key={model} className="rounded-md border border-border/70 bg-background/45 p-2.5">
<div className="flex min-w-0 items-center justify-between gap-2">
<div className="min-w-0"><div className="truncate text-xs font-medium" title={model}>{model}</div><div className="mt-0.5 truncate font-mono text-[var(--fs-tiny)] text-foreground/40" title={modelProtocolDefinition(protocol)?.create}>{modelProtocolDefinition(protocol)?.create}</div></div>
<Button size="small" icon={<FlaskConical className="size-3.5" />} loading={testingModel === model} disabled={Boolean(testingModel && testingModel !== model)} onClick={() => void testModel(model, capability, protocol)}>测试</Button>
</div>
<div className="mt-2 space-y-2">
<ModelCapabilityProtocolModal
value={{ capability, protocol }}
onChange={({ capability: nextCapability, protocol: nextProtocol }) => updateCost(model, { protocol: nextProtocol, capability: nextCapability, billingMode: nextCapability === "video" ? billingMode : "fixed_request", capabilityConfig: nextCapability === "video" ? defaultModelCapabilityConfig(nextProtocol) : undefined })}
/>
{capability === "video" ? <div className="grid gap-2 lg:grid-cols-[176px_1fr]"><Segmented size="small" block value={billingMode} options={[{ label: "按次", value: "fixed_request" }, { label: "按秒", value: "per_second" }]} onChange={(value) => updateCost(model, { billingMode: value as ModelCost["billingMode"] })} /><InputNumber size="small" min={0} max={1_000_000} precision={6} step={0.1} className="w-full" placeholder={billingMode === "per_second" ? "每秒价格" : "每次价格"} addonAfter={`积分/${billingMode === "per_second" ? "秒" : "次"}`} value={cost ? cost.unitPriceMicrocredits / 1_000_000 : null} onChange={(value) => updateCost(model, { unitPriceMicrocredits: Math.round(Number(value || 0) * 1_000_000) })} /></div> : null}
{capability === "video" ? <ModelCapabilityEditor value={cost?.capabilityConfig || defaultModelCapabilityConfig(protocol)} protocol={protocol} onChange={(capabilityConfig) => updateCost(model, { capabilityConfig })} /> : null}
<div key={model} className="flex min-w-0 items-center gap-3 rounded-md border border-border/70 bg-background/45 px-3 py-2.5">
<span className="grid size-8 shrink-0 place-items-center rounded-md border border-border/70 bg-muted/35 text-foreground/65">
<Settings2 className="size-4" />
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium" title={model}>
{model}
</div>
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5">
<Tag className="mr-0 text-[var(--fs-tiny)]" bordered={false}>
{capabilityLabel(capability)}
</Tag>
<span className="truncate font-mono text-[var(--fs-tiny)] text-foreground/40" title={modelProtocolDefinition(protocol)?.create}>
{modelProtocolDefinition(protocol)?.create || "待配置请求协议"}
</span>
</div>
</div>
<Button type="text" size="small" icon={<ChevronRight className="size-4" />} iconPosition="end" onClick={() => setActiveModel(model)}>
配置使用
</Button>
</div>
);
})}
</div>
<Drawer
title={activeModel ? `${activeModel} · 使用配置` : "模型使用配置"}
open={Boolean(activeModel)}
onClose={() => setActiveModel(null)}
size="min(720px, 100vw)"
destroyOnHidden
extra={
activeModel && activeCapability && activeProtocol ? (
<Button
size="small"
icon={<FlaskConical className="size-3.5" />}
loading={testingModel === activeModel}
disabled={Boolean(testingModel && testingModel !== activeModel)}
onClick={() => void testModel(activeModel, activeCapability, activeProtocol)}
>
测试模型
</Button>
) : null
}
>
{activeModel && activeCapability && activeProtocol ? (
<div className="space-y-4">
<div className="rounded-md border border-border/70 bg-muted/20 px-3 py-2.5">
<div className="text-xs font-medium">模型能力与请求协议</div>
<div className="mt-1 text-[var(--fs-tiny)] text-foreground/45">这些设置只影响当前渠道的这个模型,保存后会同步到生成校验。</div>
</div>
<ModelCapabilityProtocolModal
value={{ capability: activeCapability, protocol: activeProtocol }}
onChange={({ capability: nextCapability, protocol: nextProtocol }) =>
updateCost(activeModel, {
protocol: nextProtocol,
capability: nextCapability,
billingMode: nextCapability === "video" ? activeBillingMode : "fixed_request",
capabilityConfig: nextCapability === "video" ? defaultModelCapabilityConfig(nextProtocol) : undefined,
})
}
/>
{activeCapability === "video" ? (
<div className="space-y-2">
<div className="text-xs font-medium">计费方式</div>
<div className="grid gap-2 lg:grid-cols-[176px_1fr]">
<Segmented
size="small"
block
value={activeBillingMode}
options={[
{ label: "按次", value: "fixed_request" },
{ label: "按秒", value: "per_second" },
]}
onChange={(value) => updateCost(activeModel, { billingMode: value as ModelCost["billingMode"] })}
/>
<InputNumber
size="small"
min={0}
max={1_000_000}
precision={6}
step={0.1}
className="w-full"
placeholder={activeBillingMode === "per_second" ? "每秒价格" : "每次价格"}
addonAfter={`积分/${activeBillingMode === "per_second" ? "秒" : "次"}`}
value={activeModelCost ? activeModelCost.unitPriceMicrocredits / 1_000_000 : null}
onChange={(value) => updateCost(activeModel, { unitPriceMicrocredits: Math.round(Number(value || 0) * 1_000_000) })}
/>
</div>
</div>
) : null}
{activeCapability === "video" ? (
<ModelCapabilityEditor value={activeModelCost?.capabilityConfig || defaultModelCapabilityConfig(activeProtocol)} protocol={activeProtocol} onChange={(capabilityConfig) => updateCost(activeModel, { capabilityConfig })} />
) : null}
</div>
) : null}
</Drawer>
</div>
);
}