fix(web): 导演台检查 - 隔离开发复现入口的外观初始化请求

This commit is contained in:
ddCat
2026-09-20 09:35:07 +08:00
parent 104396ba2f
commit 1be8ffce3b
6 changed files with 96 additions and 3 deletions

View File

@@ -69,6 +69,12 @@ PR #543 本次未合入:其已发布迁移校验和改写、管理员显式图
- [ ] 检查浅色、深色与窄屏下的费用区域、模型表格横向滚动及长金额换行。
- [ ] 新建或编辑模型,在“模型展示名(一级目录)”选择其他渠道已有分组;输入新分组保存后再次编辑,确认下拉更新且创作端归组正确。
## CI 检查修复与镜像构建提速
- [x] 修复导演台 DEV 复现入口在 React 初始化前请求 `/api/public/appearance` 导致的 E2E 502:入口和 Provider 共用精确路径隔离,普通页面与生产环境仍执行外观初始化,未放宽浏览器错误断言。
- [x] 外观启动与导演台专项测试 54 项通过,包含执行真实 `main.tsx` 分支的 9 项新增回归;生产构建(含 TypeScript 检查)通过,仍有 chunk 大小告警。
- [ ] 在 GitHub 重跑 Director P0 Chrome E2E,确认六个场景不再出现外观接口 502;本轮未启动本地开发服务器或执行 Chrome E2E,不以入口回归测试替代浏览器验收。
## 画布素材链路与工作条改造(2026-09-19)
已实现:

View File

@@ -9,6 +9,7 @@ import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader"
import { getAntThemeConfig } from "@/lib/app-theme";
import { applySkinTheme } from "@/lib/skin-themes";
import { appQueryClient } from "@/lib/query-client";
import { isIsolatedDirectorRepro } from "@/lib/dev-repro";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { applyAppearanceMetadata, useAppearanceStore } from "@/stores/use-appearance-store";
import { useUserStore } from "@/stores/use-user-store";
@@ -35,8 +36,8 @@ export function AppProviders({ children }: { children: ReactNode }) {
// DEV 复现台必须是同源本地确定性场景:AuthSessionHydrator 会打 /api/auth/session,
// ClientRootInit 会打 /api/model-catalog,没有后端时产生真实 502,与导演台无关却会污染判据。
// 只精确匹配该路径;生产构建中 import.meta.env.DEV 为 false,本分支被摇树删除。
const isolateDevRepro = import.meta.env.DEV && typeof window !== "undefined" && window.location.pathname === "/dev/director-repro";
// 与启动入口共用精确路径边界;生产构建中 import.meta.env.DEV 为 false,始终不启用隔离。
const isolateDevRepro = typeof window !== "undefined" && isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname);
return (
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark, appearance.activeSkin)} wave={{ disabled: true }}>

3
web/src/lib/dev-repro.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isIsolatedDirectorRepro(dev: boolean, pathname: string): boolean {
return dev && pathname === "/dev/director-repro";
}

View File

@@ -1,7 +1,12 @@
import "@fontsource-variable/inter";
import "@fontsource-variable/jetbrains-mono";
import { bootstrapAppearance } from "@/services/appearance-bootstrap";
import { isIsolatedDirectorRepro } from "@/lib/dev-repro";
// The public film entry checks its availability independently of workspace bootstrap.
if (/^\/welcome\/?$/.test(window.location.pathname)) void import("./welcome-application");
else void bootstrapAppearance().finally(() => import("./application"));
else {
// The backend-free DEV lab must not make requests before AppProviders isolates it.
const appearanceReady = isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname) ? Promise.resolve() : bootstrapAppearance();
void appearanceReady.finally(() => import("./application"));
}

View File

@@ -7,6 +7,8 @@ test("initial HTML stays brand neutral until the public appearance is resolved",
expect(html).not.toContain("影策");
expect(html).toContain("<title>正在加载</title>");
expect(mainSource).toContain("bootstrapAppearance()");
expect(mainSource).toContain('import("./application")');
expect(mainSource.indexOf("bootstrapAppearance()")).toBeLessThan(mainSource.indexOf('import("./application")'));
});

View File

@@ -0,0 +1,76 @@
import { expect, test } from "bun:test";
import { runInNewContext } from "node:vm";
import { isIsolatedDirectorRepro } from "../src/lib/dev-repro";
// Execute the real entry point; replace only its font, network and UI side effects.
async function prepareEntry(dev, pathname) {
const build = await Bun.build({
entrypoints: [new URL("../src/main.tsx", import.meta.url).pathname],
target: "browser",
format: "iife",
define: { "import.meta.env.DEV": JSON.stringify(dev) },
plugins: [
{
name: "entry-side-effects",
setup(builder) {
builder.onResolve({ filter: /^(@fontsource-variable\/|@\/services\/appearance-bootstrap$|\.\/(welcome-)?application$)/ }, ({ path }) => ({ path, namespace: "entry-test" }));
builder.onLoad({ filter: /.*/, namespace: "entry-test" }, ({ path }) => {
if (path.startsWith("@fontsource-variable/")) return { contents: "", loader: "js" };
if (path === "@/services/appearance-bootstrap") {
return { contents: 'export function bootstrapAppearance() { events.push("appearance"); return appearanceReady; }', loader: "js" };
}
return { contents: `events.push(${JSON.stringify(path)}); entryLoaded();`, loader: "js" };
});
},
},
],
});
expect(build.success).toBe(true);
const events = [];
let resolveAppearance;
let entryLoaded;
const appearanceReady = new Promise((resolve) => (resolveAppearance = resolve));
const loaded = new Promise((resolve) => (entryLoaded = resolve));
runInNewContext(await build.outputs[0].text(), { window: { location: { pathname } }, events, appearanceReady, entryLoaded });
return { events, resolveAppearance, loaded };
}
test("DEV director lab loads without calling the appearance backend", async () => {
const entry = await prepareEntry(true, "/dev/director-repro");
await entry.loaded;
expect(entry.events).toEqual(["./application"]);
});
for (const [dev, pathname] of [
[false, "/dev/director-repro"],
[true, "/login"],
[false, "/login"],
[true, "/dev/director-repro/"],
[true, "/dev/director-repro-other"],
]) {
test(`appearance still blocks normal startup: dev=${dev} path=${pathname}`, async () => {
const entry = await prepareEntry(dev, pathname);
expect(entry.events).toEqual(["appearance"]);
entry.resolveAppearance();
await entry.loaded;
expect(entry.events).toEqual(["appearance", "./application"]);
});
}
for (const pathname of ["/welcome", "/welcome/"]) {
test(`public film entry remains independent: ${pathname}`, async () => {
const entry = await prepareEntry(false, pathname);
await entry.loaded;
expect(entry.events).toEqual(["./welcome-application"]);
});
}
test("provider isolation shares the exact DEV-only route boundary", () => {
expect(isIsolatedDirectorRepro(true, "/dev/director-repro")).toBe(true);
expect(isIsolatedDirectorRepro(false, "/dev/director-repro")).toBe(false);
for (const path of ["/", "/login", "/dev/director-repro/", "/dev/director-repro-other"]) {
expect(isIsolatedDirectorRepro(true, path)).toBe(false);
}
});