fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组 (#773)

* fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组

进入画布时形象配置与 Live2D 首帧未就绪不再显示默认球,改为加载提示,失败才回退。

新导入模型一级目录默认未分类、二级渠道展示名默认跟随上游 ID;历史非空分组(含与模型 ID 同名)保留。

拉取模型弹窗默认只勾选已存在模型并移除逐项已存在字样;一级分组下拉点击即展开全部选项。

* style(web): 画布与模型导入 - 按 Prettier 规范格式化改动文件

修复 PR 中 Web checks 的格式化检查失败:Live2D 启动器 JSX、模型导入选择工具函数与 Live2D 加载测试按项目 Prettier 配置重新排版,不改变行为。

* test(web): Live2D 生命周期 - 修复 bun 1.3.9 下 text 导入与模块导入冲突

agent-live2d-lifecycle 曾用 with { type: text } 导入 live2d-avatar.tsx,bun 1.3.9 会按路径复用该缓存,导致新增的模块导入拿不到 Live2DAvatar 具名导出并报 SyntaxError。改为 readFileSync 读取源码文本,本地以 bun 1.3.9 复现并验证通过。
This commit is contained in:
Rou
2026-10-14 09:36:01 +08:00
committed by GitHub
parent db8a301474
commit 054dab28cc
22 changed files with 251 additions and 66 deletions

View File

@@ -34,10 +34,12 @@ import { markdownPlainText } from "@/lib/markdown-plain-text";
import type { ApprovalState } from "./canvas-cloud-agent-events";
// hidden 为 true 时入口保持挂载但不显示:Live2D 模型加载开销大,面板开合不能卸载它,
// 否则每次重新挂载都要重新下载模型并重建渲染上下文,期间只能显示默认形象。
// 否则每次重新挂载都要重新下载模型并重建渲染上下文。
export function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, hidden = false, onOpen }: { theme: CanvasTheme; statusColor: string; approvalPending: boolean; reducedMotion: boolean; hidden?: boolean; onOpen: () => void }) {
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const appearanceResolved = useAppearanceStore((state) => state.resolved);
const live = appearance.avatarType === "live2d" && Boolean(appearance.live2dResourceId && appearance.live2dEntry);
const [live2DError, setLive2DError] = useState("");
const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight }));
useEffect(() => {
const resize = () => setViewport({ width: window.innerWidth, height: window.innerHeight });
@@ -51,7 +53,7 @@ export function AgentLauncher({ theme, statusColor, approvalPending, reducedMoti
<motion.button
type="button"
aria-label={`打开${appearance.agentName}`}
title={`${approvalPending ? "Agent 等待你的审批" : "打开 Agent 助手"} · 拖动可调整位置,聚焦后可用方向键移动`}
title={live && live2DError ? live2DError : `${approvalPending ? "Agent 等待你的审批" : "打开 Agent 助手"} · 拖动可调整位置,聚焦后可用方向键移动`}
className={cn("canvas-agent-launcher fixed z-[calc(var(--z-toast)+1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/35", dragging && "is-dragging", live && "canvas-agent-launcher-live2d")}
style={{ ...position, width, height, color: theme.node.text, "--canvas-agent-launcher-shadow": theme.spatial.shadow, ...(hidden ? { display: "none" } : null) } as CSSProperties}
aria-hidden={hidden || undefined}
@@ -63,11 +65,29 @@ export function AgentLauncher({ theme, statusColor, approvalPending, reducedMoti
transition={{ duration: reducedMotion ? 0 : 0.18 }}
>
{live ? (
<Live2DAvatar url={live2DModelURL(appearance.live2dResourceId, appearance.live2dEntry)} width={width} height={height} reducedMotion={reducedMotion} fallback={<FluidOrb size={60} color="#7164f6" />} />
) : (
<Live2DAvatar
url={live2DModelURL(appearance.live2dResourceId, appearance.live2dEntry)}
width={width}
height={height}
reducedMotion={reducedMotion}
fallback={<FluidOrb size={60} color="#7164f6" />}
onReady={() => setLive2DError("")}
onError={setLive2DError}
/>
) : appearanceResolved ? (
<FluidOrb size={60} color="#7164f6" />
) : (
<span role="status" aria-busy="true">
<LoaderCircle className={reducedMotion ? "size-6" : "size-6 motion-safe:animate-spin"} aria-hidden="true" />
<span className="sr-only">正在加载助手形象</span>
</span>
)}
{appearance.launcherLabel ? <span className="canvas-agent-launcher-label">{appearance.launcherLabel}</span> : null}
{live && live2DError ? (
<span className="sr-only" role="status">
{live2DError}
</span>
) : null}
<span className={cn("canvas-agent-launcher-status", approvalPending && "is-pending")} style={{ "--canvas-agent-status-color": statusColor } as CSSProperties} />
{approvalPending ? <span className="canvas-agent-launcher-badge">待审批</span> : null}
</motion.button>

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { LoaderCircle } from "lucide-react";
import { installLive2DLoader } from "@/services/live2d-loader";
import { adaptLive2DRenderOrders } from "@/lib/canvas/live2d-core-adapter";
@@ -95,8 +96,9 @@ export function Live2DAvatar({
model.update(Math.min(now - elapsed, 100));
elapsed = now;
app.render();
} catch {
failRendering("Live2D 渲染失败,已回退默认形象");
} catch (error) {
const detail = error instanceof Error && error.message ? `:${error.message}` : "";
failRendering(`Live2D 渲染失败${detail},已回退默认形象`);
}
};
const sync = () => {
@@ -188,9 +190,16 @@ export function Live2DAvatar({
};
}, [url, width, height, reducedMotion]);
return (
<span style={{ display: "grid", placeItems: "center", width, height, pointerEvents: "none" }}>
<span aria-busy={!failed && !ready} style={{ display: "grid", placeItems: "center", width, height, pointerEvents: "none" }}>
<span ref={host} style={{ gridArea: "1 / 1", visibility: failed || !ready ? "hidden" : "visible", width, height }} />
{failed || !ready ? <span style={{ gridArea: "1 / 1" }}>{fallback}</span> : null}
{failed ? (
<span style={{ gridArea: "1 / 1" }}>{fallback}</span>
) : !ready ? (
<span role="status" style={{ gridArea: "1 / 1" }}>
<LoaderCircle className={reducedMotion ? "size-6" : "size-6 motion-safe:animate-spin"} aria-hidden="true" />
<span className="sr-only">正在加载 Live2D 模型</span>
</span>
) : null}
</span>
);
}