fix: stabilize v1.5.1 workspace build

This commit is contained in:
coso
2026-04-08 01:57:44 +08:00
parent 7431c19f31
commit a057d76e6a
8 changed files with 123 additions and 78 deletions
@@ -6,6 +6,7 @@ import {
EMPTY_STATE_BADGE_TONE_CLASSNAMES,
EMPTY_STATE_CARD_SURFACE_CLASSNAME,
EMPTY_STATE_ICON_TONE_CLASSNAMES,
type EmptyStateTone,
} from "./emptyStateSurfaceTokens";
const heroReveal = keyframes`
@@ -314,7 +315,7 @@ const CardEyebrow = styled.span`
export interface EmptyStateHeroBadge {
key: string;
label: string;
tone?: "slate" | "sky" | "emerald" | "amber";
tone?: EmptyStateTone;
}
export interface EmptyStateHeroCard {
@@ -326,7 +327,7 @@ export interface EmptyStateHeroCard {
icon: ReactNode;
imageSrc?: string;
imageAlt?: string;
tone?: "slate" | "sky" | "emerald" | "amber";
tone?: EmptyStateTone;
action?: ReactNode;
onMediaAction?: () => void;
mediaActionLabel?: string;
@@ -40,7 +40,10 @@ function createPreparedSend(
};
}
function createEnv() {
type SlashSkillPreflightTestEnv =
Parameters<typeof maybeHandleSlashSkillBeforeSend>[0]["env"];
function createEnv(): SlashSkillPreflightTestEnv {
return {
ensureSession: async () => "session-1",
sessionIdRef: { current: null },
@@ -53,7 +56,7 @@ function createEnv() {
playTypewriterSound: vi.fn(),
playToolcallSound: vi.fn(),
onWriteFile: vi.fn(),
} as never;
};
}
describe("agentStreamSlashSkillPreflight", () => {
@@ -40,10 +40,14 @@ interface ParsedRuntimeSceneCommand {
userInput: string;
}
type ServiceSceneLaunchRequestContext =
| Record<string, unknown>
| ServiceSkillClawLaunchContext;
interface ServiceSceneLaunchRequest {
skill: ServiceSkillItem;
sceneEntry: SkillCatalogSceneEntry;
requestContext: Record<string, unknown>;
requestContext: ServiceSceneLaunchRequestContext;
}
export class RuntimeSceneLaunchValidationError extends Error {
@@ -53,6 +57,20 @@ export class RuntimeSceneLaunchValidationError extends Error {
}
}
function isServiceSkillClawLaunchContext(
value: ServiceSceneLaunchRequestContext,
): value is ServiceSkillClawLaunchContext {
return (
value.kind === "site_adapter" &&
typeof value.skillId === "string" &&
typeof value.skillTitle === "string" &&
typeof value.adapterName === "string" &&
value.args !== null &&
typeof value.args === "object" &&
!Array.isArray(value.args)
);
}
function normalizeCommandToken(value?: string | null): string {
if (typeof value !== "string") {
return "";
@@ -252,7 +270,7 @@ function buildServiceSceneLaunchRequestContext(params: {
skill_title: skill.title,
skill_summary: skill.summary,
runner_type: skill.runnerType,
execution_kind: sceneEntry.executionKind ?? skill.executionKind,
execution_kind: sceneEntry.executionKind ?? skill.defaultExecutorBinding,
execution_location: skill.executionLocation,
project_id: projectId ?? undefined,
content_id: contentId ?? undefined,
@@ -327,12 +345,12 @@ async function resolveSiteSceneLaunchReadiness(
export function buildServiceSceneLaunchRequestMetadata(
existingMetadata: Record<string, unknown> | undefined,
requestContext: Record<string, unknown>,
requestContext: ServiceSceneLaunchRequestContext,
): Record<string, unknown> {
if (requestContext.kind === "site_adapter") {
if (isServiceSkillClawLaunchContext(requestContext)) {
const existingHarness = asRecord(existingMetadata?.harness);
const siteMetadata = buildServiceSkillClawLaunchRequestMetadata(
requestContext as ServiceSkillClawLaunchContext,
requestContext,
);
const siteHarness = asRecord(siteMetadata.harness);
@@ -109,6 +109,16 @@ interface RestoredImageTaskSnapshot extends LoadedImageTaskSnapshot {
taskFamily: string;
}
interface ImageTaskPreviewRuntimeContext {
sessionId?: string | null;
projectId?: string | null;
contentId?: string | null;
projectRootPath?: string | null;
messages?: Message[];
currentImageWorkbenchState?: SessionImageWorkbenchState;
canvasState: CanvasStateUnion | null;
}
function shouldPreferLoadedImageTaskSnapshot(
current: LoadedImageTaskSnapshot | null,
candidate: LoadedImageTaskSnapshot | null,
@@ -1592,12 +1602,13 @@ export function useWorkspaceImageTaskPreviewRuntime({
const restoreSeedMessagesRef = useRef<
((seedMessages?: Message[]) => void) | null
>(null);
const runtimeContextRef = useRef({
const runtimeContextRef = useRef<ImageTaskPreviewRuntimeContext>({
sessionId,
projectId,
contentId,
projectRootPath,
messages,
currentImageWorkbenchState,
canvasState,
});
@@ -1701,7 +1712,7 @@ export function useWorkspaceImageTaskPreviewRuntime({
artifactPath?: string;
}) => {
const existing = trackedTasks.get(params.taskId);
if (existing?.timerId !== null) {
if (existing && existing.timerId !== null) {
window.clearTimeout(existing.timerId);
}
trackedTasks.set(params.taskId, {
@@ -1306,6 +1306,10 @@ async function resolveVoiceSkillLaunchRequestContext(params: {
}
const runtime = resolveOemCloudRuntimeContext();
if (!runtime) {
toast.error("当前未连接云端运行时,请稍后再试");
return null;
}
return {
kind: "cloud_scene",
@@ -3,7 +3,7 @@ import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { emitVideoWorkbenchTaskAction } from "@/lib/videoWorkbenchEvents";
import type { Message } from "../types";
import type { Message, MessageVideoTaskPreview } from "../types";
import { useWorkspaceVideoTaskActionRuntime } from "./useWorkspaceVideoTaskActionRuntime";
const { mockCancelTask, mockCreateTask, mockGetTask, toast } = vi.hoisted(
@@ -34,41 +34,50 @@ type HookProps = Parameters<typeof useWorkspaceVideoTaskActionRuntime>[0];
const mountedRoots: Array<{ container: HTMLDivElement; root: Root }> = [];
function buildFailedVideoPreview(): MessageVideoTaskPreview {
return {
kind: "video_generate",
taskId: "task-video-old",
taskType: "video_generate",
prompt: "新品发布会短视频",
status: "failed",
durationSeconds: 15,
aspectRatio: "16:9",
resolution: "720p",
projectId: "project-video-1",
contentId: "content-video-1",
providerId: "doubao",
model: "seedance-1-5-pro-251215",
};
}
function buildFailedVideoMessage(): Message {
return {
id: "msg-video-failed-1",
role: "assistant",
content: "视频任务失败。",
timestamp: new Date(),
taskPreview: {
kind: "video_generate",
taskId: "task-video-old",
taskType: "video_generate",
prompt: "新品发布会短视频",
status: "failed",
durationSeconds: 15,
aspectRatio: "16:9",
resolution: "720p",
projectId: "project-video-1",
contentId: "content-video-1",
providerId: "doubao",
model: "seedance-1-5-pro-251215",
},
taskPreview: buildFailedVideoPreview(),
};
}
function buildRunningVideoPreview(): MessageVideoTaskPreview {
return {
...buildFailedVideoPreview(),
taskId: "task-video-running",
status: "running",
progress: 32,
statusMessage: "视频任务正在生成中,工作区会继续同步最新状态。",
};
}
function buildRunningVideoMessage(): Message {
return {
...buildFailedVideoMessage(),
id: "msg-video-running-1",
role: "assistant",
content: "视频任务进行中。",
taskPreview: {
...buildFailedVideoMessage().taskPreview!,
taskId: "task-video-running",
status: "running",
progress: 32,
statusMessage: "视频任务正在生成中,工作区会继续同步最新状态。",
},
timestamp: new Date(),
taskPreview: buildRunningVideoPreview(),
};
}
@@ -2,7 +2,7 @@ import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Message } from "../types";
import type { Message, MessageVideoTaskPreview } from "../types";
import { useWorkspaceVideoTaskPreviewRuntime } from "./useWorkspaceVideoTaskPreviewRuntime";
const { mockGetTask } = vi.hoisted(() => ({
@@ -19,24 +19,30 @@ type HookProps = Parameters<typeof useWorkspaceVideoTaskPreviewRuntime>[0];
const mountedRoots: Array<{ container: HTMLDivElement; root: Root }> = [];
function buildVideoMessage(): Message {
function buildRunningVideoPreview(): MessageVideoTaskPreview {
return {
kind: "video_generate",
taskId: "task-video-1",
taskType: "video_generate",
prompt: "新品发布会短视频",
status: "running",
durationSeconds: 15,
aspectRatio: "16:9",
resolution: "720p",
projectId: "project-video-1",
contentId: "content-video-1",
};
}
function buildVideoMessage(
taskPreview: MessageVideoTaskPreview = buildRunningVideoPreview(),
): Message {
return {
id: "msg-video-1",
role: "assistant",
content: "视频任务已提交",
timestamp: new Date(),
taskPreview: {
kind: "video_generate",
taskId: "task-video-1",
taskType: "video_generate",
prompt: "新品发布会短视频",
status: "running",
durationSeconds: 15,
aspectRatio: "16:9",
resolution: "720p",
projectId: "project-video-1",
contentId: "content-video-1",
},
taskPreview,
};
}
@@ -135,14 +141,11 @@ describe("useWorkspaceVideoTaskPreviewRuntime", () => {
const setChatMessages: HookProps["setChatMessages"] = vi.fn();
const harness = renderHook({
messages: [
{
...buildVideoMessage(),
taskPreview: {
...buildVideoMessage().taskPreview!,
status: "complete",
videoUrl: "https://example.com/already-synced.mp4",
},
},
buildVideoMessage({
...buildRunningVideoPreview(),
status: "complete",
videoUrl: "https://example.com/already-synced.mp4",
}),
],
setChatMessages,
});
@@ -159,14 +162,11 @@ describe("useWorkspaceVideoTaskPreviewRuntime", () => {
it("历史里的视频任务已完成但尚未带回 videoUrl 时,也应继续回填最终结果", async () => {
let messages: Message[] = [
{
...buildVideoMessage(),
taskPreview: {
...buildVideoMessage().taskPreview!,
status: "complete",
statusMessage: "视频已经生成完成,正在同步最终结果。",
},
},
buildVideoMessage({
...buildRunningVideoPreview(),
status: "complete",
statusMessage: "视频已经生成完成,正在同步最终结果。",
}),
];
const setChatMessages: HookProps["setChatMessages"] = (value) => {
messages = typeof value === "function" ? value(messages) : value;
+13 -14
View File
@@ -1,6 +1,7 @@
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { WorkspaceEnsureResult } from "@/lib/api/project";
const projectApiMocks = vi.hoisted(() => ({
createProject: vi.fn(),
@@ -137,23 +138,21 @@ describe("useProjects", () => {
it("不应等待默认项目目录健康检查完成才暴露项目列表", async () => {
const defaultProject = createProject();
let resolveEnsure:
| ((value: {
workspaceId: string;
rootPath: string;
existed: boolean;
created: boolean;
repaired: boolean;
}) => void)
| null = null;
const deferredEnsure: {
resolve: ((value: WorkspaceEnsureResult) => void) | null;
} = {
resolve: null,
};
const ensureReadyPromise = new Promise<WorkspaceEnsureResult>(
(resolve) => {
deferredEnsure.resolve = resolve;
},
);
projectApiMocks.listProjects.mockResolvedValueOnce([defaultProject]);
projectApiMocks.getDefaultProject.mockResolvedValueOnce(defaultProject);
projectApiMocks.ensureWorkspaceReady.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveEnsure = resolve;
}),
() => ensureReadyPromise,
);
const harness = mountHook();
@@ -168,7 +167,7 @@ describe("useProjects", () => {
expect(harness.getValue().projects).toHaveLength(1);
expect(harness.getValue().defaultProject?.id).toBe(defaultProject.id);
resolveEnsure?.({
deferredEnsure.resolve?.({
workspaceId: defaultProject.id,
rootPath: defaultProject.rootPath,
existed: true,