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:
22 files changed
+251
-66
No files matched your search
@@ -82,9 +82,9 @@ test("channel model fetch requires explicit selection before import", async () =
|
||||
expect(apiSource).toContain("http.post<{ models: string[] }>(`/admin/channels/${encodeURIComponent(channelId)}/models/fetch`)");
|
||||
expect(apiSource).toContain("http.post<{ models: string[]; added: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/import`, { models })");
|
||||
expect(component).toContain('title="选择要导入的模型"');
|
||||
expect(component).toContain("默认已全选");
|
||||
expect(component).toContain("默认勾选已存在的模型,新模型默认不勾选");
|
||||
expect(component).toContain("setFetchPreviewOpen(true)");
|
||||
expect(component).toContain("setSelectedFetchModels(result.models)");
|
||||
expect(component).toContain("setSelectedFetchModels(selectExistingImportModels(result.models,");
|
||||
expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型");
|
||||
expect(component).toContain("disabled={importing || !visibleFetchModels.length || allVisibleFetchModelsSelected}");
|
||||
expect(component).toContain("onClick={() => setSelectedFetchModels((current) => selectVisibleModels(current, visibleFetchModels))}");
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import panelSource from "../src/components/canvas/canvas-cloud-agent-panel.tsx" with { type: "text" };
|
||||
import partsSource from "../src/components/canvas/canvas-cloud-agent-panel-parts.tsx" with { type: "text" };
|
||||
|
||||
// 同一路径一旦被 text 加载器注册,bun 1.3.9 的模块缓存会让其它测试的具名导入失效,这里直接读源码文本。
|
||||
const avatarSource = readFileSync(new URL("../src/components/canvas/live2d-avatar.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("Live2D launcher remains mounted while the Agent panel is open", () => {
|
||||
expect(panelSource).toContain("<AgentLauncher");
|
||||
expect(panelSource).toContain("hidden={open}");
|
||||
@@ -10,3 +14,17 @@ test("Live2D launcher remains mounted while the Agent panel is open", () => {
|
||||
expect(partsSource).toContain("hidden = false");
|
||||
expect(partsSource).toContain('display: "none"');
|
||||
});
|
||||
|
||||
test("launcher waits for appearance configuration before displaying the default orb", () => {
|
||||
expect(partsSource).toContain("const appearanceResolved = useAppearanceStore((state) => state.resolved)");
|
||||
expect(partsSource).toContain(") : appearanceResolved ? (");
|
||||
expect(partsSource).toContain("正在加载助手形象");
|
||||
});
|
||||
|
||||
test("Live2D fallback is reserved for load or render failures, not initial loading", () => {
|
||||
expect(avatarSource).toContain("{failed ? (");
|
||||
expect(avatarSource).toContain(") : !ready ? (");
|
||||
expect(avatarSource).not.toContain("{failed || !ready ? <span");
|
||||
expect(avatarSource).toContain("setFailed(true)");
|
||||
expect(avatarSource).toContain("callbacks.current.onError?.");
|
||||
});
|
||||
@@ -18,6 +18,12 @@ const protocols = [definition("disabled-text", "text", false), definition("text"
|
||||
const protocolsWithAudio = [...protocols, definition("audio", "audio")];
|
||||
|
||||
describe("channel model editor drafts", () => {
|
||||
test("new drafts use 未分类 and imported drafts display the upstream ID without overwriting custom names", () => {
|
||||
expect(initialChannelModelValues(null, protocols).displayName).toBe("未分类");
|
||||
const imported = { modelKey: "product-id", providerModelKey: "upstream-id", displayName: "未分类", channelLabel: "", priceTiers: [] } as unknown as ChannelModel;
|
||||
expect(initialChannelModelValues(imported, protocols)).toMatchObject({ displayName: "未分类", channelLabel: "upstream-id" });
|
||||
expect(initialChannelModelValues({ ...imported, displayName: "product-id", channelLabel: "手动命名" }, protocols)).toMatchObject({ displayName: "product-id", channelLabel: "手动命名" });
|
||||
});
|
||||
test("Kemei Seedream selection initializes the image profile and preserves pricing", () => {
|
||||
const draft = initialChannelModelValues(null, protocols);
|
||||
draft.priceTiers[0].unitPrice = 19;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { deselectVisibleModels, filterImportModels, selectVisibleModels } from "../src/pages/admin/components/channel-model-import-selection";
|
||||
import { deselectVisibleModels, filterImportModels, selectExistingImportModels, selectVisibleModels } from "../src/pages/admin/components/channel-model-import-selection";
|
||||
|
||||
const MODELS = [
|
||||
"MiniMax-H3",
|
||||
@@ -50,3 +50,9 @@ test("取消选中只移除可见的,隐藏的勾选不动", () => {
|
||||
test("无关键字时取消选中等于清空,与旧行为一致", () => {
|
||||
expect(deselectVisibleModels(MODELS, filterImportModels(MODELS, ""))).toEqual([]);
|
||||
});
|
||||
|
||||
test("已有模型默认勾选,新模型默认不勾选,沿用上游标识匹配规则", () => {
|
||||
const available = ["models/gpt-a", "new-model", " GPT-B ", "gpt-c"];
|
||||
expect(selectExistingImportModels(available, ["gpt-a", "models/gpt-b", "gpt-c"])).toEqual(["models/gpt-a", " GPT-B ", "gpt-c"]);
|
||||
expect(selectExistingImportModels(available, [])).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { Live2DAvatar } from "../src/components/canvas/live2d-avatar";
|
||||
|
||||
function renderLoadingAvatar(reducedMotion = false) {
|
||||
return renderToStaticMarkup(<Live2DAvatar url="/api/public/appearance/live2d/model-id/model.model3.json" width={165} height={220} reducedMotion={reducedMotion} fallback={<span data-default-avatar>默认球</span>} />);
|
||||
}
|
||||
|
||||
test("Live2D first render shows loading rather than the failure fallback", () => {
|
||||
const markup = renderLoadingAvatar();
|
||||
expect(markup).not.toContain("data-default-avatar");
|
||||
expect(markup).not.toContain("默认球");
|
||||
expect(markup).toContain('role="status"');
|
||||
expect(markup).toContain("正在加载 Live2D 模型");
|
||||
expect(markup).toContain('aria-busy="true"');
|
||||
expect(markup).toContain("visibility:hidden");
|
||||
expect(markup).toContain("width:165px;height:220px");
|
||||
});
|
||||
|
||||
test("Live2D loading respects reduced motion without showing the default avatar", () => {
|
||||
expect(renderLoadingAvatar()).toContain("motion-safe:animate-spin");
|
||||
const markup = renderLoadingAvatar(true);
|
||||
expect(markup).not.toContain("animate-spin");
|
||||
expect(markup).not.toContain("data-default-avatar");
|
||||
expect(markup).toContain("正在加载 Live2D 模型");
|
||||
});
|
||||
@@ -35,7 +35,7 @@ test("same model display name groups all channels and preserves their prices", (
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(config.channels.map((channel) => channel.modelCosts![0].description)).toEqual(["", "优惠渠道-993的使用说明", "特惠渠道-730的使用说明"]);
|
||||
expect(groups.map((group) => [group.label, group.kind, group.models.map((item) => [item.label, item.models])])).toEqual([
|
||||
["Seedance 2.0", "product", [["系统模型", ["a::seedance-2.0"]], ["优惠渠道-993", ["b::seedance-2.0"]], ["特惠渠道-730", ["c::seedance-2.0"]]]],
|
||||
["Seedance 2.0", "product", [["seedance-2.0", ["a::seedance-2.0"]], ["优惠渠道-993", ["b::seedance-2.0"]], ["特惠渠道-730", ["c::seedance-2.0"]]]],
|
||||
]);
|
||||
expect(config.channels.map((channel) => priceTierSummaryLabel(priceTiersForCurrentSelection(channel.modelCosts![0].logicalPriceTiers!, "video", config)))).toEqual(["0.3 积分/秒", "0.3 积分/秒", "0.2 积分/秒"]);
|
||||
});
|
||||
@@ -155,3 +155,18 @@ test("managed logical models and personal channels never become system product r
|
||||
expect(groups[0].models).toHaveLength(1);
|
||||
expect(groupModelsForPicker(config, [])).toEqual([]);
|
||||
});
|
||||
|
||||
test("new models share 未分类 while explicit groups equal to model IDs remain intact", () => {
|
||||
const channels = systemChannelModelChannels([{ id: "upstream", name: "Provider", displayName: "Provider", models: [
|
||||
model("upstream-a", 300000, "a", "未分类"),
|
||||
model("upstream-b", 300000, "b", "未分类"),
|
||||
model("upstream-c", 300000, "c", "c"),
|
||||
] }]);
|
||||
const config = normalizeConfigSnapshot({ config: { ...defaultConfig, channels } }).config;
|
||||
const groups = groupModelsForPicker(config, selectableModelsByCapability(config, "video"));
|
||||
expect(groups.map((group) => group.label)).toEqual(["未分类", "c"]);
|
||||
expect(groups[0].models.map((item) => [item.label, item.models])).toEqual([
|
||||
["upstream-a", ["upstream::a"]], ["upstream-b", ["upstream::b"]],
|
||||
]);
|
||||
expect(groups[1].models[0].models).toEqual(["upstream::c"]);
|
||||
});
|
||||
Reference in new issue
Block a user