diff --git a/web/src/components/canvas/canvas-resource-mention-textarea.tsx b/web/src/components/canvas/canvas-resource-mention-textarea.tsx index 002f91c6c..c97b9e50e 100644 --- a/web/src/components/canvas/canvas-resource-mention-textarea.tsx +++ b/web/src/components/canvas/canvas-resource-mention-textarea.tsx @@ -45,7 +45,8 @@ export const CanvasResourceMentionTextarea = forwardRef state.theme)]; + const rawTheme = useThemeStore((state) => state.theme); + const theme = canvasThemes[rawTheme as keyof typeof canvasThemes] ?? canvasThemes.dark; const containerRef = useRef(null); const textareaRef = useRef(null); const editorRef = useRef(null); diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx index 4e31f2f7e..9d4b4e866 100644 --- a/web/src/components/model-picker.tsx +++ b/web/src/components/model-picker.tsx @@ -2,7 +2,7 @@ import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent } from import { Check, ChevronDown, Coins, Cpu } from "lucide-react"; import { Popover } from "antd"; -import { canvasThemes } from "@/lib/canvas-theme"; +import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme"; import { cn } from "@/lib/utils"; import { modelDisplayName, modelOptionLabel, modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store"; import { useThemeStore } from "@/stores/use-theme-store"; @@ -24,7 +24,9 @@ type ModelPickerProps = { export function ModelPicker({ config, value, onChange, capability, className, fullWidth = false, placeholder = "选择模型", onMissingConfig, showSelectedPrice = true, variant = "default" }: ModelPickerProps) { const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); const pickerId = useId(); - const theme = canvasThemes[useThemeStore((state) => state.theme)]; + // 双保险:即使 store merge 写出非法 theme,这里也兜底到 dark,避免 "reading 'node'" 崩溃 + const rawTheme = useThemeStore((state) => state.theme); + const theme = (canvasThemes[rawTheme as keyof typeof canvasThemes] ?? canvasThemes.dark) as CanvasTheme; const [open, setOpen] = useState(false); const menuRef = useRef(null); const triggerRef = useRef(null); diff --git a/web/src/stores/use-config-store.ts b/web/src/stores/use-config-store.ts index b9f5abdfb..a0843e67c 100644 --- a/web/src/stores/use-config-store.ts +++ b/web/src/stores/use-config-store.ts @@ -279,8 +279,9 @@ export const useConfigStore = create()( ), ); -export function normalizeConfigSnapshot(snapshot: ConfigStoreSnapshot) { - const persistedConfig = (snapshot.config || {}) as Partial; +export function normalizeConfigSnapshot(snapshot: ConfigStoreSnapshot | undefined = {}) { + // 坏存储/旧版本快照可能是 undefined 或缺 config,兜底为 defaultConfig,保证渲染不崩溃 + const persistedConfig = (snapshot?.config || {}) as Partial; const config = { ...defaultConfig, ...persistedConfig }; const hasPersistedChannels = Array.isArray(persistedConfig.channels); if (!hasPersistedChannels) config.channels = []; diff --git a/web/src/stores/use-theme-store.ts b/web/src/stores/use-theme-store.ts index b6073cee5..466f337f8 100644 --- a/web/src/stores/use-theme-store.ts +++ b/web/src/stores/use-theme-store.ts @@ -8,12 +8,26 @@ type ThemeStore = { setTheme: (theme: ThemeName) => void; }; +const VALID_THEMES: ThemeName[] = ["light", "dark"]; + export const useThemeStore = create()( persist( (set) => ({ theme: "dark", - setTheme: (theme) => set({ theme }), + // 写入前校验:非法 theme 直接忽略,防止 syncRemoteUserData 等路径绕过 merge 写入坏值 + setTheme: (next) => set((state) => (VALID_THEMES.includes(next) ? { theme: next } : state)), }), - { name: "infinite-canvas:theme_store" }, + { + name: "infinite-canvas:theme_store", + // 持久化恢复校验:旧版本/坏 session 写入的非法值回退到 dark, + // 避免 canvasThemes[非法值] = undefined 触发 "reading 'node'" 崩溃 + merge: (persisted, current) => { + const stored = (persisted || {}) as Partial; + const theme = VALID_THEMES.includes(stored.theme as ThemeName) + ? (stored.theme as ThemeName) + : "dark"; + return { ...current, theme }; + }, + }, ), );