From 074ea0bb1a01c29ac1fa76bc4e85522e1c498bf9 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sun, 7 Jun 2026 09:51:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E7=BD=AE=E9=A1=B5=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=A8=A1=E5=9E=8B=E5=8D=A1=E7=89=87=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=BA7=E7=A7=8D=E8=83=BD=E5=8A=9B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/db/settings.ts | 1 + src/shared/api.ts | 13 ++ .../settings/components/ModelSettingsCard.tsx | 119 ++++++++++++++++++ src/web/features/settings/index.tsx | 54 ++++---- tests/server/db/settings.test.ts | 41 ++++++ tests/server/routes/settings.test.ts | 46 ++++++- .../features/settings/SettingsPage.test.tsx | 22 ++++ .../components/ModelSettingsCard.test.tsx | 67 ++++++++++ 8 files changed, 338 insertions(+), 25 deletions(-) create mode 100644 src/web/features/settings/components/ModelSettingsCard.tsx create mode 100644 tests/web/features/settings/components/ModelSettingsCard.test.tsx diff --git a/src/server/db/settings.ts b/src/server/db/settings.ts index 79c7ae5..7987e9d 100644 --- a/src/server/db/settings.ts +++ b/src/server/db/settings.ts @@ -26,6 +26,7 @@ export function getSettings(raw: Database): SettingsData { const parsed = JSON.parse(row.data) as Partial; return { compact: typeof parsed.compact === "boolean" ? parsed.compact : false, + defaultModels: parsed.defaultModels, theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system", }; } catch { diff --git a/src/shared/api.ts b/src/shared/api.ts index 8a56ccd..5f2500b 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -237,8 +237,21 @@ export type ProviderType = "anthropic" | "openai" | "openai-compatible"; export type RuntimeMode = "development" | "production" | "test"; +/** 模型能力到默认模型 ID 的映射,用于后台自动流程 */ +export interface DefaultModelSettings { + /** 文本能力,覆盖 text + reasoning */ + text?: string | null; + imageRecognition?: string | null; + audioRecognition?: string | null; + videoRecognition?: string | null; + imageGeneration?: string | null; + audioGeneration?: string | null; + videoGeneration?: string | null; +} + export interface SettingsData { compact?: boolean; + defaultModels?: DefaultModelSettings; theme: ThemePreference; } diff --git a/src/web/features/settings/components/ModelSettingsCard.tsx b/src/web/features/settings/components/ModelSettingsCard.tsx new file mode 100644 index 0000000..e298ea4 --- /dev/null +++ b/src/web/features/settings/components/ModelSettingsCard.tsx @@ -0,0 +1,119 @@ +import { useQueries } from "@tanstack/react-query"; +import { App as AntApp, Card, Form, Select } from "antd"; +import { useCallback, useMemo } from "react"; + +import type { DefaultModelSettings, ModelCapability } from "../../../../shared/api"; +import { fetchModelList } from "../../../shared/hooks/use-models"; +import { useSettings } from "../../../shared/hooks/use-settings"; + +interface CapabilityItem { + field: keyof DefaultModelSettings; + filterCapabilities: ModelCapability[]; + help?: string; + label: string; +} + +const CAPABILITY_ITEMS: CapabilityItem[] = [ + { field: "text", filterCapabilities: ["text", "reasoning"], help: "同时用于推理场景", label: "文本" }, + { field: "imageRecognition", filterCapabilities: ["image-recognition"], label: "图片识别" }, + { field: "audioRecognition", filterCapabilities: ["audio-recognition"], label: "音频识别" }, + { field: "videoRecognition", filterCapabilities: ["video-recognition"], label: "视频识别" }, + { field: "imageGeneration", filterCapabilities: ["image-generation"], label: "图片生成" }, + { field: "audioGeneration", filterCapabilities: ["audio-generation"], label: "音频生成" }, + { field: "videoGeneration", filterCapabilities: ["video-generation"], label: "视频生成" }, +]; + +const SAVE_MESSAGE_KEY = "model-save"; + +interface CapabilityModelSelectProps { + disabled: boolean; + item: CapabilityItem; + onChange: (field: keyof DefaultModelSettings, modelId: string | undefined) => void; + value: string | undefined; +} + +function CapabilityModelSelect({ disabled, item, onChange, value }: CapabilityModelSelectProps) { + const queries = useQueries({ + queries: item.filterCapabilities.map((cap) => ({ + queryFn: () => fetchModelList({ capabilities: cap, pageSize: 200 }), + queryKey: ["models", "list-cap", cap], + })), + }); + + const isLoading = queries.some((q) => q.isLoading); + + const options = useMemo(() => { + const seen = new Set(); + const result: Array<{ label: string; value: string }> = []; + for (const q of queries) { + for (const model of q.data?.items ?? []) { + if (!seen.has(model.id)) { + seen.add(model.id); + result.push({ label: model.name, value: model.id }); + } + } + } + return result; + }, [queries]); + + return ( + +