refactor(config): 将AI配置逻辑迁移至统一的状态管理store

- 移除独立的ai-config.ts文件,将其功能整合到use-config-store
- 更新所有组件导入路径从 "@/lib/ai-config" 到 "@/stores/use-config-store"
- 实现云端渠道和本地直连两种配置模式的支持
- 添加模型渠道配置管理和API请求代理转发功能
- 统一配置验证逻辑和有效配置获取方法
- 更新组件中使用的配置状态钩子和API调用方式
This commit is contained in:
HouYunFei
2026-05-21 13:36:30 +08:00
parent afd9631735
commit 6f1e0d347b
23 changed files with 431 additions and 201 deletions
+4 -7
View File
@@ -7,9 +7,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { App, ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import { ClientRootInit } from "@/components/client-root-init";
import { getAntThemeConfig } from "@/lib/app-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useUserStore } from "@/stores/use-user-store";
const queryClient = new QueryClient({
defaultOptions: {
@@ -23,7 +23,6 @@ const queryClient = new QueryClient({
export function AppProviders({ children }: { children: ReactNode }) {
const theme = useThemeStore((state) => state.theme);
const hydrateUser = useUserStore((state) => state.hydrateUser);
const dark = theme === "dark";
useEffect(() => {
@@ -31,15 +30,13 @@ export function AppProviders({ children }: { children: ReactNode }) {
document.documentElement.style.colorScheme = theme;
}, [dark, theme]);
useEffect(() => {
void hydrateUser();
}, [hydrateUser]);
return (
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark)}>
<ProConfigProvider dark={dark}>
<App>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<ClientRootInit>{children}</ClientRootInit>
</QueryClientProvider>
</App>
</ProConfigProvider>
</ConfigProvider>
+68 -30
View File
@@ -2,15 +2,14 @@
import { LogOut, Menu, Settings2, Shield } from "lucide-react";
import Link from "next/link";
import { App, Button, Drawer, Form, Input, Modal } from "antd";
import { App, Button, Drawer, Form, Input, Modal, Segmented } from "antd";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import { ModelPicker } from "@/components/model-picker";
import { GitHubLink } from "@/components/github-link";
import { UserStatusActions } from "@/components/user-status-actions";
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
import { VersionReleaseModal } from "@/components/version-release-modal";
import type { AiConfig } from "@/lib/ai-config";
import { useConfigStore, type AiConfig } from "@/stores/use-config-store";
import { navigationTools, type NavigationToolSlug } from "@/lib/navigation-tools";
import { fetchImageModels } from "@/services/api/image";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -30,20 +29,33 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
const [loadingModels, setLoadingModels] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
const isConfigOpen = useConfigDialogStore((state) => state.isOpen);
const shouldPromptContinue = useConfigDialogStore((state) => state.shouldPromptContinue);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const setConfigDialogOpen = useConfigDialogStore((state) => state.setConfigDialogOpen);
const clearPromptContinue = useConfigDialogStore((state) => state.clearPromptContinue);
const isConfigOpen = useConfigStore((state) => state.isConfigOpen);
const shouldPromptContinue = useConfigStore((state) => state.shouldPromptContinue);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const setConfigDialogOpen = useConfigStore((state) => state.setConfigDialogOpen);
const clearPromptContinue = useConfigStore((state) => state.clearPromptContinue);
const theme = useThemeStore((state) => state.theme);
const setTheme = useThemeStore((state) => state.setTheme);
const user = useUserStore((state) => state.user);
const isReady = useUserStore((state) => state.isReady);
const logout = useUserStore((state) => state.clearSession);
const publicSettings = useConfigStore((state) => state.publicSettings);
const modelChannel = publicSettings?.modelChannel;
const allowCustomChannel = modelChannel?.allowCustomChannel === true;
const effectiveMode = allowCustomChannel ? config.channelMode : "remote";
const modelConfig = effectiveMode === "remote" && modelChannel ? {
...config,
models: modelChannel.availableModels,
imageModel: modelChannel.availableModels.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
textModel: modelChannel.availableModels.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
systemPrompt: modelChannel.systemPrompt,
} : config;
const finishConfig = () => {
setConfigDialogOpen(false);
if (!config.baseUrl.trim() || !config.imageModel.trim() || !config.textModel.trim() || !config.apiKey.trim()) return;
if (effectiveMode === "local" && (!config.baseUrl.trim() || !config.apiKey.trim())) return;
if (!modelConfig.imageModel.trim() || !modelConfig.textModel.trim()) return;
if (!allowCustomChannel && config.channelMode !== "remote") onConfigChange("channelMode", "remote");
if (shouldPromptContinue) {
message.success("配置已保存,请继续刚才的请求");
} else {
@@ -52,6 +64,7 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
clearPromptContinue();
};
const refreshModels = async () => {
if (effectiveMode === "remote") return;
if (!config.baseUrl.trim() || !config.apiKey.trim()) {
message.error("请先填写 Base URL 和 API Key");
return;
@@ -197,35 +210,60 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
<Modal
title={<div><div className="text-lg font-semibold"></div><div className="mt-1 text-xs font-normal text-stone-500"></div></div>}
open={isConfigOpen}
width={560}
width={760}
centered
onCancel={() => setConfigDialogOpen(false)}
footer={<Button type="primary" size="large" onClick={finishConfig}></Button>}
>
<div className="pt-1">
<Form layout="vertical" requiredMark={false} size="large">
<Form.Item label="Base URL" className="mb-4">
<Input value={config.baseUrl} onChange={(event) => onConfigChange("baseUrl", event.target.value)} />
</Form.Item>
<Form.Item label="API Key" className="mb-4">
<Input.Password value={config.apiKey} onChange={(event) => onConfigChange("apiKey", event.target.value)} />
</Form.Item>
<div className="mb-4 flex items-center justify-between gap-3 rounded-lg border border-stone-200 p-3 dark:border-stone-800">
<div className="min-w-0">
<div className="text-sm font-medium"></div>
<div className="mt-1 text-xs text-stone-500"> {config.models.length} </div>
{allowCustomChannel ? (
<Form.Item label="渠道模式" className="mb-4">
<Segmented
block
value={effectiveMode}
onChange={(value) => onConfigChange("channelMode", value as AiConfig["channelMode"])}
options={[{ label: "云端渠道", value: "remote" }, { label: "本地直连", value: "local" }]}
/>
</Form.Item>
) : null}
{effectiveMode === "local" ? (
<>
<div className="grid gap-4 md:grid-cols-2">
<Form.Item label="Base URL" className="mb-4">
<Input value={config.baseUrl} onChange={(event) => onConfigChange("baseUrl", event.target.value)} />
</Form.Item>
<Form.Item label="API Key" className="mb-4">
<Input.Password value={config.apiKey} onChange={(event) => onConfigChange("apiKey", event.target.value)} />
</Form.Item>
</div>
<div className="mb-4 flex items-center justify-between gap-3 rounded-lg border border-stone-200 px-3 py-2 dark:border-stone-800">
<div className="min-w-0">
<div className="text-sm font-medium"></div>
<div className="mt-1 text-xs text-stone-500"> {config.models.length} </div>
</div>
<Button loading={loadingModels} onClick={() => void refreshModels()}></Button>
</div>
</>
) : (
<div className="mb-4 rounded-lg border border-stone-200 p-3 text-sm text-stone-500 dark:border-stone-800">
<div className="font-medium text-stone-900 dark:text-stone-100"></div>
<div className="mt-1"> {modelChannel?.availableModels.length || 0} </div>
</div>
<Button loading={loadingModels} onClick={() => void refreshModels()}></Button>
)}
<div className="grid gap-4 md:grid-cols-2">
<Form.Item label="默认生图模型" className="mb-4">
<ModelPicker config={modelConfig} value={modelConfig.imageModel} onChange={(model) => onConfigChange("imageModel", model)} fullWidth />
</Form.Item>
<Form.Item label="默认文本模型" className="mb-4">
<ModelPicker config={modelConfig} value={modelConfig.textModel} onChange={(model) => onConfigChange("textModel", model)} fullWidth />
</Form.Item>
</div>
<Form.Item label="默认生图模型" className="mb-4">
<ModelPicker config={config} value={config.imageModel} onChange={(model) => onConfigChange("imageModel", model)} fullWidth />
</Form.Item>
<Form.Item label="默认文本模型" className="mb-0">
<ModelPicker config={config} value={config.textModel} onChange={(model) => onConfigChange("textModel", model)} fullWidth />
</Form.Item>
<Form.Item label="系统提示词" className="mb-0">
<Input.TextArea rows={4} value={config.systemPrompt} placeholder="例如:你是一位擅长电影感写实摄影的视觉导演。" onChange={(event) => onConfigChange("systemPrompt", event.target.value)} />
</Form.Item>
{effectiveMode === "local" ? (
<Form.Item label="系统提示词" className="mb-0">
<Input.TextArea rows={3} value={config.systemPrompt} placeholder="例如:你是一位擅长电影感写实摄影的视觉导演。" onChange={(event) => onConfigChange("systemPrompt", event.target.value)} />
</Form.Item>
) : null}
</Form>
</div>
</Modal>
+25
View File
@@ -0,0 +1,25 @@
"use client";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { useConfigStore } from "@/stores/use-config-store";
import { useUserStore } from "@/stores/use-user-store";
export function ClientRootInit({ children }: { children: ReactNode }) {
const pathname = usePathname();
const hydrateUser = useUserStore((state) => state.hydrateUser);
const loadPublicSettings = useConfigStore((state) => state.loadPublicSettings);
const isLoginPage = pathname === "/login" || pathname === "/admin/login";
useEffect(() => {
void loadPublicSettings();
}, [loadPublicSettings]);
useEffect(() => {
if (!isLoginPage) void hydrateUser();
}, [hydrateUser, isLoginPage]);
return <>{children}</>;
}
+1 -1
View File
@@ -2,7 +2,7 @@
import { Select } from "antd";
import type { AiConfig } from "@/lib/ai-config";
import type { AiConfig } from "@/stores/use-config-store";
type ModelPickerProps = {
config: AiConfig;