import { useCallback, useMemo, useState } from "react"; import { Button, Table } from "antd"; import { Switch } from "@/components/ui/base/switch"; import { CanvasDirectorWorkbench } from "@/components/canvas/director/canvas-director-workbench"; import { DIRECTOR_REPRO_MATRIX, createDirectorReproScene, directorReproSceneIsOffline, injectDirectorReproModel, type DirectorReproModelVariant } from "@/lib/canvas/director/director-repro-fixture"; import { readDirectorReproSnapshot, type DirectorReproSnapshot } from "@/lib/canvas/director/director-repro-runtime"; import { resetDirectorDiagnosticDedupe } from "@/lib/canvas/director/director-diagnostics-recorder"; import { getClientDiagnosticEvents } from "@/services/diagnostics/client-diagnostics"; import { StatusBadge } from "@/components/ui/base/badges"; import type { DirectorScene } from "@/types/director"; /** * P0 手工复现入口(仅 DEV 注册)。 * * 渲染真实 CanvasDirectorWorkbench + 确定性 fixture,不是静态 mock。 * 本页自己的 callback 都是本地确定性实现,不调用任何 API: * onChange 只写本页 state,onFlush 只在「强制失败」开关打开时抛错, * onApply 只累加本地计数。 * * 范围声明:只有「初始 fixture + 本页 callback」保证不发网。 * 真实工作台自带的新增控件(默认演员 GLB、上传模型、画布图片立牌) * 仍可能联网,这是刻意保留的真实行为 —— 不要据此宣称整个工作台永不发网。 */ export default function DirectorReproLab() { const [scene, setScene] = useState(() => createDirectorReproScene()); // workbench 是 fixed inset-0 全屏浮层,会完全遮住本页。默认关闭, // 让环境快照 / 事件列表 / 复现矩阵先可读,再由用户显式打开进入复现。 const [workbenchOpen, setWorkbenchOpen] = useState(false); const [forceSaveFailure, setForceSaveFailure] = useState(false); const [appliedCount, setAppliedCount] = useState(0); const [flushCount, setFlushCount] = useState(0); const [events, setEvents] = useState(() => readDirectorEvents()); const snapshot = useMemo(() => readDirectorReproSnapshot(), []); const refreshEvents = useCallback(() => setEvents(readDirectorEvents()), []); /** 本地确定性保存:只写本页 state,不触碰项目 store,也不发请求。 */ const onChange = useCallback((next: DirectorScene) => setScene(next), []); /** 强制失败开关用于复现「保存失败 / 关闭恢复」,抛错后由 coordinator 走真实 error 语义。 */ const onFlush = useCallback(() => { setFlushCount((count) => count + 1); if (forceSaveFailure) throw new Error("repro forced flush failure"); }, [forceSaveFailure]); const onApply = useCallback(async () => { setAppliedCount((count) => count + 1); }, []); const reset = useCallback(() => { resetDirectorDiagnosticDedupe(); setScene(createDirectorReproScene()); setForceSaveFailure(false); setAppliedCount(0); setFlushCount(0); setWorkbenchOpen(false); refreshEvents(); }, [refreshEvents]); /** 显式注入模型对象:固定 id,重复点击替换同一对象而不是堆叠。 */ const injectModel = useCallback((variant: DirectorReproModelVariant) => { setScene((current) => injectDirectorReproModel(current, variant)); }, []); return (

导演台 P0 复现台

对象数 {scene.objects.length} 强制保存失败
{workbenchOpen ? ( setWorkbenchOpen(false)} onChange={onChange} onApply={onApply} onDeleteImageNode={() => undefined} onFlush={onFlush} /> ) : null}
); } type DirectorEventRow = { id: string; timestamp: string; level: string; code: string; message: string }; /** 只显示导演台自己的稳定码事件,避免整条诊断流噪声淹没复现现场。 */ function readDirectorEvents(): DirectorEventRow[] { return getClientDiagnosticEvents() .filter((event) => (event.code || "").startsWith("DIRECTOR_")) .map((event) => ({ id: event.id, timestamp: event.timestamp, level: event.level, code: event.code || "", message: event.message })) .reverse(); } function EnvironmentSnapshot({ snapshot, appliedCount, flushCount, sceneRevisionHint }: { snapshot: DirectorReproSnapshot; appliedCount: number; flushCount: number; sceneRevisionHint: string }) { const { runtime, webgl } = snapshot; const rows: Array<[string, string]> = [ ["应用版本", runtime.appVersion], ["构建提交", runtime.buildCommit], ["浏览器", runtime.browser || "(不可用)"], ["操作系统", runtime.os || "(不可用)"], ["时区", runtime.timezone || "(不可用)"], ["DPR", String(runtime.devicePixelRatio)], ["本地 onApply 次数", String(appliedCount)], ["本地 onFlush 次数", String(flushCount)], ["场景 updatedAt", sceneRevisionHint], ]; if (webgl.available) { rows.push( ["WebGL 版本", webgl.version || "(空)"], ["WebGL vendor", webgl.vendor || "(空)"], ["WebGL renderer", webgl.renderer || "(空)"], ["最大纹理尺寸", String(webgl.maxTextureSize)], ["最大 renderbuffer", String(webgl.maxRenderbufferSize)], ["最大视口", webgl.maxViewportWidth + "×" + webgl.maxViewportHeight], ); } else { rows.push(["WebGL", webgl.reason === "unsupported" ? "不支持(已稳定降级)" : "上下文创建失败(已稳定降级)"]); } return (

环境快照

仅本地展示,不上报;字符串已限长并剥离 URL query/hash 与凭证片段。

{rows.map(([label, value]) => (
{label} {value}
))}
); } function DiagnosticEventList({ events }: { events: DirectorEventRow[] }) { return (

结构化事件({events.length})

来自统一 client-diagnostics 缓冲区,只筛选 DIRECTOR_ 稳定码;不含 stack、URL 与业务正文。

{events.length ? ( {value} }, { title: "级别", dataIndex: "level", width: 96, render: (value: string) => }, { title: "稳定码", dataIndex: "code", render: (value: string) => {value} }, { title: "消息", dataIndex: "message" }, ]} /> ) : (

暂无导演台事件。触发下方矩阵中的失败场景后点击「刷新事件」。

)} ); } function ReproMatrix() { return (

P0 手工复现矩阵({DIRECTOR_REPRO_MATRIX.length})

); }