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
@@ -6,11 +6,10 @@ import { useParams, useRouter } from "next/navigation";
import { Home, ImageIcon, Images, Keyboard, List, LogOut, Menu, MessageSquare, Plus, Redo2, Settings2, Trash2, Undo2, Upload } from "lucide-react";
import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image";
import { defaultConfig, isAiConfigReady, type AiConfig, useConfigStore, useEffectiveAiConfig } from "@/stores/use-config-store";
import { resolveImageUrl, uploadImage, type UploadedImage } from "@/services/image-storage";
import { createId } from "@/lib/id";
import { getDataUrlByteSize, readImageMeta } from "@/lib/image-utils";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
import { useAssetStore } from "@/stores/use-asset-store";
@@ -18,7 +17,6 @@ import { useUserStore } from "@/stores/use-user-store";
import { UserStatusActions } from "@/components/user-status-actions";
import { cropDataUrl } from "../utils/canvas-image-data";
import { App, Button, Dropdown, Modal } from "antd";
import { defaultConfig, type AiConfig } from "@/lib/ai-config";
import { NODE_DEFAULT_SIZE, getNodeSpec } from "../constants";
import { ActiveConnectionPath, ConnectionPath } from "../components/canvas-connections";
import { CanvasConfigNodePanel } from "../components/canvas-config-node-panel";
@@ -232,8 +230,9 @@ function InfiniteCanvasPage() {
initialSelectedNodes: [],
});
const config = useAiConfigStore((state) => state.config);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const config = useConfigStore((state) => state.config);
const effectiveConfig = useEffectiveAiConfig(config);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const addAsset = useAssetStore((state) => state.addAsset);
const cleanupAssetImages = useAssetStore((state) => state.cleanupImages);
const hydrated = useCanvasStore((state) => state.hydrated);
@@ -479,7 +478,7 @@ function InfiniteCanvasPage() {
}, [message]);
const createConnectedNode = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config, pending: PendingConnectionCreate) => {
const metadata = type === CanvasNodeType.Config ? { model: config.imageModel || config.model, size: config.size, count: 3 } : undefined;
const metadata = type === CanvasNodeType.Config ? { model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, count: 3 } : undefined;
const newNode = createCanvasNode(type, pending.position, metadata);
const connection = normalizeConnection(pending.connection.nodeId, newNode.id, [...nodesRef.current, newNode], pending.connection.handleType);
if (!connection) {
@@ -493,7 +492,7 @@ function InfiniteCanvasPage() {
setDialogNodeId(newNode.id);
setPendingConnectionCreate(null);
setConnecting(null);
}, [config.imageModel, config.model, config.size, message, setConnecting]);
}, [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, setConnecting]);
const cancelPendingConnectionCreate = useCallback(() => {
setPendingConnectionCreate(null);
@@ -591,8 +590,8 @@ function InfiniteCanvasPage() {
(type: CanvasNodeType, position?: Position) => {
const targetPosition = position || getCanvasCenter();
const configMetadata = type === CanvasNodeType.Config ? {
model: config.imageModel || config.model,
size: config.size,
model: effectiveConfig.imageModel || effectiveConfig.model,
size: effectiveConfig.size,
count: 3,
} : undefined;
const newNode = createCanvasNode(type, targetPosition, configMetadata);
@@ -602,7 +601,7 @@ function InfiniteCanvasPage() {
setSelectedConnectionId(null);
setDialogNodeId(newNode.id);
},
[config.imageModel, config.model, config.size, getCanvasCenter],
[effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, getCanvasCenter],
);
const deleteNodes = useCallback((ids: Set<string>) => {
@@ -1310,8 +1309,8 @@ function InfiniteCanvasPage() {
const generateAngleNode = useCallback(async (node: CanvasNodeData, params: CanvasImageAngleParams) => {
if (!node.metadata?.content) return;
const generationConfig = { ...buildGenerationConfig(config, node, "image"), count: "1" };
if (!generationConfig.baseUrl.trim() || !generationConfig.model.trim() || !generationConfig.apiKey.trim()) {
const generationConfig = { ...buildGenerationConfig(effectiveConfig, node, "image"), count: "1" };
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
openConfigDialog(true);
return;
}
@@ -1344,7 +1343,7 @@ function InfiniteCanvasPage() {
} finally {
setRunningNodeId(null);
}
}, [config, openConfigDialog]);
}, [effectiveConfig, openConfigDialog]);
const handleFontSizeChange = useCallback((nodeId: string, fontSize: number) => {
setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, fontSize } } : node)));
@@ -1434,8 +1433,8 @@ function InfiniteCanvasPage() {
const handleGenerateNode = useCallback(
async (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => {
const sourceNode = nodesRef.current.find((node) => node.id === nodeId);
const generationConfig = buildGenerationConfig(config, sourceNode, mode);
if (!generationConfig.baseUrl.trim() || !generationConfig.model.trim() || !generationConfig.apiKey.trim()) {
const generationConfig = buildGenerationConfig(effectiveConfig, sourceNode, mode);
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
openConfigDialog(true);
return;
}
@@ -1617,14 +1616,14 @@ function InfiniteCanvasPage() {
setRunningNodeId(null);
}
},
[config, openConfigDialog],
[effectiveConfig, openConfigDialog],
);
const handleRetryNode = useCallback(
async (node: CanvasNodeData) => {
const sourceNode = findRetrySourceNode(node.id, nodesRef.current, connectionsRef.current) || node;
const generationConfig = { ...buildGenerationConfig(config, sourceNode, node.type === CanvasNodeType.Text ? "text" : "image"), count: "1" };
if (!generationConfig.baseUrl.trim() || !generationConfig.model.trim() || !generationConfig.apiKey.trim()) {
const generationConfig = { ...buildGenerationConfig(effectiveConfig, sourceNode, node.type === CanvasNodeType.Text ? "text" : "image"), count: "1" };
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
openConfigDialog(true);
return;
}
@@ -1670,7 +1669,7 @@ function InfiniteCanvasPage() {
setRunningNodeId(null);
}
},
[config, message, openConfigDialog],
[effectiveConfig, message, openConfigDialog],
);
const generateImageFromTextNode = useCallback((node: CanvasNodeData) => {
@@ -1687,8 +1686,8 @@ function InfiniteCanvasPage() {
y: sourceNode.position.y + sourceNode.height / 2,
}, {
prompt: "",
model: config.imageModel || config.model,
size: config.size,
model: effectiveConfig.imageModel || effectiveConfig.model,
size: effectiveConfig.size,
count: 3,
});
const connection = { id: createId(), fromNodeId: sourceNode.id, toNodeId: configNode.id };
@@ -1701,7 +1700,7 @@ function InfiniteCanvasPage() {
setSelectedNodeIds(new Set([configNode.id]));
setSelectedConnectionId(null);
setDialogNodeId(configNode.id);
}, [config.imageModel, config.model, config.size, message]);
}, [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message]);
const insertAssistantImage = useCallback(async (image: CanvasAssistantImage) => {
const storedImage = image.storageKey
@@ -7,15 +7,13 @@ import { motion } from "motion/react";
import { ImageGenerationPending } from "@/components/image-generation-pending";
import { ModelPicker } from "@/components/model-picker";
import type { AiConfig } from "@/lib/ai-config";
import { isAiConfigReady, useConfigStore, useEffectiveAiConfig, type AiConfig } from "@/stores/use-config-store";
import { canvasThemes } from "@/lib/canvas-theme";
import { createId } from "@/lib/id";
import { cn } from "@/lib/utils";
import { requestEdit, requestGeneration, requestImageQuestion, type ChatCompletionMessage } from "@/services/api/image";
import { imageToDataUrl, uploadImage } from "@/services/image-storage";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useAssetStore } from "@/stores/use-asset-store";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import { useThemeStore } from "@/stores/use-theme-store";
import type { ReferenceImage } from "@/types/image";
import { DiaTextReveal } from "@/components/ui/dia-text-reveal";
@@ -42,10 +40,11 @@ type CanvasAssistantPanelProps = {
export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onInsertImage, onInsertText, onPasteImage, onCollapseStart, onCollapse }: CanvasAssistantPanelProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const config = useAiConfigStore((state) => state.config);
const config = useConfigStore((state) => state.config);
const effectiveConfig = useEffectiveAiConfig(config);
const cleanupImages = useAssetStore((state) => state.cleanupImages);
const updateConfig = useAiConfigStore((state) => state.updateConfig);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const updateConfig = useConfigStore((state) => state.updateConfig);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const [width, setWidth] = useState(390);
const [view, setView] = useState<"chat" | "history">("chat");
const [mode, setMode] = useState<AssistantMode>("image");
@@ -137,8 +136,8 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeS
};
const sendMessage = async (text: string, nextMode: AssistantMode, history: CanvasAssistantMessage[], savedReferences?: CanvasAssistantReference[]) => {
const requestConfig = { ...config, model: nextMode === "image" ? config.imageModel || config.model : config.textModel || config.model };
if (!requestConfig.baseUrl.trim() || !requestConfig.model.trim() || !requestConfig.apiKey.trim()) {
const requestConfig = { ...effectiveConfig, model: nextMode === "image" ? effectiveConfig.imageModel || effectiveConfig.model : effectiveConfig.textModel || effectiveConfig.model };
if (!isAiConfigReady(requestConfig, requestConfig.model)) {
openConfigDialog(true);
return;
}
@@ -290,7 +289,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeS
prompt={prompt}
isRunning={isRunning}
references={selectedReferences}
config={config}
config={effectiveConfig}
onModeChange={setMode}
onPromptChange={setPrompt}
onSubmit={submit}
@@ -6,10 +6,8 @@ import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Edit3, Eye, Image as ImageIc
import { App, Button, Empty, Input, InputNumber, Modal, Segmented } from "antd";
import { ModelPicker } from "@/components/model-picker";
import { defaultConfig, type AiConfig } from "@/lib/ai-config";
import { defaultConfig, useConfigStore, type AiConfig } from "@/stores/use-config-store";
import { canvasThemes } from "@/lib/canvas-theme";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasSizePicker } from "./canvas-size-picker";
import type { NodeGenerationInput } from "./canvas-node-generation";
@@ -30,8 +28,8 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, inputs, o
const [previewOpen, setPreviewOpen] = useState(false);
const [editingTextId, setEditingTextId] = useState<string | null>(null);
const [editingText, setEditingText] = useState("");
const globalConfig = useAiConfigStore((state) => state.config);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const globalConfig = useConfigStore((state) => state.config);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const mode = node.metadata?.generationMode || "image";
const config = buildNodeConfig(globalConfig, node, mode);
@@ -5,10 +5,8 @@ import { ArrowUp, LoaderCircle } from "lucide-react";
import { Button, InputNumber } from "antd";
import { ModelPicker } from "@/components/model-picker";
import { defaultConfig, type AiConfig } from "@/lib/ai-config";
import { defaultConfig, useConfigStore, type AiConfig } from "@/stores/use-config-store";
import { canvasThemes } from "@/lib/canvas-theme";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasPromptLibrary } from "./canvas-prompt-library";
import { CanvasSizePicker } from "./canvas-size-picker";
@@ -25,8 +23,8 @@ type CanvasNodePromptPanelProps = {
};
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate }: CanvasNodePromptPanelProps) {
const globalConfig = useAiConfigStore((state) => state.config);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const globalConfig = useConfigStore((state) => state.config);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const mode = defaultMode(node.type);
const config = buildNodeConfig(globalConfig, node, mode);
+12 -13
View File
@@ -8,14 +8,12 @@ import localforage from "localforage";
import { ModelPicker } from "@/components/model-picker";
import { PromptSelectDialog } from "@/components/prompts/prompt-select-dialog";
import { AssetPickerModal, type InsertAssetPayload } from "@/app/(user)/canvas/components/asset-picker-modal";
import type { AiConfig } from "@/lib/ai-config";
import { isAiConfigReady, useConfigStore, useEffectiveAiConfig, type AiConfig } from "@/stores/use-config-store";
import { createId } from "@/lib/id";
import { formatBytes, formatDuration, getDataUrlByteSize, readImageMeta } from "@/lib/image-utils";
import { requestEdit, requestGeneration } from "@/services/api/image";
import { uploadImage } from "@/services/image-storage";
import { useAssetStore } from "@/stores/use-asset-store";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useConfigDialogStore } from "@/stores/use-config-dialog-store";
import type { ReferenceImage } from "@/types/image";
type GeneratedImage = {
@@ -61,9 +59,10 @@ const logStore = localforage.createInstance({ name: "infinite-canvas", storeName
export default function ImagePage() {
const { message } = App.useApp();
const fileInputRef = useRef<HTMLInputElement>(null);
const config = useAiConfigStore((state) => state.config);
const updateConfig = useAiConfigStore((state) => state.updateConfig);
const openConfigDialog = useConfigDialogStore((state) => state.openConfigDialog);
const config = useConfigStore((state) => state.config);
const effectiveConfig = useEffectiveAiConfig(config);
const updateConfig = useConfigStore((state) => state.updateConfig);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const addAsset = useAssetStore((state) => state.addAsset);
const [prompt, setPrompt] = useState("");
const [references, setReferences] = useState<ReferenceImage[]>([]);
@@ -80,7 +79,7 @@ export default function ImagePage() {
const [previewLog, setPreviewLog] = useState<GenerationLog | null>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const model = config.imageModel || config.model;
const model = effectiveConfig.imageModel || effectiveConfig.model;
const canGenerate = Boolean(prompt.trim());
const generationCount = Math.max(1, Math.min(10, Number(config.count) || 1));
@@ -128,7 +127,7 @@ export default function ImagePage() {
message.error("请输入生图提示词");
return;
}
if (!config.baseUrl.trim() || !config.apiKey.trim() || !model) {
if (!isAiConfigReady(effectiveConfig, model)) {
message.warning("请先完成配置");
openConfigDialog(true);
return;
@@ -241,12 +240,12 @@ export default function ImagePage() {
message.error("请输入生图提示词");
return null;
}
if (!config.baseUrl.trim() || !config.apiKey.trim() || !model) {
if (!isAiConfigReady(effectiveConfig, model)) {
message.warning("请先完成配置");
openConfigDialog(true);
return null;
}
return { text, config: { ...config, model, count: "1" }, references: [...references] };
return { text, config: { ...effectiveConfig, model, count: "1" }, references: [...references] };
};
const runGenerationSlot = async (index: number, snapshot: { text: string; config: AiConfig; references: ReferenceImage[] }) => {
@@ -337,12 +336,12 @@ export default function ImagePage() {
</div>
<div className="flex items-center justify-between rounded-lg border border-stone-200 bg-stone-50 px-3 py-2 text-sm dark:border-stone-800 dark:bg-stone-900 sm:hidden">
<span className="truncate text-stone-500 dark:text-stone-400">{model} · {config.size} · {config.quality}</span>
<span className="truncate text-stone-500 dark:text-stone-400">{model} · {effectiveConfig.size} · {effectiveConfig.quality}</span>
<Button size="small" type="text" icon={<SlidersHorizontal className="size-4" />} onClick={() => setSettingsOpen(true)}></Button>
</div>
<div className="hidden gap-4 sm:grid sm:grid-cols-2">
<GenerationSettings config={config} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
<GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
</div>
</div>
@@ -390,7 +389,7 @@ export default function ImagePage() {
</Drawer>
<Drawer title="参数" placement="bottom" size="default" open={settingsOpen} onClose={() => setSettingsOpen(false)}>
<div className="grid grid-cols-2 gap-3">
<GenerationSettings config={config} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
<GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
</div>
</Drawer>
<PromptSelectDialog open={promptDialogOpen} onOpenChange={setPromptDialogOpen} onSelect={setPrompt} />
+3 -3
View File
@@ -5,12 +5,12 @@ import { usePathname } from "next/navigation";
import { AppTopNav } from "@/components/app-top-nav";
import { type NavigationToolSlug, navigationTools } from "@/lib/navigation-tools";
import { useAiConfigStore } from "@/stores/use-ai-config-store";
import { useConfigStore } from "@/stores/use-config-store";
export default function UserLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const config = useAiConfigStore((state) => state.config);
const updateConfig = useAiConfigStore((state) => state.updateConfig);
const config = useConfigStore((state) => state.config);
const updateConfig = useConfigStore((state) => state.updateConfig);
const slug = pathname.split("/").filter(Boolean)[0];
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;