From afd96317350d99694e0b987d5589e177024bea77 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Thu, 21 May 2026 12:14:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20=E9=87=8D=E6=9E=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E8=AE=BE=E7=BD=AE=E6=A8=A1=E5=9E=8B=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E9=85=8D=E7=BD=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将公开设置中的模型相关字段统一收拢到 modelChannel 配置组 - 更新 AdminPublicSettings 类型定义为 AdminPublicModelChannelSettings - 在前端表单中调整模型配置字段路径到 public.modelChannel 下 - 更新设置规范化函数以适配新的嵌套配置结构 - 修改依赖注入和文档以反映配置结构变化 - 添加系统配置数据结构文档说明新的配置格式 --- AGENTS.md | 1 + CHANGELOG.md | 1 + README.md | 3 +- docs/backend-database.md | 6 ++ docs/pending-test.md | 2 + docs/system-settings.md | 76 +++++++++++++++++++++ model/setting.go | 34 ++------- service/settings.go | 4 +- web/bun.lock | 9 +-- web/package.json | 1 + web/src/app/(admin)/admin/settings/page.tsx | 54 +++++++++------ web/src/app/layout.tsx | 10 ++- web/src/lib/app-theme.ts | 2 +- web/src/services/api/admin.ts | 6 +- 14 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 docs/system-settings.md diff --git a/AGENTS.md b/AGENTS.md index 789ed44..a45645d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ ## 前端规范 - 前端使用 Next.js App Router、React、TypeScript、Ant Design、Tailwind、Zustand。 +- 编写 Ant Design 相关代码时,参考 https://ant.design/llms-full.txt 理解组件 API、示例和设计规范,并优先结合项目当前 antd 版本与既有写法。 - API 请求统一放在 `web/src/services/api/`。 - 全局或跨页面状态优先放在 `web/src/stores/`。 - 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。 diff --git a/CHANGELOG.md b/CHANGELOG.md index d562104..e724cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ + [新增] 管理后台新增系统设置页面,按公开/私有两个 Tab 区分配置,并支持可视化编辑和手动编辑 JSON。 + [调整] 模型渠道配置改为列表和抽屉编辑,新增协议、权重、远程获取模型列表、模型清单测试和批量测试能力。 ++ [调整] 公开系统配置中的模型相关字段统一收拢到 `modelChannel` 配置组。 + [新增] 后端新增 `settings` 表和系统设置接口,支持 public/private 两行 JSON 配置。 + [修复] Docker 构建和运行阶段补充拷贝 `CHANGELOG.md`,避免版本信息读取失败。 diff --git a/README.md b/README.md index 0b7076d..f7553b8 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ docker compose -f docker-compose.local.yml up -d --build - [画布快捷键](docs/canvas-shortcuts.md) - [待办事项](docs/todo.md) - [后端数据库说明](docs/backend-database.md) +- [系统配置数据结构](docs/system-settings.md) - [接口响应约定](docs/api-response.md) ## 社区支持 @@ -97,4 +98,4 @@ docker compose -f docker-compose.local.yml up -d --build Star History Chart - \ No newline at end of file + diff --git a/docs/backend-database.md b/docs/backend-database.md index e8125f1..597777b 100644 --- a/docs/backend-database.md +++ b/docs/backend-database.md @@ -109,6 +109,12 @@ `public.value` 当前字段: +| 字段 | 类型 | 说明 | +|-------------------|----------|----------------| +| `modelChannel` | object | 模型渠道公开配置组 | + +`modelChannel` 当前字段: + | 字段 | 类型 | 说明 | |-------------------|----------|----------------| | `availableModels` | string[] | 系统可用模型列表 | diff --git a/docs/pending-test.md b/docs/pending-test.md index 7e0c441..5f0d81a 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -4,7 +4,9 @@ - 设置弹窗中填写系统提示词后,生图、编辑图和画布助手文本请求应携带该提示词。 - Docker 镜像构建和运行时,前端应能正常读取 `CHANGELOG.md` 并完成版本信息注入。 - 后端应自动维护 `settings` 表,并支持公开读取 public 配置、管理员读取和保存 public/private 两行 JSON 配置。 +- 公开系统配置中的模型相关字段应保存到 `modelChannel` 配置组下。 - 管理后台系统设置页面应按公开/私有两个 Tab 区分配置,并支持可视化编辑和手动编辑 JSON,且可保存 public/private 配置。 +- 管理后台系统设置页面的公开配置应把私有配置中已启用渠道的模型列表作为系统可用模型的下拉选项,但系统可用模型仍由管理员手动选择。 - 管理后台私有配置中的模型渠道应以列表展示、通过抽屉新增编辑,并支持填写权重、远程获取模型列表、模型清单测试和批量测试。 - 管理后台亮色和暗色主题应使用接近 shadcn 的黑白中性色,侧栏、顶部栏、卡片、表格、弹窗和 JSON 编辑器不应再出现偏棕色或默认蓝色主色。 - 全局 Provider 和用户布局精简后,首页、工具页、画布详情页、404 页和管理后台应保持原有导航、主题切换、登录态初始化和数据请求能力。 diff --git a/docs/system-settings.md b/docs/system-settings.md new file mode 100644 index 0000000..8e03cd3 --- /dev/null +++ b/docs/system-settings.md @@ -0,0 +1,76 @@ +# 系统配置数据结构 + +系统配置保存在 `settings` 表中,目前只使用两行: + +| key | 说明 | +| --- | --- | +| `public` | 公开配置,前端可以读取 | +| `private` | 私有配置,只给后端和管理员使用 | + +## public.value + +```json +{ + "modelChannel": { + "availableModels": ["gpt-5.5", "gpt-image-2"], + "defaultModel": "gpt-image-2", + "defaultImageModel": "gpt-image-2", + "defaultTextModel": "gpt-5.5", + "systemPrompt": "", + "allowCustomChannel": false + } +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `modelChannel` | object | 模型渠道公开配置组 | + +`modelChannel` 字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `availableModels` | string[] | 系统可用模型,由管理员手动选择;页面下拉选项可来自私有渠道模型 | +| `defaultModel` | string | 默认模型,从 `availableModels` 中选择 | +| `defaultImageModel` | string | 默认图片模型,从 `availableModels` 中选择 | +| `defaultTextModel` | string | 默认文本模型,从 `availableModels` 中选择 | +| `systemPrompt` | string | 系统提示词 | +| `allowCustomChannel` | boolean | 是否允许用户自定义渠道 | + +## private.value + +```json +{ + "channels": [ + { + "protocol": "openai", + "name": "默认渠道", + "baseUrl": "https://api.example.com", + "apiKey": "sk-xxx", + "models": ["gpt-5.5", "gpt-image-2"], + "weight": 1, + "enabled": true, + "remark": "" + } + ] +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `channels` | object[] | 模型渠道列表 | + +`channels` 每项字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `protocol` | string | 协议,当前为 `openai` | +| `name` | string | 渠道名称 | +| `baseUrl` | string | OpenAI 兼容接口地址 | +| `apiKey` | string | 渠道密钥 | +| `models` | string[] | 该渠道可用模型 | +| `weight` | number | 渠道权重;同一模型有多个可用渠道时按权重随机 | +| `enabled` | boolean | 是否启用 | +| `remark` | string | 备注 | + +后端调用模型时,会从已启用、已配置 `baseUrl` 和 `apiKey`、且 `models` 包含目标模型的渠道中选择一个。 diff --git a/model/setting.go b/model/setting.go index 0466477..7faf4dc 100644 --- a/model/setting.go +++ b/model/setting.go @@ -1,7 +1,5 @@ package model -import "encoding/json" - type SettingKey string const ( @@ -21,8 +19,8 @@ type ModelChannel struct { Remark string `json:"remark"` } -// PublicSetting 公开配置。 -type PublicSetting struct { +// PublicModelChannelSetting 公开模型渠道配置。 +type PublicModelChannelSetting struct { AvailableModels []string `json:"availableModels"` DefaultModel string `json:"defaultModel"` DefaultImageModel string `json:"defaultImageModel"` @@ -31,6 +29,11 @@ type PublicSetting struct { AllowCustomChannel bool `json:"allowCustomChannel"` } +// PublicSetting 公开配置。 +type PublicSetting struct { + ModelChannel PublicModelChannelSetting `json:"modelChannel"` +} + // PrivateSetting 私有配置。 type PrivateSetting struct { Channels []ModelChannel `json:"channels"` @@ -49,26 +52,3 @@ type Settings struct { Public PublicSetting `json:"public"` Private PrivateSetting `json:"private"` } - -func (setting *PublicSetting) UnmarshalJSON(data []byte) error { - type alias struct { - AvailableModels []string `json:"availableModels"` - DefaultModel string `json:"defaultModel"` - DefaultImageModel string `json:"defaultImageModel"` - DefaultTextModel string `json:"defaultTextModel"` - SystemPrompt string `json:"systemPrompt"` - AllowCustomChannel bool `json:"allowCustomChannel"` - AllowCustomModel bool `json:"allowCustomModel"` - } - var value alias - if err := json.Unmarshal(data, &value); err != nil { - return err - } - setting.AvailableModels = value.AvailableModels - setting.DefaultModel = value.DefaultModel - setting.DefaultImageModel = value.DefaultImageModel - setting.DefaultTextModel = value.DefaultTextModel - setting.SystemPrompt = value.SystemPrompt - setting.AllowCustomChannel = value.AllowCustomChannel || value.AllowCustomModel - return nil -} diff --git a/service/settings.go b/service/settings.go index 10136aa..c3dc503 100644 --- a/service/settings.go +++ b/service/settings.go @@ -30,8 +30,8 @@ func normalizeSettings(settings model.Settings) model.Settings { } func normalizePublicSetting(setting model.PublicSetting) model.PublicSetting { - if setting.AvailableModels == nil { - setting.AvailableModels = []string{} + if setting.ModelChannel.AvailableModels == nil { + setting.ModelChannel.AvailableModels = []string{} } return setting } diff --git a/web/bun.lock b/web/bun.lock index 32220d7..fefccbf 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -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=="], diff --git a/web/package.json b/web/package.json index 70e5758..874cb4c 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app/(admin)/admin/settings/page.tsx b/web/src/app/(admin)/admin/settings/page.tsx index d494816..f1e0218 100644 --- a/web/src/app/(admin)/admin/settings/page.tsx +++ b/web/src/app/(admin)/admin/settings/page.tsx @@ -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>({}); 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" ? (
- ({ label: item, value: item }))} /> - ({ label: item, value: item }))} /> - - + ({ label: item, value: item }))} /> + ({ label: item, value: item }))} /> + +
) : ( @@ -337,7 +341,7 @@ export default function AdminSettingsPage() { )} - } destroyOnHidden> + } destroyOnHidden>
@@ -415,21 +419,23 @@ export default function AdminSettingsPage() { } function normalizeSettings(settings: Partial = {}): AdminSettings { + const privateSetting = normalizePrivateSetting(settings.private); return { public: { ...normalizePublicSetting(settings.public), }, - private: { - ...normalizePrivateSetting(settings.private), - }, + private: privateSetting, }; } function normalizePublicSetting(setting: Partial = {}): 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 = {}): 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(", "); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 45e4a9c..1314b9b 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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', }} > -