import { useEffect, useState } from "react"; import { App, Button, Checkbox, Input, Modal, Popconfirm, Space, Tooltip } from "antd"; import type { ColumnsType } from "antd/es/table"; import { Copy, Pencil, Plus, RefreshCw, Search, Trash2 } from "lucide-react"; import { PaginationBar } from "@/pages/admin/components/admin-ui"; import { ModelIcon } from "@/components/model-picker"; import { modelProtocolDefinition, modelProtocolLabel, type ModelProtocol } from "@/lib/model-protocols"; import { fetchPluginProviderCatalog } from "@/services/api/plugin-catalog"; import { deleteAdminChannelModel, deleteAdminChannelModels, fetchAdminChannelModels, importAdminChannelModels, listAdminChannelModels, type ChannelModel } from "@/services/api/wallet"; import type { ModelChannel } from "@/stores/use-config-store"; import { ChannelModelEditor } from "./channel-model-editor"; import { AdminBatchBar, AdminDataTable, AdminFilterChip, AdminStatusBadge, AdminTableEmpty } from "./admin-ui"; import { ChannelOrderDialog } from "./channel-order-dialog"; import { ChannelModelCostSummary } from "./channel-model-cost-summary"; import { ChannelModelRepriceDialog } from "./channel-model-reprice-dialog"; import { deselectVisibleModels, filterImportModels, selectVisibleModels } from "./channel-model-import-selection"; import { duplicateChannelModelValues, type ChannelModelFormValues } from "./channel-model-editor-form"; import { Select } from "@/components/ui/base/select"; export function ChannelModelManager({ channel, onChanged }: { channel: ModelChannel; onChanged: () => void | Promise }) { const { message, modal } = App.useApp(); const [items, setItems] = useState([]); const [selectedModelIds, setSelectedModelIds] = useState([]); const [deletingSelected, setDeletingSelected] = useState(false); const [repriceItems, setRepriceItems] = useState([]); const [editing, setEditing] = useState(null); const [copyDraft, setCopyDraft] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(""); const [fetching, setFetching] = useState(false); const [fetchPreviewOpen, setFetchPreviewOpen] = useState(false); const [fetchPreviewModels, setFetchPreviewModels] = useState([]); const [selectedFetchModels, setSelectedFetchModels] = useState([]); const [fetchKeyword, setFetchKeyword] = useState(""); const [importing, setImporting] = useState(false); const [editorOpen, setEditorOpen] = useState(false); const [keyword, setKeyword] = useState(""); const [capability, setCapability] = useState("all"); const [status, setStatus] = useState<"all" | "enabled" | "disabled">("all"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [availableProtocols, setAvailableProtocols] = useState([]); const [protocolLoading, setProtocolLoading] = useState(true); const [protocolError, setProtocolError] = useState(""); const loadProtocols = async () => { setProtocolLoading(true); setProtocolError(""); try { setAvailableProtocols(await fetchPluginProviderCatalog("admin.system-channel")); } catch (error) { setAvailableProtocols([]); setProtocolError(error instanceof Error ? error.message : "无法读取协议目录"); } finally { setProtocolLoading(false); } }; const reload = async () => { if (!channel) return; setLoading(true); setLoadError(""); try { const models = (await listAdminChannelModels(channel.id)).models; const availableIDs = new Set(models.map((item) => item.id)); setItems(models); setSelectedModelIds((current) => current.filter((id) => availableIDs.has(id))); } catch (error) { setLoadError(error instanceof Error ? error.message : "读取渠道模型失败"); } finally { setLoading(false); } }; useEffect(() => { void reload(); void loadProtocols(); setEditing(null); setCopyDraft(null); setEditorOpen(false); setSelectedModelIds([]); setRepriceItems([]); resetFetchPreview(); setKeyword(""); setCapability("all"); setStatus("all"); setPage(1); }, [channel.id]); const fetchModels = async () => { setFetching(true); try { const result = await fetchAdminChannelModels(channel.id); if (result.models.length === 0) { message.warning("上游没有返回可用模型"); return; } setFetchPreviewModels(result.models); setSelectedFetchModels(result.models); setFetchPreviewOpen(true); } catch (error) { message.error(error instanceof Error ? error.message : "拉取模型失败"); } finally { setFetching(false); } }; const closeFetchPreview = () => { if (importing) return; resetFetchPreview(); }; const resetFetchPreview = () => { setFetchPreviewOpen(false); setFetchPreviewModels([]); setSelectedFetchModels([]); setFetchKeyword(""); }; const importSelectedModels = async () => { if (!selectedFetchModels.length) return; if (!selectedNewFetchModels.length) { message.info("当前勾选的模型均已存在,没有需要新增的模型"); resetFetchPreview(); return; } setImporting(true); try { const result = await importAdminChannelModels(channel.id, selectedFetchModels); await reload(); await onChanged(); resetFetchPreview(); if (result.added > 0) message.success(`已导入 ${result.added} 个模型,新增模型仍需配置价格后启用`); else message.info("所选模型均已存在,没有新增模型"); } catch (error) { message.error(error instanceof Error ? error.message : "导入模型失败"); } finally { setImporting(false); } }; const startCreate = () => { setEditing(null); setCopyDraft(null); setEditorOpen(true); }; const startEdit = (item: ChannelModel) => { setEditing(item); setCopyDraft(null); setEditorOpen(true); }; const startCopy = (item: ChannelModel) => { setEditing(null); setCopyDraft(duplicateChannelModelValues(item, availableProtocols)); setEditorOpen(true); }; const remove = async (item: ChannelModel) => { try { await deleteAdminChannelModel(channel.id, item.id); await reload(); await onChanged(); message.success("模型已删除"); } catch (error) { message.error(error instanceof Error ? error.message : "删除模型失败"); } }; const confirmBatchRemove = () => { if (!selectedModelIds.length) return; const count = selectedModelIds.length; modal.confirm({ title: `删除已选的 ${count} 个模型?`, content: "删除后模型不再显示,且不能在页面恢复。只要其中任一模型仍被前台供应线路或进行中任务使用,本次就不会删除任何模型。", okText: "批量删除", cancelText: "取消", okButtonProps: { danger: true }, onOk: async () => { setDeletingSelected(true); try { const result = await deleteAdminChannelModels(channel.id, selectedModelIds); setSelectedModelIds([]); setPage(1); await reload(); await onChanged(); message.success(`已删除 ${result.deleted} 个模型`); } catch (error) { message.error(error instanceof Error ? error.message : "批量删除模型失败"); } finally { setDeletingSelected(false); } }, }); }; const columns: ColumnsType = [ { title: "模型", width: 240, render: (_, item) => (
{item.displayName || item.modelKey}
{item.modelKey}
{item.channelLabel ?
渠道展示名:{item.channelLabel}
: null} {item.providerModelKey && item.providerModelKey !== item.modelKey ?
上游:{item.providerModelKey}
: null}
), }, { title: "能力", dataIndex: "capability", width: 90, render: capabilityLabel }, { title: "请求协议", dataIndex: "protocol", width: 190, render: (value: ModelProtocol) => value ? (
{modelProtocolLabel(value, availableProtocols)}
{modelProtocolDefinition(value, availableProtocols)?.create}
) : ( ), }, { title: "规格成本价 / 销售价 / 利润率", width: 310, render: (_, item) => }, { title: "版本", dataIndex: "priceVersion", width: 75, render: (value) => `v${value}` }, { title: "状态", dataIndex: "enabled", width: 85, render: (enabled) => }, { title: "操作", width: 130, fixed: "right", render: (_, item) => ( {loadError ? (
{loadError}
) : null} } toolbar={ } value={keyword} placeholder="搜索模型标识或显示名称" onChange={(event) => { setKeyword(event.target.value); setPage(1); }} /> } toolbarActiveFilters={ <> {keyword ? ( { setKeyword(""); setPage(1); }} /> ) : null} {capability !== "all" ? ( { setCapability("all"); setPage(1); }} /> ) : null} {status !== "all" ? ( { setStatus("all"); setPage(1); }} /> ) : null} } toolbarActive={Boolean(keyword || capability !== "all" || status !== "all")} toolbarFilters={ <> { setStatus(value); setPage(1); }} options={[ { label: "全部状态", value: "all" }, { label: "已启用", value: "enabled" }, { label: "已停用", value: "disabled" }, ]} /> } onReset={() => { setKeyword(""); setCapability("all"); setStatus("all"); setPage(1); }} batchActions={ setSelectedModelIds([])}> } table={{ className: "app-data-table", rowKey: "id", size: "small", loading, rowSelection: { selectedRowKeys: selectedModelIds, preserveSelectedRowKeys: true, onChange: (keys) => { const next = keys.map(String); if (next.length > 100) message.warning("单次最多选择 100 个模型"); setSelectedModelIds(next.slice(0, 100)); }, }, columns, dataSource: pagedItems, pagination: false, scroll: { x: 1090 }, }} footer={ { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} /> } /> {repriceItems.length > 0 && setRepriceItems([])} onSaved={async () => { setSelectedModelIds([]); await reload(); await onChanged(); }} />} 取消 , , ]} >

