恢复新版创作主页与平台侧栏

恢复 /creation 新版创作工具主页及创作、项目公共侧栏。

将 creation-home 重新纳入 Vite、TypeScript、ESLint 与 Vitest 活动编译链。

恢复 /creation 的 Nginx 与 Pingora SPA 白名单并保留旧子路径下线边界。

补充路由、标题、CSS 过滤和网关 smoke 回归验证。
This commit is contained in:
2026-07-18 15:41:55 +08:00
parent f625fe8e8b
commit c56da8dc26
18 changed files with 275 additions and 86 deletions
-1
View File
@@ -127,7 +127,6 @@ module.exports = {
'src/components/big-fish-result/**',
'src/components/big-fish-runtime/**',
'src/components/creation-agent/**',
'src/components/creation-home/**',
'src/components/creative-agent/**',
'src/components/custom-world-agent/**',
'src/components/custom-world-home/**',
+1 -1
View File
@@ -141,7 +141,7 @@ http {
try_files /index.html =404;
}
location ~* "^/(?:editor/canvas|project)/?$" {
location ~* "^/(?:creation|editor/canvas|project)/?$" {
try_files $uri /index.html =404;
}
# END GENARRATIVE MAIN SPA ROUTES
+1 -1
View File
@@ -190,7 +190,7 @@ server {
try_files /index.html =404;
}
location ~* "^/(?:editor/canvas|project)/?$" {
location ~* "^/(?:creation|editor/canvas|project)/?$" {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
+1 -1
View File
@@ -210,7 +210,7 @@ server {
try_files /index.html =404;
}
location ~* "^/(?:editor/canvas|project)/?$" {
location ~* "^/(?:creation|editor/canvas|project)/?$" {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
@@ -1,12 +1,12 @@
# 旧创作模板业务退役方案
更新时间:`2026-07-17`
更新时间:`2026-07-18`
## 目标
退役整个旧创作模板体系及其专属运行态,同时保留全部相关历史持久化表、迁移白名单和必要的兼容读取定义。历史数据不删除,持久化表及字段不删除、不改名、不重排、不改变类型。
本次执行口径是“数据壳保留,业务实现退役”:历史表继续以最小 schema 代码随 `spacetime-module` 编译和发布;创作入口、模板选择、生成、发布、公开业务详情、作品广场、排行榜和专属运行态不再进入正式编译链或运行路由。
本次执行口径是“数据壳保留,业务实现退役”:历史表继续以最小 schema 代码随 `spacetime-module` 编译和发布;模板选择、生成、发布、公开业务详情、作品广场、排行榜和专属运行态不再进入正式编译链或运行路由。基于图片编辑器项目与公开素材的新版 `/creation` 创作工具主页及平台公共侧边栏继续作为现役能力保留。
## 范围
@@ -25,6 +25,7 @@
- `migration.rs` 中相关表的迁移白名单、表名兼容和字段目录。
- 为历史审计、迁移、资产归属核对所必需的最小只读表定义;不得借兼容读取重新暴露旧创作、发布、公开详情或运行接口。
- 编辑器、项目、账号、钱包、资产、HostBridge、运维和安全等平台公共能力。
- 新版 `/creation` 创作工具主页、`creation-home` 展示组件与静态资产,以及桌面端“创作 / 项目”公共侧边栏;这些能力只读取编辑器项目和公开素材接口,不恢复旧模板入口。
- 旧页面、测试、素材、handler、service、worker、生成 bindings 和纯业务 crate 的源码目录;它们仅用于历史追溯,不属于任何正式入口或编译目标。
## 编译边界
@@ -32,11 +33,11 @@
### 前端
- 主路由、Vite 入口和运行时动态 import 不得引用旧业务目录。
- 正式应用入口使用 `active-main.tsx``ActiveApp.tsx``routing/activeAppRoutes.tsx``routing/activeAppPageRoutes.ts``services/activeAppTitle.ts``platform-entry/PlatformEntryActiveFlowShell.tsx``platformEntryActiveTypes.ts`;原同名非 active 文件保持历史源码原貌并退出 Vite、TypeScript、ESLint 和 Vitest。
- 正式应用入口使用 `active-main.tsx``ActiveApp.tsx``routing/activeAppRoutes.tsx``routing/activeAppPageRoutes.ts``services/activeAppTitle.ts``platform-entry/PlatformEntryActiveFlowShell.tsx``platformEntryActiveTypes.ts``creation-home/`;原同名非 active 文件保持历史源码原貌并退出 Vite、TypeScript、ESLint 和 Vitest。
- 旧业务源码与素材保留在仓库中;Vite 不得再引用入口或动态 importTypeScript、ESLint、Vitest 必须明确排除旧目录和专属测试。退役源码只用于历史追溯,不允许从在运代码重新导入。
-`main.tsx``App.tsx`、旧路由、旧标题映射、`PlatformEntryFlowShellImpl.tsx` 与旧入口类型保持原样;正式链路由 `active-main.tsx``ActiveApp.tsx``activeApp*``PlatformEntryActiveFlowShell.tsx``platformEntryActiveTypes.ts` 承载,只包含项目、编辑器、账号、设置与钱包公共能力。`retired/legacy-creation-templates/frontend/original/` 另保留逐文件原样快照。
-`main.tsx``App.tsx`、旧路由、旧标题映射、`PlatformEntryFlowShellImpl.tsx` 与旧入口类型保持原样;正式链路由 `active-main.tsx``ActiveApp.tsx``activeApp*``PlatformEntryActiveFlowShell.tsx``platformEntryActiveTypes.ts` 承载,只包含新版创作主页、项目、编辑器、账号、设置与钱包公共能力。`retired/legacy-creation-templates/frontend/original/` 另保留逐文件原样快照。
- 退役 CSS 的源码过滤必须在 Tailwind/Vite 转换前执行,产物过滤留在 `generateBundle`;禁止在 `post` transform 中把 Vite 已生成的 JavaScript 样式模块重新交给 PostCSS 解析。
- 旧 URL 统一回落当前平台公共首页,不再提供旧业务错误页、详情页或兼容壳。
- `/creation` 保持新版创作工具主页;生产网关对旧子路径返回 404,客户端若收到未知旧路径则回落当前平台公共首页,不再提供旧业务详情页或兼容壳。
### Rust
@@ -50,6 +51,7 @@
## 验收
- 旧 URL 不再命中旧页面或后端路由。
- `/creation``/project` 显示公共侧边栏,新创作主页只调用编辑器项目和公开素材接口。
- Vite 构建产物和依赖图不包含旧前端业务目录。
- `cargo tree` 中不存在纯模板 crate 或专属运行态 crate。
- `spacetime-module` 编译结果仍包含全部历史表,但不包含任何旧模板 reducer、procedure 和业务 view。
+11
View File
@@ -571,6 +571,17 @@ async function runSmokeCases(
response.headers['cache-control'] === 'no-cache',
},
);
await expectHttp(
baseUrl,
'/creation',
200,
'site-shell',
'新创作主页深链回退 index.html',
{
validate: (response) =>
response.headers['cache-control'] === 'no-cache',
},
);
await expectHttp(
baseUrl,
'/project',
+21
View File
@@ -21,4 +21,25 @@ describe('retired creation template CSS plugin', () => {
expect(plugin).toBeDefined();
expect(plugin?.enforce).toBe('pre');
});
it('keeps the active creation landing CSS while removing retired template CSS', async () => {
const plugin = await resolveRetiredCssPlugin();
const transform = plugin?.transform;
expect(typeof transform).toBe('function');
if (typeof transform !== 'function') {
return;
}
const result = await transform.call(
{} as never,
"@import 'tailwindcss';\n.creation-landing { color: red; }\n.puzzle-runtime { color: blue; }",
'/workspace/src/index.css',
);
const code = typeof result === 'string' ? result : result?.code;
expect(code).toContain('.creation-landing');
expect(code).not.toContain('.puzzle-runtime');
expect(code).toContain('@source "./components/creation-home"');
});
});
+1 -1
View File
@@ -55,7 +55,7 @@ const SHADOW_PROBE_HEADER: &str = "x-genarrative-pingora-probe";
const PAYLOAD_TOO_LARGE_CONTEXT: &str = "genarrative_payload_too_large";
const PROTECTION_STATE_TTL: Duration = Duration::from_secs(600);
const PROTECTION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
const MAIN_SPA_PATHS: &[&str] = &["/", "/editor/canvas", "/project"];
const MAIN_SPA_PATHS: &[&str] = &["/", "/creation", "/editor/canvas", "/project"];
#[derive(Clone, Debug)]
struct GatewayConfig {
@@ -1,5 +1,18 @@
import { FolderKanban, LogIn, Settings, UserRound } from 'lucide-react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import {
FolderKanban,
LogIn,
Palette,
Settings,
UserRound,
} from 'lucide-react';
import {
type ComponentType,
lazy,
Suspense,
useCallback,
useEffect,
useState,
} from 'react';
import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry';
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
@@ -25,11 +38,87 @@ const ImageCanvasEditorView = lazy(async () => {
return { default: module.ImageCanvasEditorView };
});
const CreationLandingView = lazy(async () => {
const module = await import('../creation-home/CreationLandingView');
return { default: module.CreationLandingView };
});
const ProjectGalleryView = lazy(async () => {
const module = await import('../project/ProjectGalleryView');
return { default: module.ProjectGalleryView };
});
type ActiveRailButtonProps = {
active: boolean;
emphasized?: boolean;
icon: ComponentType<{ className?: string }>;
iconSrc: string;
label: string;
onClick: () => void;
};
function ActiveRailButton({
active,
emphasized = false,
icon: Icon,
iconSrc,
label,
onClick,
}: ActiveRailButtonProps) {
return (
<button
type="button"
aria-current={active ? 'page' : undefined}
aria-label={label}
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${active ? 'platform-desktop-rail__button--active' : ''}`}
onClick={onClick}
>
<span className="platform-desktop-rail__icon-shell">
<img
src={iconSrc}
alt=""
aria-hidden="true"
draggable={false}
className="creation-home-nav-art"
/>
<Icon
className="platform-desktop-rail__icon h-[1.1rem] w-[1.1rem]"
aria-hidden="true"
/>
</span>
<span className="platform-desktop-rail__label text-[11px] font-semibold">
{label}
</span>
</button>
);
}
function ActivePlatformBrand() {
return (
<span
className="creation-home-brand-lockup shrink-0"
role="img"
aria-label="陶泥儿 GENARRATIVE"
>
<span className="platform-brand-logo">
<img
src="/branding/taonier-product-ip.png"
alt=""
aria-hidden="true"
draggable={false}
className="platform-brand-logo__image"
/>
<span className="platform-brand-logo__copy">
<span className="platform-brand-logo__title">
<span className="platform-brand-logo__title-suffix"></span>
</span>
<span className="platform-brand-logo__subtitle">GENARRATIVE</span>
</span>
</span>
</span>
);
}
function LoadingPanel({ label }: { label: string }) {
return (
<div className="flex h-full min-h-[12rem] items-center justify-center text-sm font-semibold text-[var(--platform-text-soft)]">
@@ -76,18 +165,26 @@ export function PlatformEntryFlowShellImpl({
currentUser: authUi?.user,
});
const openCreation = useCallback(() => {
pushAppHistoryPath('/creation');
setSelectionStage('creation-home', { path: '/creation' });
}, [setSelectionStage]);
const openProjects = useCallback(() => {
pushAppHistoryPath('/project');
setSelectionStage('project', { path: '/project' });
}, [setSelectionStage]);
const openEditorProject = useCallback(
(projectId: string, options?: { guide?: boolean }) => {
(projectId: string, options?: { guide?: boolean; tool?: string }) => {
const params = new URLSearchParams();
params.set('projectid', projectId);
if (options?.guide) {
params.set('guide', 'toolbar');
}
if (options?.tool) {
params.set('tool', options.tool);
}
const path = `/editor/canvas?${params.toString()}`;
pushAppHistoryPath(path);
setSelectionStage('image-editor', { path });
@@ -118,76 +215,121 @@ export function PlatformEntryFlowShellImpl({
profileCenter.rechargeCenter?.walletBalance ??
dashboard?.walletBalance ??
null;
const isCreationStage =
selectionStage === 'platform' || selectionStage === 'creation-home';
return (
<>
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
<header className="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-4 sm:px-6">
<button
type="button"
className="flex min-w-0 items-center gap-3 text-left"
onClick={openProjects}
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--platform-accent-strong)] text-white">
<FolderKanban className="h-5 w-5" aria-hidden="true" />
</span>
<span className="truncate text-base font-black text-[var(--platform-text-strong)]">
</span>
</button>
<div className="flex shrink-0 items-center gap-2">
{isAuthenticated ? (
<PlatformMudPointWalletEntry
variant="desktop"
balance={balance}
breakdown={
profileCenter.rechargeCenter?.mudPointBalance ?? null
}
isLoading={
isLoadingDashboard || profileCenter.isLoadingRechargeCenter
}
error={profileCenter.rechargeError}
onRequestDetails={profileCenter.loadRechargeCenter}
onRecharge={() => {
profileCenter.setIsRechargeOpen(true);
profileCenter.loadRechargeCenter();
}}
onOpenLedger={profileCenter.openWalletLedgerPanel}
/>
) : (
<PlatformActionButton
size="xs"
onClick={() => authUi?.openLoginModal()}
<div
className={`flex h-full min-h-0 min-w-0 flex-col overflow-hidden ${isCreationStage ? 'creation-home-stage--landing' : ''}`}
>
<div className="platform-desktop-shell platform-desktop-shell--workbench flex h-full min-h-0 flex-col">
<div className="platform-desktop-layout flex min-h-0 flex-1">
<aside className="platform-desktop-leftbar hidden shrink-0 flex-col lg:flex">
<ActivePlatformBrand />
<nav
className="platform-desktop-rail flex shrink-0 flex-col gap-3"
aria-label="平台导航"
>
<LogIn className="h-4 w-4" aria-hidden="true" />
</PlatformActionButton>
)}
<PlatformIconButton
label={isAuthenticated ? '账号' : '登录'}
icon={<UserRound className="h-5 w-5" aria-hidden="true" />}
onClick={() => {
if (isAuthenticated) {
authUi?.openAccountModal();
return;
}
authUi?.openLoginModal();
}}
/>
<PlatformIconButton
label="设置"
icon={<Settings className="h-5 w-5" aria-hidden="true" />}
onClick={() => authUi?.openSettingsModal()}
/>
</div>
</header>
<ActiveRailButton
active={isCreationStage}
emphasized={isCreationStage}
icon={Palette}
iconSrc="/creation-home/nav-create.png"
label="创作"
onClick={openCreation}
/>
<ActiveRailButton
active={selectionStage === 'project'}
icon={FolderKanban}
iconSrc="/creation-home/nav-projects.png"
label="项目"
onClick={openProjects}
/>
</nav>
</aside>
<main className="min-h-0 min-w-0 flex-1 overflow-auto px-3 py-4 sm:px-6 sm:py-6">
<Suspense fallback={<LoadingPanel label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
</Suspense>
</main>
<div className="platform-desktop-main flex min-w-0 flex-1 flex-col">
<header className="platform-desktop-topbar flex min-h-16 shrink-0 items-center justify-between gap-3 px-4 sm:px-6">
<div className="min-w-0 lg:hidden">
<ActivePlatformBrand />
</div>
<span className="hidden truncate text-base font-black text-[var(--platform-text-strong)] lg:block">
{isCreationStage ? '创作工具' : '项目'}
</span>
<div className="platform-desktop-topbar__actions flex shrink-0 items-center gap-2">
{isAuthenticated ? (
<PlatformMudPointWalletEntry
variant="desktop"
balance={balance}
breakdown={
profileCenter.rechargeCenter?.mudPointBalance ?? null
}
isLoading={
isLoadingDashboard ||
profileCenter.isLoadingRechargeCenter
}
error={profileCenter.rechargeError}
onRequestDetails={profileCenter.loadRechargeCenter}
onRecharge={() => {
profileCenter.setIsRechargeOpen(true);
profileCenter.loadRechargeCenter();
}}
onOpenLedger={profileCenter.openWalletLedgerPanel}
/>
) : (
<PlatformActionButton
size="xs"
onClick={() => authUi?.openLoginModal()}
>
<LogIn className="h-4 w-4" aria-hidden="true" />
</PlatformActionButton>
)}
<PlatformIconButton
label={isAuthenticated ? '账号' : '登录'}
icon={
<UserRound className="h-5 w-5" aria-hidden="true" />
}
onClick={() => {
if (isAuthenticated) {
authUi?.openAccountModal();
return;
}
authUi?.openLoginModal();
}}
/>
<PlatformIconButton
label="设置"
icon={<Settings className="h-5 w-5" aria-hidden="true" />}
onClick={() => authUi?.openSettingsModal()}
/>
</div>
</header>
<main
id={isCreationStage ? 'platform-tab-panel-create' : undefined}
className={`platform-tab-panel min-h-0 min-w-0 flex-1 overflow-auto ${isCreationStage ? '' : 'px-3 py-4 sm:px-6 sm:py-6'}`}
>
{isCreationStage ? (
<Suspense
fallback={<LoadingPanel label="正在加载创作主页..." />}
>
<CreationLandingView
onOpenProject={openEditorProject}
onOpenProjects={openProjects}
/>
</Suspense>
) : (
<Suspense fallback={<LoadingPanel label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
</Suspense>
)}
</main>
</div>
</div>
</div>
</div>
{profileCenter.isRechargeOpen ? (
@@ -8,7 +8,7 @@ export type { PlatformEntryFlowShellProps, SelectionStage };
/**
* 平台入口通用壳层。
* 平台运行入口承载项目、编辑器与账户公共能力。
* 平台运行入口承载新创作主页、项目、编辑器与账户公共能力。
*/
export function PlatformEntryFlowShell(props: PlatformEntryFlowShellProps) {
return <PlatformEntryFlowShellImpl {...props} />;
@@ -1,4 +1,8 @@
export type SelectionStage = 'platform' | 'project' | 'image-editor';
export type SelectionStage =
| 'platform'
| 'creation-home'
| 'project'
| 'image-editor';
export type PlatformEntryFlowShellProps = {
selectionStage: SelectionStage;
+4 -2
View File
@@ -14,17 +14,19 @@ import {
} from './activeAppPageRoutes';
describe('appPageRoutes', () => {
it('only resolves public platform, project, and editor stages', () => {
it('resolves the active creation, project, and editor stages', () => {
expect(resolveSelectionStageFromPath('/')).toBe('platform');
expect(resolveSelectionStageFromPath('/creation')).toBe('creation-home');
expect(resolveSelectionStageFromPath('/project')).toBe('project');
expect(resolveSelectionStageFromPath('/EDITOR/CANVAS/')).toBe(
'image-editor',
);
expect(resolveSelectionStageFromPath('/creation')).toBe('platform');
expect(resolveSelectionStageFromPath('/creation/rpg')).toBe('platform');
expect(resolveSelectionStageFromPath('/runtime/puzzle')).toBe('platform');
expect(isKnownMainAppPagePath('/creation')).toBe(true);
expect(isKnownMainAppPagePath('/creation/rpg')).toBe(false);
expect(isKnownMainAppPagePath('/runtime/puzzle')).toBe(false);
expect(resolvePathForSelectionStage('creation-home')).toBe('/creation');
expect(resolvePathForSelectionStage('project')).toBe('/project');
});
+1
View File
@@ -5,6 +5,7 @@ export const EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM = 'projectid';
const STAGE_ROUTE_ENTRIES = [
['platform', '/'],
['creation-home', '/creation'],
['project', '/project'],
['image-editor', '/editor/canvas'],
] as const satisfies readonly (readonly [SelectionStage, string])[];
+3
View File
@@ -24,6 +24,9 @@ afterEach(() => {
describe('activeAppTitle', () => {
test('maps only active platform stages', () => {
expect(resolveAppTitleForSelectionStage('platform')).toBe(APP_HOME_TITLE);
expect(resolveAppTitleForSelectionStage('creation-home')).toBe(
APP_HOME_TITLE,
);
expect(resolveAppTitleForSelectionStage('project')).toBe('项目 - 陶泥儿');
expect(resolveAppTitleForSelectionStage('image-editor')).toBe(
'美术编辑器 - 陶泥儿',
+1
View File
@@ -7,6 +7,7 @@ export const APP_HOME_TITLE =
const APP_TITLE_BY_SELECTION_STAGE: Record<SelectionStage, string> = {
platform: APP_HOME_TITLE,
'creation-home': APP_HOME_TITLE,
project: '项目 - 陶泥儿',
'image-editor': '美术编辑器 - 陶泥儿',
};
+2
View File
@@ -16,6 +16,8 @@
"src/services/activeAppTitle.test.ts",
"src/components/auth/**/*.test.ts",
"src/components/auth/**/*.test.tsx",
"src/components/creation-home/**/*.test.ts",
"src/components/creation-home/**/*.test.tsx",
"src/components/image-editor/**/*.test.ts",
"src/components/image-editor/**/*.test.tsx",
"src/components/project/**/*.test.ts",
+3 -4
View File
@@ -7,7 +7,7 @@ import postcss from 'postcss';
import { defineConfig, loadEnv, type Plugin } from 'vite';
const RETIRED_TEMPLATE_CSS_PATTERN =
/(?:baby-object|bark-battle|big-fish|child-motion|creation-(?:agent|home|landing|work)|creative-agent|custom-world|jump-hop|match3d|platform-recommend|platform-work-detail|public-work|puzzle|rpg|square-hole|unified-creation|visual-novel|wooden-fish)/iu;
/(?:baby-object|bark-battle|big-fish|child-motion|creation-(?:agent|work)|creative-agent|custom-world|jump-hop|match3d|platform-recommend|platform-work-detail|public-work|puzzle|rpg|square-hole|unified-creation|visual-novel|wooden-fish)/iu;
const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
@source "./active-main.tsx";
@@ -15,6 +15,7 @@ const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
@source "./AuthenticatedApp.tsx";
@source "./components/auth";
@source "./components/common";
@source "./components/creation-home";
@source "./components/image-editor";
@source "./components/project";
@source "./components/platform-entry/PlatformEntryFlowShell.tsx";
@@ -33,7 +34,6 @@ const RETIRED_PUBLIC_ASSET_PATHS = [
'/bark-battle-assets',
'/character',
'/child-motion-demo',
'/creation-home',
'/creation-type-references',
'/edutainment-baby-object',
'/generated-character-drafts',
@@ -165,7 +165,6 @@ export default defineConfig(({ mode }) => {
'**/public/anthro-cat-illustrations/**',
'**/public/bark-battle-assets/**',
'**/public/character/**',
'**/public/creation-home/**',
'**/public/creation-type-references/**',
'**/public/edutainment-baby-object/**',
'**/public/match3d-background-references/**',
@@ -177,7 +176,7 @@ export default defineConfig(({ mode }) => {
'**/src/components/asset-studio/**',
'**/src/components/bark-battle-creation/**',
'**/src/components/big-fish-*/**',
'**/src/components/creation-*/**',
'**/src/components/creation-agent/**',
'**/src/components/creative-agent/**',
'**/src/components/custom-world-*/**',
'**/src/components/edutainment-*/**',
+2
View File
@@ -26,6 +26,8 @@ export default defineConfig({
'src/services/payment/**/*.test.ts',
'src/components/auth/**/*.test.ts',
'src/components/auth/**/*.test.tsx',
'src/components/creation-home/**/*.test.ts',
'src/components/creation-home/**/*.test.tsx',
'src/components/image-editor/**/*.test.ts',
'src/components/image-editor/**/*.test.tsx',
'src/components/project/**/*.test.ts',