import { App, Button, Form, Input, Popconfirm, Segmented, Select, Tooltip } from "antd"; import { Pencil, Plus, RefreshCw, Trash2, Workflow } from "lucide-react"; import { useState, type ReactNode } from "react"; import { ModelEditorModal } from "@/components/model-editor-modal"; import { ChannelHeadersEditor, validateChannelHeaders } from "@/components/channel-headers-editor"; import { WorkspaceState } from "@/components/layout/workspace-state"; import { mergeFetchedChannelModelCosts } from "@/lib/channel-model-catalog"; import { fetchChannelModels } from "@/services/api/image"; import { createModelChannel, defaultBaseUrlForApiFormat, filterModelsByCapability, modelOptionsFromChannels, useConfigStore, type AiConfig, type ModelChannel, } from "@/stores/use-config-store"; import { ChannelModelSettings } from "./channel-video-pricing"; type UserChannelConnection = "openai" | "gemini"; type ChannelSettingsPaneProps = { onOpenModels: () => void; onOpenRunningHub?: () => void; }; export function ChannelSettingsPane({ onOpenModels, onOpenRunningHub }: ChannelSettingsPaneProps) { const { message } = App.useApp(); const config = useConfigStore((state) => state.config); const replaceConfig = useConfigStore((state) => state.replaceConfig); const [loadingChannelIds, setLoadingChannelIds] = useState([]); const [editingChannelId, setEditingChannelId] = useState(null); const [newChannelId, setNewChannelId] = useState(null); const userChannels = config.channels.filter((channel) => channel.scope !== "system"); const runningHubReady = Boolean(config.runningHub.enabled && config.runningHub.baseUrl.trim() && config.runningHub.apiKey.trim() && config.runningHub.workflowId.trim()); const updateChannels = (channels: ModelChannel[], baseConfig = config) => { replaceConfig(withChannels(baseConfig, channels)); }; const updateChannel = (id: string, patch: Partial) => { updateChannels(config.channels.map((channel) => { if (channel.id !== id) return channel; const models = patch.models ? uniqueModels(patch.models) : channel.models; return { ...channel, ...patch, models, modelCosts: patch.modelCosts !== undefined ? patch.modelCosts : (patch.models ? channel.modelCosts?.filter((item) => models.includes(item.model)) : channel.modelCosts), }; })); }; const updateChannelConnection = (channel: ModelChannel, connection: UserChannelConnection) => { const apiFormat = connection; const defaultBaseUrl = defaultBaseUrlForApiFormat(apiFormat); const baseUrl = isKnownDefaultBaseUrl(channel.baseUrl) ? defaultBaseUrl : channel.baseUrl; // 渠道只负责连接类型;具体模型能力和请求协议由下方共享能力卡片维护。 updateChannel(channel.id, { apiFormat, interfaceType: undefined, baseUrl }); }; const addChannel = () => { const channel = createModelChannel({ name: `渠道 ${userChannels.length + 1}` }); updateChannels([...config.channels, channel]); setNewChannelId(channel.id); setEditingChannelId(channel.id); }; const closeChannelEditor = () => { setEditingChannelId(null); setNewChannelId(null); }; const deleteChannel = (id: string) => { const channel = config.channels.find((item) => item.id === id); if (channel?.scope === "system") { message.warning("系统渠道由管理员维护"); return; } updateChannels(config.channels.filter((item) => item.id !== id)); }; const setChannelLoading = (id: string, loading: boolean) => { setLoadingChannelIds((items) => (loading ? Array.from(new Set([...items, id])) : items.filter((item) => item !== id))); }; const refreshChannelModels = async (channel: ModelChannel) => { const connectionError = channelConnectionError(channel); if (connectionError) { message.error(`${channel.name || "当前渠道"}:${connectionError}`); return; } setChannelLoading(channel.id, true); try { const result = await fetchChannelModels(channel, true); if (!result.models.length) { message.warning(`${channel.name || "当前渠道"}未返回模型,已保留现有手工模型`); return; } const latestConfig = useConfigStore.getState().config; const latestChannel = latestConfig.channels.find((item) => item.id === channel.id); if (!latestChannel) return; if (channelConnectionSignature(latestChannel) !== channelConnectionSignature(channel)) { message.warning(`${latestChannel.name || "当前渠道"}的连接配置已改变,已忽略旧的拉取结果`); return; } updateChannels( latestConfig.channels.map((item) => (item.id === channel.id ? { ...item, models: result.models, modelCosts: mergeFetchedChannelModelCosts(item, result.catalog) } : item)), latestConfig, ); message.success(`${latestChannel.name || "当前渠道"}模型列表已更新`); } catch (error) { message.error(channelModelFetchErrorMessage(error)); } finally { setChannelLoading(channel.id, false); } }; const refreshAllModels = async () => { const runnable = userChannels.filter((channel) => !channelConnectionError(channel)); const skipped = userChannels.filter((channel) => channelConnectionError(channel)); if (!runnable.length) { const detail = skipped.map((channel) => `${channel.name || "未命名渠道"}:${channelConnectionError(channel)}`).join(";"); message.error(detail || "没有可拉取的个人模型渠道,请先填写有效 Base URL 和 API Key"); return; } setChannelLoading("all", true); try { const results = await Promise.all( runnable.map(async (channel) => { try { const result = await fetchChannelModels(channel, true); return { channel, result, error: "" }; } catch (error) { return { channel, result: { models: [], catalog: [] }, error: error instanceof Error ? error.message : "读取失败" }; } }), ); const latestConfig = useConfigStore.getState().config; const successful = results.filter((item) => { const latestChannel = latestConfig.channels.find((channel) => channel.id === item.channel.id); return Boolean(item.result.models.length && latestChannel && channelConnectionSignature(latestChannel) === channelConnectionSignature(item.channel)); }); const stale = results.filter((item) => { const latestChannel = latestConfig.channels.find((channel) => channel.id === item.channel.id); return Boolean(item.result.models.length && (!latestChannel || channelConnectionSignature(latestChannel) !== channelConnectionSignature(item.channel))); }); const failed = results.filter((item) => !item.result.models.length); if (successful.length) { const resultMap = new Map(successful.map((item) => [item.channel.id, item.result] as const)); updateChannels( latestConfig.channels.map((channel) => { const fetched = resultMap.get(channel.id); return fetched ? { ...channel, models: fetched.models, modelCosts: mergeFetchedChannelModelCosts(channel, fetched.catalog) } : channel; }), latestConfig, ); message.success(`已更新 ${successful.length} 个渠道的模型`); } const warnings = [ ...failed.map((item) => `${item.channel.name || "未命名渠道"}:${item.error || "未返回模型"}`), ...stale.map((item) => `${item.channel.name || "未命名渠道"}:连接配置已改变,已忽略旧结果`), ...skipped.map((channel) => `${channel.name || "未命名渠道"}:${channelConnectionError(channel)}`), ]; if (warnings.length) message.warning(`${warnings.join(";")}。未更新的渠道已保留原有模型列表`); } catch (error) { message.error(error instanceof Error ? error.message : "批量读取模型失败,原有模型列表未改动"); } finally { setChannelLoading("all", false); } }; return (

个人渠道

管理个人模型服务和工作流渠道。普通渠道只保存连接类型;模型能力在“模型与能力”中配置。

{onOpenRunningHub ?

个人工作流渠道

RunningHub 使用独立的云端工作流参数与执行通道。

{onOpenRunningHub ? ( } title="RunningHub" description="云端工作流和 RunningHub App" status={runningHubReady ? `${config.runningHub.workflows.length} 个工作流已配置` : config.runningHub.enabled ? "待完成连接和工作流配置" : "未启用"} ready={runningHubReady} onOpen={onOpenRunningHub} /> ) : null}
: null} {userChannels.length ? (
{userChannels.map((channel) => { const editing = editingChannelId === channel.id; return (

{channel.name || "未命名渠道"}

{channelProtocolLabel(channel)} · 已保存 {channel.models.length} 个模型
deleteChannel(channel.id)}>
{editing && ( 更改实时保存到云端渠道配置
} >

连接信息

用于拉取模型目录并向当前渠道发起请求。

updateChannel(channel.id, { name: event.target.value })} onBlur={(event) => updateChannel(channel.id, { name: event.target.value.trim() || "未命名渠道" })} /> block value={channelConnectionMode(channel)} options={[{ label: "OpenAI", value: "openai" }, { label: "Gemini", value: "gemini" }]} onChange={(value) => updateChannelConnection(channel, value)} /> updateChannel(channel.id, { baseUrl: event.target.value })} onBlur={(event) => updateChannel(channel.id, { baseUrl: event.target.value.trim().replace(/\/+$/u, "") })} /> updateChannel(channel.id, { apiKey: event.target.value })} onBlur={(event) => updateChannel(channel.id, { apiKey: event.target.value.trim() })} /> updateChannel(channel.id, { secretKey: event.target.value })} onBlur={(event) => updateChannel(channel.id, { secretKey: event.target.value.trim() })} />
updateChannel(channel.id, { headers })} />

模型与能力

维护渠道模型,并在单个模型中配置调用协议、能力和定价。