Replicate target video canvas frontend
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-18 11:50:04 +08:00
parent 0edd9872ea
commit 799dd3007b
6 changed files with 12929 additions and 46 deletions

View File

@ -89,6 +89,7 @@ import {
import { CanvasWorkspace } from "@/components/canvas";
import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel";
import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel";
import { LiblibVideoCanvasReplica } from "@/components/canvas/LiblibVideoCanvasReplica";
import { ObjectPickerOverlay } from "@/components/canvas/ObjectPickerOverlay";
import { SceneFusionDialog } from "@/components/scene-fusion-dialog";
import { Badge } from "@/components/ui/badge";
@ -1340,6 +1341,24 @@ function getFirstResultUrlFromCollections(
}
export default function CanvasEditorPage({ params }: PageProps) {
const routeParams = useParams();
const searchParams = useSearchParams();
const id = (routeParams?.id as string) || use(params).id;
const mode = searchParams.get("mode");
const requestedWorkspaceType = searchParams.get("workspaceType");
const requestedAgentMode = mode === "agent";
const isVideoMode =
!requestedAgentMode &&
(mode === "video" || requestedWorkspaceType === "video");
if (isVideoMode) {
return <LiblibVideoCanvasReplica canvasId={id} />;
}
return <CanvasEditorLegacyPage params={params} />;
}
function CanvasEditorLegacyPage({ params }: PageProps) {
// 使用 useParams 确保客户端导航时能正确获取更新的 id
const routeParams = useParams();
const searchParams = useSearchParams();

View File

@ -0,0 +1,49 @@
# LibTV frontend capture
Captured URL: https://www.liblib.tv/canvas?projectId=ed9a2c12570a49b9a1186017b2c38b52
This folder contains the browser-delivered frontend assets and API responses that were accessible with the logged-in Chrome session:
- `snapshot.html`: hydrated page HTML snapshot after opening the canvas.
- `assets/`: downloaded JS/CSS/static assets loaded or referenced by the page.
- `api/`: captured JSON/API responses and worker/icon resources where applicable.
- `network.har`: embedded HAR of the capture session.
- `manifest.json`: URL to local file mapping.
- `project-detail.json`: canvas project detail API response.
Limits: this is not the private source repository or server-side code. Backend logic, model generation services, database code, and non-public source files are not delivered to the browser and cannot be recovered from the page. The project itself is currently empty: `nodes` and `edges` are both empty.
Public sourcemaps downloaded: 0.
## Added readable analysis
- `readable/`: formatted copies of JS/CSS/HTML and pretty-printed JSON.
- `api-endpoints.md` / `api-endpoints.json`: grouped endpoint inventory.
- `feature-map.md`: observed UI features mapped to network calls.
- `keyword-index.md` / `keyword-index.json`: keyword hits inside formatted JS bundles.
- `network.sanitized.har`: HAR with cookie/auth-like fields redacted.
- `deep-completeness-review.md`: final layered completeness review of what is complete, what is partial, and what cannot be recovered from a browser capture.
The original web app did not expose public sourcemaps, so `readable/js` is formatted production bundle code, not the original authored TypeScript/React source tree.
## Module Split
The production Turbopack chunks have also been split into per-module factory files for easier reading and searching.
- Module files: `module-split/`
- Module index: `module-split/module-index.md` and `module-split/module-index.json`
- Module keyword index: `module-split/module-keyword-index.md` and `module-split/module-keyword-index.json`
- Extracted modules: 1872 from 70 Turbopack chunks
## Completeness Audit
A static and safe dynamic-click audit was run after the initial capture.
- Audit report: `capture-audit.md` / `capture-audit.json`
- Deep review: `deep-completeness-review.md`
- Dynamic Skills audit: `dynamic-audit/`
- Dynamic toolbar/add-node/share audit: `dynamic-audit-toolbars/`
- New JS/CSS/chunk resources found dynamically: 0
- New JSON/media resources saved dynamically: 17
Security note: the raw capture folder and full archive may include logged-in session traces. Use the sanitized archive for sharing.

View File

@ -0,0 +1,101 @@
# LibLib Canvas 前端获取完整性深度审计
生成时间:2026-04-18
项目地址:https://www.liblib.tv/canvas?projectId=ed9a2c12570a49b9a1186017b2c38b52
## 结论
这份抓取对于“浏览器实际交付到当前登录画布页的生产前端代码”完整性很高:静态 HTML、JS、CSS、字体、图片、主要 API JSON、画布工具配置、动态安全点击后新增的 JSON/图片资源都已落盘;没有发现缺失的浏览器 JS/CSS chunk。
但它不是原始源码仓库,也不是后端代码。没有公开 sourcemap,因此无法还原成一模一样的 TypeScript/React 源码目录;浏览器也拿不到服务端私有实现、鉴权逻辑、任务调度、支付、上传、生成等后端代码。
## 已确认完整的部分
- 静态资源清单:171 条,全部 HTTP 200,本地缺失 0。
- 脱敏 HAR:205 条请求,全部 HTTP 200,失败 0。
- 浏览器前端包:74 个 JS、11 个 CSS、3 个 HTML、51 个格式化 API JSON。
- Turbopack 模块拆分:74 个 JS 文件中提取 70 个 Turbopack chunk,1872 个模块,解析错误 0。
- HTML/JS 中引用的完整静态 chunk URL:83 个,清单缺失 0。
- HTML 中直接引用的静态 chunk URL:66 个,清单缺失 0。
- 画布功能模块配置:67 个 tool spec、25 个 workflow preset 已从接口保存。
- 动态安全点击审计:两轮共 281 条响应,新增资源均为 JSON/图片/埋点,没有新增 JS/CSS/chunk。
## 画布功能模块覆盖
画布工具配置来自 `/api/tool_spec/list` 与 `/api/canvas/workflow/list`,已整理到 `canvas-modules.json` 和 `canvas-modules.md`。
已覆盖的工具类型:
- image:22
- video:38
- text:3
- audio:3
- compose:1
已覆盖的模式:
- image2image:17
- image2text:3
- video2text:3
- mixed2video:8
- frames2video:24
- videoEdit2video:3
- audio2video:9
- singleImage2video:31
- image2video:9
- video2video:2
这说明画布中可添加的主要节点、模型、参数、属性枚举、工作流预设已经拿到配置层数据。真正点击“生成”、真实上传素材、真实支付、发布成功后的后端任务响应不属于静态前端配置,未强行触发。
## 动态交互覆盖
执行过的安全交互包括:
- 左侧添加面板及文本、图片、视频、视频组合、音频、脚本、上传、图库入口。
- 左侧 workflow、assets、history、help、support。
- 顶部 share/publish 相关入口、会员入口、头像入口。
- LibTV Skills/access key 相关弹窗入口。
为避免改动项目,审计时阻断了草稿更新、节点批量写入、版本创建、发布、分享开关等写接口。动态审计新增资源包括媒体素材 tag tree、会员 modelCards、activity config、若干 PNG/SVG 静态素材;没有新增前端代码包。
## Worker 与异步资源检查
- `draft-save.worker.js` 是真实独立 Worker,已抓取为 `assets/draft-save.worker.593c8f9e44.js`,并在 `readable/js/` 中有可读化副本。
- `elk-worker.min.js` 字符串出现在 ELK 布局库的 Browserify/Turbopack 内部模块依赖映射中,属于打包进现有 chunk 的内部模块,不是浏览器另行请求的缺失文件。
- HLS、代码高亮等第三方库里有 Worker 创建逻辑,但动态审计没有发现它们拉取额外 JS/CSS 静态资源。
## 当前项目实例状态
项目详情接口显示该画布项目名为“未命名”,项目草稿里:
- `nodeList`:0
- `connectionList`:null
- `draftData.nodes`:0
- `draftData.edges`:0
所以这次拿到的是“空画布页面及其完整可用功能前端/配置”,不是某个已有复杂画布的节点实例数据。
## 不能算完整的边界
以下内容不是遗漏,而是浏览器抓包天然拿不到,或出于安全没有触发:
- 原始源码仓库、源码注释、源码文件名、未压缩组件层级。
- 后端服务代码、数据库结构、任务队列、模型调用实现。
- 未公开 sourcemap;本地没有 `.map` 文件。
- 需要真实消耗积分/付费/上传/生成/删除/发布成功后的动作级响应。
- 只有特定账号权限、特定素材、已有节点数据、或服务端灰度开关才会出现的私有返回。
## 风险提示
原始抓取目录和全量压缩包可能包含登录态、用户信息或请求头痕迹,仅适合本机分析。对外传递应使用重新生成后的脱敏包,或只传 `readable/`、`module-split/`、`canvas-modules.*`、`feature-map.md`、`api-endpoints.*` 这类分析产物。
## 最终判断
完整性分层判断:
- 浏览器交付的生产前端运行代码:高,可以认为当前访问路径下已完整。
- 画布功能模块配置:高,主要工具、模型、参数和 workflow 已保存。
- 弹窗和普通 UI 交互前端:中高,安全可点入口已覆盖,未发现新代码包。
- 真实业务动作返回:部分,仅保存了已触发/安全触发的 API JSON。
- 原始前端源码仓库:不完整,公开页面无法直接取得。
- 后端代码:不完整,浏览器无法取得。

View File

@ -184,6 +184,7 @@ interface CanvasWorkspaceProps {
workspaceVariant?: "canvas" | "video";
layerSplitEnabled?: boolean;
showEmptyState?: boolean;
showZoomControls?: boolean;
onBlankCanvasDoubleClick?: (payload: {
screenPosition: Position;
canvasPosition: Position;
@ -197,6 +198,7 @@ export function CanvasWorkspace({
workspaceVariant = "canvas",
layerSplitEnabled = false,
showEmptyState = true,
showZoomControls = true,
onBlankCanvasDoubleClick,
}: CanvasWorkspaceProps) {
const SNAP_THRESHOLD = 10;
@ -1525,7 +1527,8 @@ export function CanvasWorkspace({
<div
ref={containerRef}
className={cn(
"relative w-full h-full overflow-hidden overscroll-none touch-none bg-background",
"relative w-full h-full overflow-hidden overscroll-none touch-none",
isWorkflowCanvas ? "bg-[#050607] text-white" : "bg-background",
tool === 'pan' && "cursor-grab",
tool === 'draw' && "cursor-crosshair",
isPanning && "cursor-grabbing"
@ -1544,12 +1547,12 @@ export function CanvasWorkspace({
>
{isWorkflowCanvas ? (
<>
<div className="pointer-events-none absolute inset-0 pattern-grid-lg text-foreground opacity-[0.028]" />
<div className="pointer-events-none absolute inset-0 pattern-grid-lg text-white opacity-[0.065]" />
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"radial-gradient(circle at top left, rgba(91,118,254,0.05), transparent 26%), radial-gradient(circle at bottom right, rgba(164,176,220,0.08), transparent 26%)",
"radial-gradient(circle at top left, rgba(91,118,254,0.08), transparent 24%), radial-gradient(circle at bottom right, rgba(164,176,220,0.05), transparent 28%)",
}}
/>
</>
@ -1830,52 +1833,54 @@ export function CanvasWorkspace({
</div>
)}
<div
className={cn(
zoomOverlayClassName,
isWorkflowCanvas
? "flex items-center gap-1.5 px-1.5 py-1.5"
: "flex items-center gap-1 px-1 py-1 border border-border/50"
)}
data-canvas-overlay
>
<Button
variant="ghost"
size="icon"
className={cn("h-7 w-7", isWorkflowCanvas && "rounded-full")}
onClick={() => {
const newZoom = Math.max(0.1, viewport.zoom - 0.1);
setViewport({ zoom: newZoom });
}}
title="缩小"
>
<Minus className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
{showZoomControls ? (
<div
className={cn(
"h-7 min-w-[50px] text-xs font-medium",
isWorkflowCanvas && "rounded-full px-3"
zoomOverlayClassName,
isWorkflowCanvas
? "flex items-center gap-1.5 px-1.5 py-1.5"
: "flex items-center gap-1 px-1 py-1 border border-border/50"
)}
onClick={() => setViewport({ zoom: 1, x: 0, y: 0 })}
title="重置缩放"
data-canvas-overlay
>
{Math.round(viewport.zoom * 100)}%
</Button>
<Button
variant="ghost"
size="icon"
className={cn("h-7 w-7", isWorkflowCanvas && "rounded-full")}
onClick={() => {
const newZoom = Math.min(4, viewport.zoom + 0.1);
setViewport({ zoom: newZoom });
}}
title="放大"
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
<Button
variant="ghost"
size="icon"
className={cn("h-7 w-7", isWorkflowCanvas && "rounded-full")}
onClick={() => {
const newZoom = Math.max(0.1, viewport.zoom - 0.1);
setViewport({ zoom: newZoom });
}}
title="缩小"
>
<Minus className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className={cn(
"h-7 min-w-[50px] text-xs font-medium",
isWorkflowCanvas && "rounded-full px-3"
)}
onClick={() => setViewport({ zoom: 1, x: 0, y: 0 })}
title="重置缩放"
>
{Math.round(viewport.zoom * 100)}%
</Button>
<Button
variant="ghost"
size="icon"
className={cn("h-7 w-7", isWorkflowCanvas && "rounded-full")}
onClick={() => {
const newZoom = Math.min(4, viewport.zoom + 0.1);
setViewport({ zoom: newZoom });
}}
title="放大"
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
) : null}
</div>
);
}

File diff suppressed because it is too large Load Diff