fix(canvas): 主题存储 - 修复非法 theme 值导致 /create 崩溃 (#155)
useThemeStore persist 无 merge 校验,坏 session/后端同步失败时可能将 非法 theme 值写入 localStorage,导致 canvasThemes[非法值] 为 undefined, 渲染时访问 theme.node 抛出 "Cannot read properties of undefined (reading 'node')",/create 页面崩溃。 4 层防线(只加校验,不回退代码): - useThemeStore persist merge 白名单校验,非法值回退 dark - useThemeStore setTheme 写入前校验,防止同步路径绕过 merge - normalizeConfigSnapshot 入口兜底 undefined/partial 快照 - ModelPicker 与 CanvasResourceMentionTextarea 消费端加 fallback Closes #154 Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
@@ -45,7 +45,8 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
|
||||
{ value, references, onChange, onSubmit, onKeyDown, className, containerClassName, style, highlightLabels = true, mentionMenuWidth = 256, sendOnEnter = true, onContentSizeChange, ...props },
|
||||
forwardedRef,
|
||||
) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const rawTheme = useThemeStore((state) => state.theme);
|
||||
const theme = canvasThemes[rawTheme as keyof typeof canvasThemes] ?? canvasThemes.dark;
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -279,8 +279,9 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
),
|
||||
);
|
||||
|
||||
export function normalizeConfigSnapshot(snapshot: ConfigStoreSnapshot) {
|
||||
const persistedConfig = (snapshot.config || {}) as Partial<AiConfig>;
|
||||
export function normalizeConfigSnapshot(snapshot: ConfigStoreSnapshot | undefined = {}) {
|
||||
// 坏存储/旧版本快照可能是 undefined 或缺 config,兜底为 defaultConfig,保证渲染不崩溃
|
||||
const persistedConfig = (snapshot?.config || {}) as Partial<AiConfig>;
|
||||
const config = { ...defaultConfig, ...persistedConfig };
|
||||
const hasPersistedChannels = Array.isArray(persistedConfig.channels);
|
||||
if (!hasPersistedChannels) config.channels = [];
|
||||
|
||||
@@ -8,12 +8,26 @@ type ThemeStore = {
|
||||
setTheme: (theme: ThemeName) => void;
|
||||
};
|
||||
|
||||
const VALID_THEMES: ThemeName[] = ["light", "dark"];
|
||||
|
||||
export const useThemeStore = create<ThemeStore>()(
|
||||
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<ThemeStore>;
|
||||
const theme = VALID_THEMES.includes(stored.theme as ThemeName)
|
||||
? (stored.theme as ThemeName)
|
||||
: "dark";
|
||||
return { ...current, theme };
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user