refactor(admin): 重构管理员设置模型渠道配置结构
- 将公开设置中的模型相关字段统一收拢到 modelChannel 配置组 - 更新 AdminPublicSettings 类型定义为 AdminPublicModelChannelSettings - 在前端表单中调整模型配置字段路径到 public.modelChannel 下 - 更新设置规范化函数以适配新的嵌套配置结构 - 修改依赖注入和文档以反映配置结构变化 - 添加系统配置数据结构文档说明新的配置格式
This commit is contained in:
+5
-4
@@ -6,6 +6,7 @@
|
||||
"name": "infinite-canvas",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/nextjs-registry": "^1.3.0",
|
||||
"@ant-design/pro-components": "3.0.0-beta.3",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
"@ant-design/colors": ["@ant-design/colors@8.0.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="],
|
||||
|
||||
"@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
|
||||
"@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
|
||||
"@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="],
|
||||
|
||||
@@ -53,6 +54,8 @@
|
||||
|
||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||
|
||||
"@ant-design/nextjs-registry": ["@ant-design/nextjs-registry@1.3.0", "", { "peerDependencies": { "@ant-design/cssinjs": ">=1.0.0", "antd": ">=5.0.0", "next": ">=14.0.0", "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-lHlg5bnj3LPYHUD+iZsO1k11NhIrx1mLWh2DXQ0m3Ly3OSaYfgeHX0WoVXxdVjEGjjYnPTe+OUY7YvU/p1aPZw=="],
|
||||
|
||||
"@ant-design/pro-components": ["@ant-design/pro-components@3.0.0-beta.3", "https://registry.npmmirror.com/@ant-design/pro-components/-/pro-components-3.0.0-beta.3.tgz", { "dependencies": { "@ant-design/cssinjs": "^1.24.0", "@ant-design/icons": "^5.6.1", "@babel/runtime": "^7.27.6", "@ctrl/tinycolor": "^4.1.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.2", "@emotion/cache": "^11.14.0", "@emotion/css": "^11.13.5", "@emotion/serialize": "^1.3.3", "@rc-component/util": "^1.2.2", "@umijs/route-utils": "^4.0.1", "@umijs/use-params": "^1.0.9", "classnames": "^2.5.1", "dayjs": "^1.11.13", "lodash-es": "^4.17.21", "path-to-regexp": "^6.3.0", "rc-field-form": "^2.7.0", "rc-footer": "^0.6.8", "rc-resize-observer": "^1.4.3", "rc-steps": "^6.0.1", "rc-table": "^7.51.1", "react-draggable": "^4.5.0", "react-layout-kit": "^1.9.2", "react-lazy-load": "^4.0.1", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.6.1", "safe-stable-stringify": "^2.5.0", "shiki-es": "^0.2.0", "swr": "^2.3.4" }, "peerDependencies": { "antd": "^5.11.2", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-ehdIGlDcy4J5kv6E3fZ8e4YLQY4+7S3ZsIZnZ//vtfd+s0Qkv13hQ65CepoMiC/x6gNBlDnHozKANNi7JOtJAw=="],
|
||||
|
||||
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
|
||||
@@ -1447,7 +1450,7 @@
|
||||
|
||||
"zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
|
||||
|
||||
"@ant-design/cssinjs-utils/@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
"@ant-design/pro-components/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
|
||||
|
||||
"@ant-design/pro-components/@ant-design/icons": ["@ant-design/icons@5.6.1", "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz", { "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg=="],
|
||||
|
||||
@@ -1501,8 +1504,6 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"antd/@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="],
|
||||
|
||||
"antd/@ant-design/icons": ["@ant-design/icons@6.2.3", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
|
||||
|
||||
"babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/nextjs-registry": "^1.3.0",
|
||||
"@ant-design/pro-components": "3.0.0-beta.3",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CheckCircleOutlined, DeleteOutlined, FormatPainterOutlined, PlusOutline
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { App, Button, Card, 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, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { EditorView } from "@uiw/react-codemirror";
|
||||
|
||||
import { fetchAdminSettings, fetchChannelModels, saveAdminSettings, testChannelModel, type AdminModelChannel, type AdminSettings } from "@/services/api/admin";
|
||||
@@ -26,12 +26,14 @@ const jsonEditorTheme = EditorView.theme({
|
||||
|
||||
const emptySettings: AdminSettings = {
|
||||
public: {
|
||||
availableModels: [],
|
||||
defaultModel: "",
|
||||
defaultImageModel: "",
|
||||
defaultTextModel: "",
|
||||
systemPrompt: "",
|
||||
allowCustomChannel: false,
|
||||
modelChannel: {
|
||||
availableModels: [],
|
||||
defaultModel: "",
|
||||
defaultImageModel: "",
|
||||
defaultTextModel: "",
|
||||
systemPrompt: "",
|
||||
allowCustomChannel: false,
|
||||
},
|
||||
},
|
||||
private: { channels: [] },
|
||||
};
|
||||
@@ -58,7 +60,9 @@ export default function AdminSettingsPage() {
|
||||
const [testResults, setTestResults] = useState<Record<string, { status: "success" | "error"; duration?: string; message: string }>>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const models = Form.useWatch(["public", "availableModels"], form) || [];
|
||||
const publicModels = Form.useWatch(["public", "modelChannel", "availableModels"], form) || [];
|
||||
const channelModels = useMemo(() => collectChannelModels(channels), [channels]);
|
||||
const modelOptions = useMemo(() => uniqueModels([...publicModels, ...channelModels]), [publicModels, channelModels]);
|
||||
const activeMode = editorMode[activeTab];
|
||||
const activeJsonText = jsonText[activeTab];
|
||||
const jsonError = activeMode === "json" ? getJsonError(activeJsonText) : "";
|
||||
@@ -266,12 +270,12 @@ export default function AdminSettingsPage() {
|
||||
activeMode === "visual" ? (
|
||||
<Form form={form} layout="vertical" initialValues={emptySettings} requiredMark={false}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}><Form.Item name={["public", "availableModels"]} label="系统可用模型(请先添加渠道)"><Select mode="tags" tokenSeparators={[",", "\n"]} options={models.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "defaultModel"]} label="默认模型"><Select showSearch allowClear options={models.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "defaultImageModel"]} label="默认图片模型"><Select showSearch allowClear options={models.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "defaultTextModel"]} label="默认文本模型"><Select showSearch allowClear options={models.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col span={24}><Form.Item name={["public", "systemPrompt"]} label="系统提示词"><Input.TextArea rows={4} /></Form.Item></Col>
|
||||
<Col span={24}><Form.Item name={["public", "allowCustomChannel"]} label="是否允许用户自定义渠道" extra="开启后,前端可提供走后端渠道和用户自定义 baseUrl 直连两种模式" valuePropName="checked"><Switch /></Form.Item></Col>
|
||||
<Col span={24}><Form.Item name={["public", "modelChannel", "availableModels"]} label="系统可用模型(请先配置渠道)"><Select mode="tags" tokenSeparators={[",", "\n"]} options={modelOptions.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "modelChannel", "defaultModel"]} label="默认模型"><Select showSearch allowClear options={publicModels.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "modelChannel", "defaultImageModel"]} label="默认图片模型"><Select showSearch allowClear options={publicModels.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name={["public", "modelChannel", "defaultTextModel"]} label="默认文本模型"><Select showSearch allowClear options={publicModels.map((item) => ({ label: item, value: item }))} /></Form.Item></Col>
|
||||
<Col span={24}><Form.Item name={["public", "modelChannel", "systemPrompt"]} label="系统提示词"><Input.TextArea rows={4} /></Form.Item></Col>
|
||||
<Col span={24}><Form.Item name={["public", "modelChannel", "allowCustomChannel"]} label="是否允许用户自定义渠道" extra="开启后,前端可提供走后端渠道和用户自定义 baseUrl 直连两种模式" valuePropName="checked"><Switch /></Form.Item></Col>
|
||||
</Row>
|
||||
</Form>
|
||||
) : (
|
||||
@@ -337,7 +341,7 @@ export default function AdminSettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<Drawer title={editingChannelIndex === null ? "新增渠道" : "编辑渠道"} open={isChannelDrawerOpen} width={560} onClose={closeChannelDrawer} extra={<Space><Button onClick={closeChannelDrawer}>取消</Button><Button type="primary" onClick={() => void saveChannel()}>保存</Button></Space>} destroyOnHidden>
|
||||
<Drawer title={editingChannelIndex === null ? "新增渠道" : "编辑渠道"} open={isChannelDrawerOpen} size={560} onClose={closeChannelDrawer} extra={<Space><Button onClick={closeChannelDrawer}>取消</Button><Button type="primary" onClick={() => void saveChannel()}>保存</Button></Space>} destroyOnHidden>
|
||||
<Form form={channelForm} layout="vertical" requiredMark={false} initialValues={emptyChannel}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}><Form.Item name="name" label="渠道名称" rules={[{ required: true, message: "请输入渠道名称" }]}><Input /></Form.Item></Col>
|
||||
@@ -415,21 +419,23 @@ export default function AdminSettingsPage() {
|
||||
}
|
||||
|
||||
function normalizeSettings(settings: Partial<AdminSettings> = {}): AdminSettings {
|
||||
const privateSetting = normalizePrivateSetting(settings.private);
|
||||
return {
|
||||
public: {
|
||||
...normalizePublicSetting(settings.public),
|
||||
},
|
||||
private: {
|
||||
...normalizePrivateSetting(settings.private),
|
||||
},
|
||||
private: privateSetting,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePublicSetting(setting: Partial<AdminSettings["public"]> = {}): AdminSettings["public"] {
|
||||
return {
|
||||
...emptySettings.public,
|
||||
...setting,
|
||||
availableModels: setting.availableModels || [],
|
||||
modelChannel: {
|
||||
...emptySettings.public.modelChannel,
|
||||
...(setting.modelChannel || {}),
|
||||
availableModels: setting.modelChannel?.availableModels || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -452,6 +458,14 @@ function normalizeChannel(item: Partial<AdminModelChannel> = {}): AdminModelChan
|
||||
};
|
||||
}
|
||||
|
||||
function collectChannelModels(channels: AdminModelChannel[]) {
|
||||
return uniqueModels(channels.filter((channel) => channel.enabled).flatMap((channel) => channel.models || []));
|
||||
}
|
||||
|
||||
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(", ");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { AntdRegistry } from "@ant-design/nextjs-registry";
|
||||
import { AppProviders } from "@/components/app-providers";
|
||||
import "antd/dist/reset.css";
|
||||
import "./globals.css";
|
||||
@@ -22,12 +24,16 @@ export default function RootLayout({
|
||||
'"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif',
|
||||
}}
|
||||
>
|
||||
<script
|
||||
<Script
|
||||
id="theme-script"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `try{var s=JSON.parse(localStorage.getItem("infinite-canvas:theme_store")||"{}");var t=s.state&&s.state.theme==="light"?"light":"dark";document.documentElement.classList.toggle("dark",t==="dark");document.documentElement.style.colorScheme=t}catch(e){}`,
|
||||
}}
|
||||
/>
|
||||
<AppProviders>{children}</AppProviders>
|
||||
<AntdRegistry>
|
||||
<AppProviders>{children}</AppProviders>
|
||||
</AntdRegistry>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function getAntThemeConfig(dark: boolean): ThemeConfig {
|
||||
|
||||
return {
|
||||
algorithm: dark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||
cssVar: true,
|
||||
cssVar: { key: dark ? "infinite-canvas-dark" : "infinite-canvas-light" },
|
||||
token: {
|
||||
colorPrimary: color.primary,
|
||||
colorInfo: color.primary,
|
||||
|
||||
@@ -95,7 +95,7 @@ export type AdminModelChannel = {
|
||||
remark: string;
|
||||
};
|
||||
|
||||
export type AdminPublicSettings = {
|
||||
export type AdminPublicModelChannelSettings = {
|
||||
availableModels: string[];
|
||||
defaultModel: string;
|
||||
defaultImageModel: string;
|
||||
@@ -104,6 +104,10 @@ export type AdminPublicSettings = {
|
||||
allowCustomChannel: boolean;
|
||||
};
|
||||
|
||||
export type AdminPublicSettings = {
|
||||
modelChannel: AdminPublicModelChannelSettings;
|
||||
};
|
||||
|
||||
export type AdminPrivateSettings = {
|
||||
channels: AdminModelChannel[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user