From 7765a3b029cbfe43c975a18fd7a1889cf7034f56 Mon Sep 17 00:00:00 2001 From: coso Date: Sun, 22 Mar 2026 00:34:16 +0800 Subject: [PATCH] fix: unblock v0.93.0 release build --- .../agent/chat/AgentChatHomeShell.test.tsx | 10 ++- .../agent/chat/AgentChatWorkspace.tsx | 4 +- .../hooks/useRuntimeTeamFormation.test.tsx | 30 ++++--- .../hooks/useSelectedTeamPreference.test.tsx | 4 +- .../useThemeWorkbenchEntryPrompt.test.tsx | 82 ++++++++++++------- .../hooks/useThemeWorkbenchEntryPrompt.ts | 1 - src/components/agent/chat/index.test.tsx | 56 ++++++------- .../chat/utils/internalImagePlaceholder.ts | 2 +- .../agent/chat/utils/teamAutoGeneration.ts | 19 ++++- 9 files changed, 122 insertions(+), 86 deletions(-) diff --git a/src/components/agent/chat/AgentChatHomeShell.test.tsx b/src/components/agent/chat/AgentChatHomeShell.test.tsx index 97d942fdd..1ee67760f 100644 --- a/src/components/agent/chat/AgentChatHomeShell.test.tsx +++ b/src/components/agent/chat/AgentChatHomeShell.test.tsx @@ -2,6 +2,8 @@ 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 { ConfiguredProvider } from "@/hooks/useConfiguredProviders"; +import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry"; import { AgentChatHomeShell } from "./AgentChatHomeShell"; import { SettingsTabs } from "@/types/settings"; @@ -67,8 +69,12 @@ const { mockSetProviderType: vi.fn(), mockSetModel: vi.fn(), mockSetExecutionStrategy: vi.fn(), - mockLoadConfiguredProviders: vi.fn(async () => []), - mockLoadProviderModels: vi.fn(async () => []), + mockLoadConfiguredProviders: vi.fn( + async (): Promise => [], + ), + mockLoadProviderModels: vi.fn( + async (): Promise => [], + ), mockFilterModelsByTheme: vi.fn( (_theme: string | undefined, models: unknown[]) => ({ models, diff --git a/src/components/agent/chat/AgentChatWorkspace.tsx b/src/components/agent/chat/AgentChatWorkspace.tsx index e9bf79b0c..07e1a6a79 100644 --- a/src/components/agent/chat/AgentChatWorkspace.tsx +++ b/src/components/agent/chat/AgentChatWorkspace.tsx @@ -5398,8 +5398,8 @@ export function AgentChatWorkspace({ dismissThemeWorkbenchEntryPrompt, } = useThemeWorkbenchEntryPrompt({ activeTheme, - contentId, - sessionId, + contentId: contentId ?? undefined, + sessionId: sessionId ?? undefined, isThemeWorkbench, shouldUseCompactThemeWorkbench, messagesCount: messages.length, diff --git a/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx b/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx index a12f1c0ec..1f4013b7e 100644 --- a/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx +++ b/src/components/agent/chat/hooks/useRuntimeTeamFormation.test.tsx @@ -40,19 +40,23 @@ function renderHook(props?: Partial) { selectedTeam: createSelectedTeam(), subagentEnabled: true, hasRealTeamGraph: false, - generateRuntimeTeam: vi.fn(async () => ({ - id: "ephemeral-1", - source: "ephemeral", - label: "临时 Team", - description: "自动生成", - roles: [ - { - id: "member-1", - label: "执行者", - summary: "执行当前任务", - }, - ], - })), + generateRuntimeTeam: vi.fn( + async ( + _options: Parameters>[0], + ): Promise => ({ + id: "ephemeral-1", + source: "ephemeral", + label: "临时 Team", + description: "自动生成", + roles: [ + { + id: "member-1", + label: "执行者", + summary: "执行当前任务", + }, + ], + }), + ), createRequestId: () => "request-1", now: () => 1_710_000_000_000, }; diff --git a/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx b/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx index a013ccd1a..02f3fe33f 100644 --- a/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx +++ b/src/components/agent/chat/hooks/useSelectedTeamPreference.test.tsx @@ -140,7 +140,7 @@ describe("useSelectedTeamPreference", () => { agentTeam: { selectedTeam: { id: engineeringTeam.id, - source: engineeringTeam.source, + source: "builtin", }, }, }; @@ -228,7 +228,7 @@ describe("useSelectedTeamPreference", () => { agentTeam: { selectedTeam: { id: engineeringTeam.id, - source: engineeringTeam.source, + source: "builtin", }, }, }, diff --git a/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.test.tsx b/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.test.tsx index fad96bbe2..1d8578c68 100644 --- a/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.test.tsx +++ b/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.test.tsx @@ -1,6 +1,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { WorkflowState as ContentWorkflowState } from "@/lib/api/content-workflow"; +import type { ThemeWorkbenchRunState as BackendThemeWorkbenchRunState } from "@/lib/api/executionRun"; import { useThemeWorkbenchEntryPrompt } from "./useThemeWorkbenchEntryPrompt"; interface HookHarness { @@ -29,8 +31,13 @@ function mountHook(initialProps?: Partial): HookHarness { const root = createRoot(container); const onHydrateInitialPrompt = vi.fn(); - const loadWorkflow = vi.fn(async () => null); - const loadRunState = vi.fn(async () => null); + const loadWorkflow = vi.fn( + async (_contentId: string): Promise => null, + ); + const loadRunState = vi.fn( + async (_sessionId: string): Promise => + null, + ); let hookValue: ReturnType | null = null; let currentProps: HookProps = { @@ -135,32 +142,51 @@ describe("useThemeWorkbenchEntryPrompt", () => { document.body.appendChild(container); const root = createRoot(container); const onHydrateInitialPrompt = vi.fn(); - const loadWorkflow = vi.fn(async () => ({ - id: "wf-1", - content_id: "content-1", - theme: "social-media", - mode: "guided", - current_step_index: 1, - created_at: Date.now(), - updated_at: Date.now(), - steps: [ - { - id: "step-1", - title: "撰写主稿", - status: "completed", - }, - { - id: "step-2", - title: "润色结尾", - status: "pending", - }, - ], - })); - const loadRunState = vi.fn(async () => null); - let hookValue: ReturnType | null = null; + const loadWorkflow = vi.fn( + async (_contentId: string): Promise => ({ + id: "wf-1", + content_id: "content-1", + theme: "social-media", + mode: "guided", + current_step_index: 1, + created_at: Date.now(), + updated_at: Date.now(), + steps: [ + { + id: "step-1", + type: "write", + title: "撰写主稿", + behavior: { + skippable: false, + redoable: true, + auto_advance: false, + }, + status: "completed", + }, + { + id: "step-2", + type: "polish", + title: "润色结尾", + behavior: { + skippable: false, + redoable: true, + auto_advance: false, + }, + status: "pending", + }, + ], + }), + ); + const loadRunState = vi.fn( + async (_sessionId: string): Promise => + null, + ); + const hookValueRef: { + current: ReturnType | null; + } = { current: null }; function TestComponent() { - hookValue = useThemeWorkbenchEntryPrompt({ + hookValueRef.current = useThemeWorkbenchEntryPrompt({ activeTheme: "social-media", contentId: "content-1", sessionId: "session-1", @@ -185,11 +211,11 @@ describe("useThemeWorkbenchEntryPrompt", () => { try { await flushEffects(); expect(loadWorkflow).toHaveBeenCalledWith("content-1"); - expect(hookValue?.themeWorkbenchEntryPrompt).toMatchObject({ + expect(hookValueRef.current?.themeWorkbenchEntryPrompt).toMatchObject({ kind: "resume", title: "发现上次未完成任务", }); - expect(hookValue?.themeWorkbenchEntryCheckPending).toBe(false); + expect(hookValueRef.current?.themeWorkbenchEntryCheckPending).toBe(false); } finally { act(() => { root.unmount(); diff --git a/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.ts b/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.ts index 4bc578417..6fd71710a 100644 --- a/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.ts +++ b/src/components/agent/chat/hooks/useThemeWorkbenchEntryPrompt.ts @@ -54,7 +54,6 @@ function resolveThemeWorkbenchGateLabel( return "写作推进"; case "publish_confirm": return "发布确认"; - case "idle": case null: case undefined: default: diff --git a/src/components/agent/chat/index.test.tsx b/src/components/agent/chat/index.test.tsx index 4ba3f4a9e..e2554c4cf 100644 --- a/src/components/agent/chat/index.test.tsx +++ b/src/components/agent/chat/index.test.tsx @@ -640,6 +640,21 @@ interface MountedHarness { rerender: (props?: Partial>) => void; } +type MockInputbarSendProps = { + onToolStatesChange?: ( + next: + | Record + | ((prev: Record) => Record), + ) => void; + onSend?: ( + images?: unknown[], + webSearch?: boolean, + thinking?: boolean, + textOverride?: string, + executionStrategy?: "react" | "code_orchestrated" | "auto", + ) => void | Promise | Promise | boolean; +}; + const mountedRoots: MountedHarness[] = []; const observedWorkspaceIds: string[] = []; let sharedSwitchTopicMock: ReturnType; @@ -1582,13 +1597,7 @@ describe("AgentChatPage 通用工作台", () => { await flushEffects(10); let latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as - | { - onToolStatesChange?: ( - next: - | Record - | ((prev: Record) => Record), - ) => void; - } + | MockInputbarSendProps | undefined; act(() => { @@ -1600,15 +1609,7 @@ describe("AgentChatPage 通用工作台", () => { await flushEffects(8); latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as - | { - onSend?: ( - images?: unknown[], - webSearch?: boolean, - thinking?: boolean, - textOverride?: string, - executionStrategy?: "react" | "code_orchestrated" | "auto", - ) => Promise; - } + | MockInputbarSendProps | undefined; await act(async () => { @@ -1720,13 +1721,7 @@ describe("AgentChatPage 通用工作台", () => { await flushEffects(10); let latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as - | { - onToolStatesChange?: ( - next: - | Record - | ((prev: Record) => Record), - ) => void; - } + | MockInputbarSendProps | undefined; act(() => { @@ -1738,15 +1733,7 @@ describe("AgentChatPage 通用工作台", () => { await flushEffects(8); latestInputbarProps = mockInputbar.mock.calls.at(-1)?.[0] as - | { - onSend?: ( - images?: unknown[], - webSearch?: boolean, - thinking?: boolean, - textOverride?: string, - executionStrategy?: "react" | "code_orchestrated" | "auto", - ) => Promise; - } + | MockInputbarSendProps | undefined; await act(async () => { @@ -3156,9 +3143,12 @@ describe("AgentChatPage 自动引导", () => { const mounted = mountedRoots.at(-1); expect(mounted).toBeTruthy(); + if (!mounted) { + throw new Error("未找到挂载页面"); + } act(() => { - mounted?.root.render( + mounted.root.render( { + const sanitizedParts = parts.flatMap((part) => { if (part.type !== "text") { return [part]; } diff --git a/src/components/agent/chat/utils/teamAutoGeneration.ts b/src/components/agent/chat/utils/teamAutoGeneration.ts index 3f7a122ad..422f14997 100644 --- a/src/components/agent/chat/utils/teamAutoGeneration.ts +++ b/src/components/agent/chat/utils/teamAutoGeneration.ts @@ -13,6 +13,7 @@ import { createTeamDefinitionId, normalizeTeamDefinition, type TeamDefinition, + type TeamRoleDefinition, } from "./teamDefinitions"; interface GenerateTeamWithModelOptions { @@ -136,13 +137,23 @@ function parseGeneratedTeam( const json = extractJsonObject(raw); const parsed = JSON.parse(json) as GeneratedTeamPayload; const payload = parsed.team && typeof parsed.team === "object" ? parsed.team : parsed; + const normalizedRoles: TeamRoleDefinition[] | undefined = payload.roles?.map( + (role) => ({ + id: role.id?.trim() || "", + label: role.label?.trim() || "", + summary: role.summary?.trim() || "", + profileId: role.profileId?.trim() || undefined, + roleKey: role.roleKey?.trim() || undefined, + skillIds: role.skillIds?.map((skillId) => skillId.trim()).filter(Boolean) || [], + }), + ); const normalized = normalizeTeamDefinition({ id: createTeamDefinitionId("ephemeral-team"), source: "ephemeral", label: payload.label, description: payload.description, theme: activeTheme?.trim() || undefined, - roles: payload.roles, + roles: normalizedRoles, }); if (!normalized) { @@ -188,7 +199,7 @@ export async function generateEphemeralTeamWithModel( resolvedExecutionStrategy, ); const eventName = `agent_team_draft:${sessionId}:${Date.now()}`; - let unlisten: (() => void) | null = null; + const unlistenRef: { current: (() => void) | null } = { current: null }; try { const completion = new Promise((resolve, reject) => { @@ -207,7 +218,7 @@ export async function generateEphemeralTeamWithModel( void (async () => { try { - unlisten = await safeListen(eventName, async (event) => { + unlistenRef.current = await safeListen(eventName, async (event) => { const parsed = parseStreamEvent(event.payload); if (!parsed) { return; @@ -292,7 +303,7 @@ export async function generateEphemeralTeamWithModel( return await completion; } finally { try { - unlisten?.(); + unlistenRef.current?.(); } catch { // ignore cleanup failure }