import { App, Button, Form, Input, Popconfirm, Segmented, Select, Tooltip } from "antd"; import { ChevronDown, ChevronUp, Plus, RefreshCw, Trash2 } from "lucide-react"; import { useState } from "react"; import { ChannelHeadersEditor, validateChannelHeaders } from "@/components/channel-headers-editor"; import { WorkspaceState } from "@/components/layout/workspace-state"; import { defaultModelCapabilityConfig } from "@/lib/model-capabilities"; import { modelProtocolCapability, protocolForModelCatalog } from "@/lib/model-protocols"; import { fetchChannelModels, type ChannelModelCatalogItem } 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 ChannelModelCost = NonNullable[number]; export function ChannelSettingsPane({ onOpenModels }: { onOpenModels: () => void }) { const { message } = App.useApp(); const config = useConfigStore((state) => state.config); const replaceConfig = useConfigStore((state) => state.replaceConfig); const [loadingChannelIds, setLoadingChannelIds] = useState([]); const [collapsedChannelIds, setCollapsedChannelIds] = useState>(new Set()); const userChannels = config.channels.filter((channel) => channel.scope !== "system"); 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]); requestAnimationFrame(() => document.getElementById(`channel-${channel.id}-name`)?.focus()); }; 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 toggleChannelCollapsed = (id: string) => { setCollapsedChannelIds((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; 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: mergeFetchedModelCosts(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: mergeFetchedModelCosts(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 (

自定义渠道

渠道只保存连接类型;拉取模型后,请在“模型与能力”中配置协议。

{userChannels.length ? (
{userChannels.map((channel) => { const collapsed = collapsedChannelIds.has(channel.id); return (

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

{channelProtocolLabel(channel)} · 已保存 {channel.models.length} 个模型