上游共返回 {fetchPreviewModels.length} 个模型。默认已全选,可批量全选或取消全选;已存在的模型不会重复导入。

} value={fetchKeyword} placeholder="搜索模型标识,例如 minimax、sd2.5" aria-label="搜索上游模型" disabled={importing} onChange={(event) => setFetchKeyword(event.target.value)} />
已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型 {fetchKeywordQuery ? `,当前匹配 ${visibleFetchModels.length} 个(其中 ${selectedVisibleFetchCount} 个已勾选)` : ""}
{visibleFetchModels.length ? ( setSelectedFetchModels(values as string[])} /> ) : (

没有匹配「{fetchKeyword.trim()}」的模型

)}
{selectedNewFetchModels.length > 0 ? `将导入 ${selectedNewFetchModels.length} 个新模型` : "当前勾选的模型均已存在"} {selectedExistingFetchCount > 0 ? `,另有 ${selectedExistingFetchCount} 个已存在模型已勾选` : ""}
{editorOpen && ( void loadProtocols()} onClose={() => { setEditorOpen(false); setCopyDraft(null); setEditing(null); }} onSaved={async () => { await reload(); await onChanged(); }} /> )} ); } function capabilityLabel(value: ChannelModel["capability"]) { return { text: "文本", image: "图片", video: "视频", audio: "音频", "": "待配置" }[value]; } function normalizeFetchModelKey(value: string) { return value .trim() .replace(/^models\//, "") .toLowerCase(); }