diff --git a/src/components/agent/chat/components/EmptyStateHero.tsx b/src/components/agent/chat/components/EmptyStateHero.tsx index 0897b0b60..8c638191f 100644 --- a/src/components/agent/chat/components/EmptyStateHero.tsx +++ b/src/components/agent/chat/components/EmptyStateHero.tsx @@ -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; diff --git a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts index 0bfe72666..9d66d6e11 100644 --- a/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts +++ b/src/components/agent/chat/hooks/agentStreamSlashSkillPreflight.test.ts @@ -40,7 +40,10 @@ function createPreparedSend( }; } -function createEnv() { +type SlashSkillPreflightTestEnv = + Parameters[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", () => { diff --git a/src/components/agent/chat/workspace/serviceSkillSceneLaunch.ts b/src/components/agent/chat/workspace/serviceSkillSceneLaunch.ts index 3a08cbecf..c744df8cd 100644 --- a/src/components/agent/chat/workspace/serviceSkillSceneLaunch.ts +++ b/src/components/agent/chat/workspace/serviceSkillSceneLaunch.ts @@ -40,10 +40,14 @@ interface ParsedRuntimeSceneCommand { userInput: string; } +type ServiceSceneLaunchRequestContext = + | Record + | ServiceSkillClawLaunchContext; + interface ServiceSceneLaunchRequest { skill: ServiceSkillItem; sceneEntry: SkillCatalogSceneEntry; - requestContext: Record; + 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 | undefined, - requestContext: Record, + requestContext: ServiceSceneLaunchRequestContext, ): Record { - 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); diff --git a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts index 5d9dfbc90..0888c7a89 100644 --- a/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts +++ b/src/components/agent/chat/workspace/useWorkspaceImageTaskPreviewRuntime.ts @@ -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({ 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, { diff --git a/src/components/agent/chat/workspace/useWorkspaceSendActions.ts b/src/components/agent/chat/workspace/useWorkspaceSendActions.ts index 8b10fe927..87a8757e5 100644 --- a/src/components/agent/chat/workspace/useWorkspaceSendActions.ts +++ b/src/components/agent/chat/workspace/useWorkspaceSendActions.ts @@ -1306,6 +1306,10 @@ async function resolveVoiceSkillLaunchRequestContext(params: { } const runtime = resolveOemCloudRuntimeContext(); + if (!runtime) { + toast.error("当前未连接云端运行时,请稍后再试"); + return null; + } return { kind: "cloud_scene", diff --git a/src/components/agent/chat/workspace/useWorkspaceVideoTaskActionRuntime.test.tsx b/src/components/agent/chat/workspace/useWorkspaceVideoTaskActionRuntime.test.tsx index 5d4f7d89a..daf2e5b35 100644 --- a/src/components/agent/chat/workspace/useWorkspaceVideoTaskActionRuntime.test.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceVideoTaskActionRuntime.test.tsx @@ -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[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(), }; } diff --git a/src/components/agent/chat/workspace/useWorkspaceVideoTaskPreviewRuntime.test.tsx b/src/components/agent/chat/workspace/useWorkspaceVideoTaskPreviewRuntime.test.tsx index d2efb2fcd..821484eb4 100644 --- a/src/components/agent/chat/workspace/useWorkspaceVideoTaskPreviewRuntime.test.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceVideoTaskPreviewRuntime.test.tsx @@ -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[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; diff --git a/src/hooks/useProjects.test.tsx b/src/hooks/useProjects.test.tsx index d5b2a62ee..f2f8e7c96 100644 --- a/src/hooks/useProjects.test.tsx +++ b/src/hooks/useProjects.test.tsx @@ -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( + (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,