"use client"; import { CheckCircleOutlined, DeleteOutlined, FormatPainterOutlined, LoadingOutlined, PlusOutlined, ReloadOutlined, SaveOutlined } from "@ant-design/icons"; import { json } from "@codemirror/lang-json"; import { App, Button, Card, Checkbox, Col, Drawer, Flex, Form, Input, InputNumber, Modal, Row, Segmented, Select, Space, Switch, Table, Tabs, Tag, Typography } from "antd"; import dynamic from "next/dynamic"; import { useEffect, useMemo, useState } from "react"; import { EditorView } from "@uiw/react-codemirror"; import { fetchAdminSettings, fetchChannelModels, saveAdminSettings, testChannelModel, type AdminModelChannel, type AdminModelCost, type AdminSettings } from "@/services/api/admin"; import { useUserStore } from "@/stores/use-user-store"; const CodeMirror = dynamic(() => import("@uiw/react-codemirror"), { ssr: false }); const jsonEditorTheme = EditorView.theme({ "&": { backgroundColor: "var(--ant-color-bg-container)", color: "var(--ant-color-text)" }, ".cm-content": { caretColor: "var(--ant-color-text)", padding: "12px 0" }, ".cm-line": { padding: "0 18px" }, ".cm-gutters": { backgroundColor: "var(--ant-color-fill-quaternary)", borderRight: "1px solid var(--ant-color-border)", color: "var(--ant-color-text-tertiary)" }, ".cm-activeLine": { backgroundColor: "var(--ant-color-fill-quaternary)" }, ".cm-activeLineGutter": { backgroundColor: "var(--ant-color-fill-quaternary)", color: "var(--ant-color-text)" }, ".cm-cursor": { borderLeftColor: "var(--ant-color-text)" }, ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: "var(--ant-control-item-bg-active)" }, ".cm-foldPlaceholder": { backgroundColor: "var(--ant-color-fill-quaternary)", border: "1px solid var(--ant-color-border)", color: "var(--ant-color-text-tertiary)" }, "&.cm-focused": { outline: "none" }, }); const emptySettings: AdminSettings = { public: { modelChannel: { availableModels: [], modelCosts: [], defaultModel: "", defaultImageModel: "", defaultVideoModel: "", defaultTextModel: "", systemPrompt: "", allowCustomChannel: true, }, auth: { allowRegister: true, linuxDo: { enabled: false } }, }, private: { channels: [], promptSync: { enabled: true, cron: "*/5 * * * *" }, auth: { linuxDo: { clientId: "", clientSecret: "" } } }, }; const emptyChannel: AdminModelChannel = { protocol: "openai", name: "", baseUrl: "", apiKey: "", models: [], weight: 1, enabled: true, remark: "" }; type SettingsTabKey = "public" | "private"; type EditorMode = "visual" | "json"; type ModelSelectTabKey = "new" | "current"; export default function AdminSettingsPage() { const token = useUserStore((state) => state.token); const { message } = App.useApp(); const [form] = Form.useForm(); const [activeTab, setActiveTab] = useState("public"); const [editorMode, setEditorMode] = useState>({ public: "visual", private: "visual" }); const [jsonText, setJsonText] = useState>({ public: "", private: "" }); const [channels, setChannels] = useState([]); const [channelForm] = Form.useForm(); const [editingChannelIndex, setEditingChannelIndex] = useState(null); const [isChannelDrawerOpen, setIsChannelDrawerOpen] = useState(false); const [testChannelIndex, setTestChannelIndex] = useState(null); const [testKeyword, setTestKeyword] = useState(""); const [selectedTestModels, setSelectedTestModels] = useState([]); const [testingModels, setTestingModels] = useState([]); const [testResults, setTestResults] = useState>({}); const [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false); const [modelSelectSource, setModelSelectSource] = useState([]); const [modelSelectExisting, setModelSelectExisting] = useState([]); const [modelSelectSelected, setModelSelectSelected] = useState([]); const [modelSelectKeyword, setModelSelectKeyword] = useState(""); const [modelSelectNewModel, setModelSelectNewModel] = useState(""); const [modelSelectTab, setModelSelectTab] = useState("new"); const [isFetchingChannelModels, setIsFetchingChannelModels] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [modelCosts, setModelCosts] = useState([]); const [knownModels, setKnownModels] = useState([]); const publicModels = Form.useWatch(["public", "modelChannel", "availableModels"], form) || []; const channelModels = useMemo(() => collectChannelModels(channels), [channels]); const channelTableData = useMemo(() => channels.map((channel, index) => ({ ...channel, _index: index, _rowKey: `${index}-${channel.name}-${channel.baseUrl}` })), [channels]); const activeMode = editorMode[activeTab]; const activeJsonText = jsonText[activeTab]; const jsonError = activeMode === "json" ? getJsonError(activeJsonText) : ""; const modelSelectGroups = useMemo(() => buildModelSelectGroups(modelSelectSource, modelSelectExisting), [modelSelectSource, modelSelectExisting]); const activeModelSelectModels = useMemo(() => { const keyword = modelSelectKeyword.trim().toLowerCase(); return modelSelectGroups[modelSelectTab].filter((model) => model.toLowerCase().includes(keyword)); }, [modelSelectGroups, modelSelectKeyword, modelSelectTab]); const activeSelectedCount = activeModelSelectModels.filter((model) => modelSelectSelected.includes(model)).length; const loadSettings = async () => { if (!token) return; setIsLoading(true); try { const data = normalizeSettings(await fetchAdminSettings(token)); form.setFieldsValue(data); setChannels(data.private.channels); setModelCosts(data.public.modelChannel.modelCosts); setKnownModels(collectKnownModels(data)); setJsonText({ public: JSON.stringify(data.public, null, 2), private: JSON.stringify(data.private, null, 2), }); } catch (error) { message.error(error instanceof Error ? error.message : "读取设置失败"); } finally { setIsLoading(false); } }; useEffect(() => { void loadSettings(); }, [token]); const changeTab = (nextTab: SettingsTabKey) => { setActiveTab(nextTab); }; const saveSettings = async () => { if (!token) return; const values = await collectSettings(form, editorMode, jsonText, message); if (!values) { return; } setIsSaving(true); try { const saved = normalizeSettings(await saveAdminSettings(token, values)); const merged = mergeChannelApiKeys(values.private.channels, saved); form.setFieldsValue(merged); setChannels(merged.private.channels); setModelCosts(merged.public.modelChannel.modelCosts); rememberKnownModels(merged); setJsonText({ public: JSON.stringify(merged.public, null, 2), private: JSON.stringify(merged.private, null, 2), }); message.success("已保存"); } catch (error) { message.error(error instanceof Error ? error.message : "保存失败"); } finally { setIsSaving(false); } }; const toggleMode = (tab: SettingsTabKey, nextMode: EditorMode) => { if (nextMode === "json") { setJsonText((current) => ({ ...current, [tab]: JSON.stringify(tab === "public" ? normalizePublicSetting(form.getFieldValue(["public"]) as Partial) : normalizePrivateSetting(form.getFieldValue(["private"]) as Partial), null, 2), })); setEditorMode((current) => ({ ...current, [tab]: nextMode })); return; } const parsed = parseTabJson(tab, jsonText[tab]); if (!parsed) { message.error("JSON 格式不正确"); return; } form.setFieldsValue({ [tab]: parsed } as Partial); if (tab === "private") setChannels((parsed as AdminSettings["private"]).channels); if (tab === "public") setModelCosts((parsed as AdminSettings["public"]).modelChannel.modelCosts); rememberKnownModels({ ...normalizeSettings(form.getFieldsValue(true) as AdminSettings), [tab]: parsed }); setEditorMode((current) => ({ ...current, [tab]: nextMode })); }; const formatJson = (tab: SettingsTabKey) => { const parsed = parseTabJson(tab, jsonText[tab]); if (!parsed) { message.error("JSON 格式不正确"); return; } if (tab === "public") setModelCosts((parsed as AdminSettings["public"]).modelChannel.modelCosts); setJsonText((current) => ({ ...current, [tab]: JSON.stringify(parsed, null, 2), })); }; const openChannelDrawer = (index: number | null) => { setEditingChannelIndex(index); setIsChannelDrawerOpen(true); const channel = index === null ? emptyChannel : normalizeChannel(channels[index]); channelForm.setFieldsValue(channel); rememberModels(channel.models); }; const closeChannelDrawer = () => { setIsChannelDrawerOpen(false); setEditingChannelIndex(null); channelForm.resetFields(); }; const saveChannel = async () => { const channel = normalizeChannel(await channelForm.validateFields()); rememberModels(channel.models); const nextChannels = [...channels]; if (editingChannelIndex === null) nextChannels.push(channel); else nextChannels[editingChannelIndex] = channel; await persistChannels(nextChannels); closeChannelDrawer(); }; const fetchChannelModelList = async () => { if (!token) return; const channel = channelForm.getFieldsValue(); if (!channel?.baseUrl) { message.warning("请先填写接口地址"); return; } if (editingChannelIndex === null && !channel?.apiKey) { message.warning("请先填写 API Key"); return; } setIsFetchingChannelModels(true); try { const channelModels = await fetchChannelModels(token, { index: editingChannelIndex ?? undefined, channel: normalizeChannel(channel) }); const current = isModelSelectorOpen ? uniqueModels(modelSelectSelected) : uniqueModels(channelForm.getFieldValue("models") || []); rememberModels(channelModels); if (!channelModels.length) { message.warning("上游未返回模型列表,请手动输入模型名称"); return; } setModelSelectExisting(current); setModelSelectSource(uniqueModels(channelModels)); setModelSelectSelected(uniqueModels([...current, ...channelModels])); setModelSelectKeyword(""); setModelSelectNewModel(""); setModelSelectTab("new"); setIsModelSelectorOpen(true); message.success(`已获取 ${channelModels.length} 个模型,请选择后确认`); } catch (error) { message.error(error instanceof Error ? error.message : "读取模型失败"); } finally { setIsFetchingChannelModels(false); } }; const openChannelModelSelector = (sourceModels?: string[]) => { const current = uniqueModels(channelForm.getFieldValue("models") || []); const source = uniqueModels(sourceModels !== undefined ? sourceModels : [...knownModels, ...current]); setModelSelectExisting(current); setModelSelectSource(source); setModelSelectSelected(sourceModels ? uniqueModels([...current, ...source]) : current); setModelSelectKeyword(""); setModelSelectNewModel(""); setModelSelectTab(sourceModels ? "new" : "current"); setIsModelSelectorOpen(true); }; const closeChannelModelSelector = () => { setIsModelSelectorOpen(false); setModelSelectKeyword(""); setModelSelectNewModel(""); }; const confirmChannelModelSelector = () => { const models = uniqueModels(modelSelectSelected); channelForm.setFieldValue("models", models); rememberModels(models); closeChannelModelSelector(); }; const toggleSelectedModel = (model: string, checked: boolean) => { setModelSelectSelected((current) => (checked ? uniqueModels([...current, model]) : current.filter((item) => item !== model))); }; const selectActiveModels = () => { setModelSelectSelected((current) => uniqueModels([...current, ...activeModelSelectModels])); }; const clearActiveModels = () => { const active = new Set(activeModelSelectModels); setModelSelectSelected((current) => current.filter((model) => !active.has(model))); }; const addModelInSelector = () => { const model = modelSelectNewModel.trim(); if (!model) return; setModelSelectExisting((current) => uniqueModels([...current, model])); setModelSelectSelected((current) => uniqueModels([...current, model])); setModelSelectNewModel(""); setModelSelectTab("current"); }; function rememberModels(models: string[]) { setKnownModels((current) => uniqueModels([...current, ...models])); } function rememberKnownModels(settings: AdminSettings) { rememberModels(collectKnownModels(settings)); } const openTestDialog = (index: number) => { const channel = normalizeChannel(channels[index]); if (!channel.baseUrl || channel.models.length === 0) { message.warning("请先填写接口地址和至少一个模型"); return; } setTestChannelIndex(index); setTestKeyword(""); setSelectedTestModels([]); setTestingModels([]); setTestResults({}); }; const closeTestDialog = () => { setTestChannelIndex(null); setTestKeyword(""); setSelectedTestModels([]); setTestingModels([]); setTestResults({}); }; const testModelOnline = async (model: string) => { if (testChannelIndex === null) return; if (!token) return; const channel = normalizeChannel(channels[testChannelIndex]); setTestingModels((current) => [...current, model]); try { const startedAt = performance.now(); const result = await testChannelModel(token, { index: testChannelIndex, channel, model }); setTestResults((current) => ({ ...current, [model]: { status: "success", duration: `${((performance.now() - startedAt) / 1000).toFixed(2)}s`, message: result } })); } catch (error) { setTestResults((current) => ({ ...current, [model]: { status: "error", message: error instanceof Error ? error.message : "测试失败" } })); } finally { setTestingModels((current) => current.filter((item) => item !== model)); } }; const batchTestModels = async () => { for (const model of selectedTestModels) { await testModelOnline(model); } }; const testChannel = testChannelIndex === null ? null : normalizeChannel(channels[testChannelIndex]); const testModels = (testChannel?.models || []).filter((model) => model.toLowerCase().includes(testKeyword.trim().toLowerCase())); async function persistChannels(nextChannels: AdminModelChannel[]) { if (!token) return; const values = normalizeSettings(form.getFieldsValue(true) as AdminSettings); const nextChannelModels = collectChannelModels(nextChannels); const nextSettings = normalizeSettings({ ...values, public: { ...values.public, modelChannel: { ...values.public.modelChannel, availableModels: nextChannelModels } }, private: { ...values.private, channels: nextChannels }, }); const saved = normalizeSettings(await saveAdminSettings(token, nextSettings)); const merged = mergeChannelApiKeys(nextChannels, saved); setChannels(merged.private.channels); setModelCosts(merged.public.modelChannel.modelCosts); rememberKnownModels(merged); form.setFieldsValue(merged); setJsonText({ public: JSON.stringify(merged.public, null, 2), private: JSON.stringify(merged.private, null, 2), }); message.success("已保存"); } return (
changeTab(key as SettingsTabKey)} items={[ { key: "public", label: "公开配置(对外暴露)" }, { key: "private", label: "私有配置(不会对外暴露)" }, ]} /> toggleMode(activeTab, value as EditorMode)} options={[ { label: "可视化编辑", value: "visual" }, { label: "手动编辑 JSON", value: "json" }, ]} /> {activeMode === "json" ? ( {jsonError ? ( {jsonError} ) : ( }> JSON 格式正确 )} ) : ( {activeTab === "public" ? "这些配置会暴露给前端读取" : "这些配置只会在后台保存"} )} {activeTab === "public" ? ( activeMode === "visual" ? (
({ label: item, value: item }))} /> ({ label: item, value: item }))} /> value || "未命名渠道" }, { title: "协议", dataIndex: "protocol", width: 96, render: (value) => {value || "openai"} }, { title: "状态", dataIndex: "enabled", width: 96, render: (value) => {value ? "已启用" : "已停用"} }, { title: "模型", dataIndex: "models", render: (value: string[]) => ( {modelSummary(value || [])} ), }, { title: "权重", dataIndex: "weight", width: 88 }, { title: "操作", key: "actions", width: 220, align: "right", render: (_, item) => ( } destroyOnHidden > setModelSelectNewModel(event.target.value)} onPressEnter={addModelInSelector} /> 如果上游不提供 OpenAI /models 模型列表接口,请在这里手动增加模型名称。 setModelSelectTab(key as ModelSelectTabKey)} items={[ { key: "new", label: `新获取的模型 (${modelSelectGroups.new.length})` }, { key: "current", label: `已有的模型 (${modelSelectGroups.current.length})` }, ]} /> 当前列表已选择 {activeSelectedCount} / {activeModelSelectModels.length}
{activeModelSelectModels.length ? (
{activeModelSelectModels.map((model) => ( toggleSelectedModel(model, event.target.checked)}> {model} ))}
) : (
没有匹配的模型
)}
{testChannel?.name || "渠道"} 渠道的模型测试共 {testChannel?.models.length || 0} 个模型 } open={testChannelIndex !== null} width={920} onCancel={closeTestDialog} footer={ } destroyOnHidden > 普通文本模型会发送一条 hi;Agent Plan / Seedance 视频模型只做配置格式检查,不会发起视频生成,也不代表模型权限已验证。 setTestKeyword(event.target.value)} />
({ model }))} rowSelection={{ selectedRowKeys: selectedTestModels, onChange: (keys) => setSelectedTestModels(keys.map(String)), }} columns={[ { title: "模型名称", dataIndex: "model", render: (value) => {value} }, { title: "状态", dataIndex: "model", width: 260, render: (value) => { if (testingModels.includes(value)) return }>测试中; const result = testResults[value]; if (!result) return 未开始; return result.status === "success" ? ( 成功 请求时长: {result.duration} ) : ( {result.message} ); }, }, { title: "操作", key: "actions", width: 120, align: "right", render: (_, item) => ( ), }, ]} /> ); } function normalizeSettings(settings: Partial = {}): AdminSettings { const privateSetting = normalizePrivateSetting(settings.private); return { public: { ...normalizePublicSetting(settings.public), }, private: privateSetting, }; } function normalizePublicSetting(setting: Partial = {}): AdminSettings["public"] { return { ...emptySettings.public, modelChannel: { ...emptySettings.public.modelChannel, ...(setting.modelChannel || {}), availableModels: setting.modelChannel?.availableModels || [], modelCosts: normalizeModelCosts(setting.modelChannel?.modelCosts || []), }, auth: { allowRegister: setting.auth?.allowRegister !== false, linuxDo: { enabled: setting.auth?.linuxDo?.enabled === true, }, }, }; } function normalizeModelCosts(items: Partial[]) { return items.filter((item) => item.model).map((item) => ({ model: item.model || "", credits: Math.max(0, Number(item.credits) || 0) })); } function normalizePrivateSetting(setting: Partial = {}): AdminSettings["private"] { return { channels: (setting.channels || []).map(normalizeChannel), promptSync: { enabled: setting.promptSync?.enabled !== false, cron: setting.promptSync?.cron || "*/5 * * * *", }, auth: { linuxDo: { clientId: setting.auth?.linuxDo?.clientId || "", clientSecret: setting.auth?.linuxDo?.clientSecret || "", }, }, }; } function normalizeChannel(item: Partial = {}): AdminModelChannel { return { protocol: "openai", name: item.name || "", baseUrl: item.baseUrl || "", apiKey: item.apiKey || "", models: item.models || [], weight: Math.max(1, Number(item.weight) || 1), enabled: item.enabled !== false, remark: item.remark || "", }; } function modelCostCredits(items: AdminSettings["public"]["modelChannel"]["modelCosts"], model: string) { return items.find((item) => item.model === model)?.credits || 0; } function setModelCost(form: any, setModelCosts: (items: AdminModelCost[]) => void, model: string, credits: number) { const current = (form.getFieldValue(["public", "modelChannel", "modelCosts"]) || []) as AdminSettings["public"]["modelChannel"]["modelCosts"]; const next = current.filter((item) => item.model !== model); next.push({ model, credits: Math.max(0, credits) }); form.setFieldValue(["public", "modelChannel", "modelCosts"], next); setModelCosts(next); } function mergeChannelApiKeys(currentChannels: AdminModelChannel[], saved: AdminSettings): AdminSettings { const channels = saved.private.channels.map((item, index) => ({ ...item, apiKey: currentChannels[index]?.apiKey || item.apiKey, })); return { public: saved.public, private: { ...saved.private, channels }, }; } function collectChannelModels(channels: AdminModelChannel[]) { return uniqueModels(channels.filter((channel) => channel.enabled).flatMap((channel) => channel.models || [])); } function collectKnownModels(settings: AdminSettings) { return uniqueModels([ ...(settings.public.modelChannel.availableModels || []), ...(settings.public.modelChannel.modelCosts || []).map((item) => item.model), ...settings.private.channels.flatMap((channel) => channel.models || []), ]); } function buildModelSelectGroups(sourceModels: string[], existingModels: string[]): Record { const source = uniqueModels(sourceModels); const existing = uniqueModels(existingModels); const existingSet = new Set(existing); return { new: source.filter((model) => !existingSet.has(model)), current: existing, }; } function uniqueModels(models: string[]) { return Array.from(new Set(models.filter(Boolean))); } function modelSummary(models: string[]) { if (!models.length) return "未配置模型"; const preview = models.slice(0, 3).join(", "); return models.length > 3 ? `${models.length} 个模型:${preview}...` : preview; } function parseTabJson(tab: "public", value: string): AdminSettings["public"] | null; function parseTabJson(tab: "private", value: string): AdminSettings["private"] | null; function parseTabJson(tab: SettingsTabKey, value: string): AdminSettings[SettingsTabKey] | null; function parseTabJson(tab: SettingsTabKey, value: string): AdminSettings[SettingsTabKey] | null { try { return tab === "public" ? normalizePublicSetting(JSON.parse(value) as Partial) : normalizePrivateSetting(JSON.parse(value) as Partial); } catch { return null; } } async function collectSettings(form: any, editorMode: Record, jsonText: Record, message: { error: (value: string) => void }) { const values = normalizeSettings(form.getFieldsValue(true) as AdminSettings); if (editorMode.public === "json") { const publicSetting = parseTabJson("public", jsonText.public); if (!publicSetting) { message.error("公开配置 JSON 格式不正确"); return null; } values.public = publicSetting; } if (editorMode.private === "json") { const privateSetting = parseTabJson("private", jsonText.private); if (!privateSetting) { message.error("私有配置 JSON 格式不正确"); return null; } values.private = privateSetting; } values.public.modelChannel.availableModels = collectChannelModels(values.private.channels); return normalizeSettings(values); } function getJsonError(value: string) { try { JSON.parse(value); return ""; } catch (error) { return error instanceof Error ? error.message : "JSON 格式不正确"; } }