From b8e6e9f3ef7b168763b6f87b796d29a9779ceff7 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 6 Jul 2026 18:51:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=94=BB=E5=B8=83=20Agent=20?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR 控制画布 Agent 入口显示。 默认 .env 关闭、本地 .env.local 开启,并补充示例和类型声明。 禁用时隐藏 dock 按钮和收起态 Agent 面板,开启时保留右侧任务列表互斥逻辑。 修正右侧 Agent 和任务列表切换不影响左侧资源栏。 补充画布 Agent 开关、面板独立性和 dock 隐藏测试。 --- .env | 2 + .env.example | 4 + .env.local | 2 + .../ImageCanvasEditorShellView.test.tsx | 1 + .../ImageCanvasEditorView.test.tsx | 175 ++++++++++++++++-- .../image-editor/ImageCanvasEditorView.tsx | 58 +++--- .../ImageCanvasPanelDockView.test.tsx | 7 + .../image-editor/ImageCanvasPanelDockView.tsx | 18 +- .../ImageCanvasSidebarView.test.tsx | 1 + .../image-editor/ImageCanvasStageView.tsx | 21 ++- .../useEditorAgentConversation.ts | 3 +- src/vite-env.d.ts | 1 + 12 files changed, 242 insertions(+), 51 deletions(-) diff --git a/.env b/.env index e1ed925f9..72324e587 100644 --- a/.env +++ b/.env @@ -1,5 +1,7 @@ # 微信小程序 web-view 登录配置。 # 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。 +VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false + WECHAT_MINI_PROGRAM_APP_ID="" WECHAT_MINI_PROGRAM_APP_SECRET="" WECHAT_JS_CODE_SESSION_ENDPOINT="" diff --git a/.env.example b/.env.example index df656b892..32a49b796 100644 --- a/.env.example +++ b/.env.example @@ -199,6 +199,10 @@ VITE_LLM_DEBUG_LOG="false" # Set to "true" to expose local diagnostic panels, or "false" to hide them. VITE_DEBUG_MODE="" +# Optional: show the image editor right-side Agent entry. +# Keep this off by default outside local development. +VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false" + # Optional: official VikingDB credentials for regenerating build-tag similarities # with the Python embedding script. The script auto-loads `.env.local` and uses # the fixed `bge-large-zh` embedding model. diff --git a/.env.local b/.env.local index 311781f60..cbc3ab9a0 100644 --- a/.env.local +++ b/.env.local @@ -42,6 +42,8 @@ LLM_DEBUG_LOG="true" # 注意:不要在客户端启用调试日志,避免敏感数据泄露 # VITE_LLM_DEBUG_LOG="false" +VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=true + ALIYUN_OSS_BUCKET="xushi-dev" ALIYUN_OSS_REGION="oss-cn-beijing" ALIYUN_OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com" diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index 818db33cb..dc5fa045a 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -139,6 +139,7 @@ function createStageProps(): ImageCanvasStageViewProps { editorAgentAssets: [], taskListRefreshKey: 0, isTaskSidebarOpen: false, + isAgentConversationEnabled: false, isAgentConversationOpen: false, generateDialog: null, cropExpandPanel: null, diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index aca59ee66..49f1b47a7 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -12,6 +12,7 @@ import userEvent from '@testing-library/user-event'; import JSZip from 'jszip'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { EditorAgentConversationClient } from './useEditorAgentConversation'; import { ApiClientError, AuthUiContext, @@ -25,11 +26,73 @@ import { setupImageCanvasEditorViewTestLifecycle, } from './ImageCanvasEditorView.test-utils'; -const listEditorAgentConversationsMock = vi.hoisted(() => vi.fn()); -const createEditorAgentConversationMock = vi.hoisted(() => vi.fn()); -const getEditorAgentConversationMock = vi.hoisted(() => vi.fn()); -const deleteEditorAgentConversationMock = vi.hoisted(() => vi.fn()); -const streamEditorAgentMessageMock = vi.hoisted(() => vi.fn()); +type EditorAgentListConversations = + EditorAgentConversationClient['listConversations']; +type EditorAgentCreateConversation = + EditorAgentConversationClient['createConversation']; +type EditorAgentGetConversation = + EditorAgentConversationClient['getConversation']; +type EditorAgentDeleteConversation = + EditorAgentConversationClient['deleteConversation']; +type EditorAgentStreamMessage = EditorAgentConversationClient['streamMessage']; +type EditorAgentConversationSummary = Awaited< + ReturnType +>[number]; +type EditorAgentConversationDetail = Awaited< + ReturnType +>; +const EDITOR_AGENT_SIDEBAR_ENV = 'VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR'; + +const listEditorAgentConversationsMock = vi.hoisted(() => + vi.fn< + Parameters, + ReturnType + >(async () => []), +); +const createEditorAgentConversationMock = vi.hoisted(() => + vi.fn< + Parameters, + ReturnType + >(async () => ({ + conversationId: 'editor-agent-conv-created', + projectId: 'editor-project-default', + title: '画布 Agent', + createdAt: '2026-07-03T00:00:00.000Z', + updatedAt: '2026-07-03T00:00:00.000Z', + messages: [], + })), +); +const getEditorAgentConversationMock = vi.hoisted(() => + vi.fn< + Parameters, + ReturnType + >(async () => ({ + conversationId: 'editor-agent-conv-test', + projectId: 'editor-project-default', + title: '画布 Agent', + createdAt: '2026-07-03T00:00:00.000Z', + updatedAt: '2026-07-03T00:00:00.000Z', + messages: [], + })), +); +const deleteEditorAgentConversationMock = vi.hoisted(() => + vi.fn< + Parameters, + ReturnType + >(async () => ({ + conversationId: 'editor-agent-conv-test', + projectId: 'editor-project-default', + title: '画布 Agent', + createdAt: '2026-07-03T00:00:00.000Z', + updatedAt: '2026-07-03T00:00:00.000Z', + })), +); +const streamEditorAgentMessageMock = vi.hoisted(() => + vi.fn< + Parameters, + ReturnType + >(async () => undefined), +); const generateEditorImageMock = vi.hoisted(() => vi.fn()); const generateEditorIconSpritesheetMock = vi.hoisted(() => vi.fn()); const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn()); @@ -85,7 +148,9 @@ vi.mock('../../services/image-editor/editorAgentClient', () => ({ streamEditorAgentMessage: streamEditorAgentMessageMock, })); -function createEditorAgentConversationSummary(overrides = {}) { +function createEditorAgentConversationSummary( + overrides: Partial = {}, +): EditorAgentConversationSummary { return { conversationId: 'editor-agent-conv-test', projectId: 'editor-project-default', @@ -96,7 +161,9 @@ function createEditorAgentConversationSummary(overrides = {}) { }; } -function createEditorAgentDetailWithGeneration(resourceId: string) { +function createEditorAgentDetailWithGeneration( + resourceId: string, +): EditorAgentConversationDetail { const summary = createEditorAgentConversationSummary(); return { ...summary, @@ -146,6 +213,18 @@ function openMinimap() { return screen.getByRole('button', { name: '画布小地图' }); } +async function ensureAgentConversationOpen() { + const agentButton = screen.getByRole('button', { name: '画布 Agent' }); + if (agentButton.getAttribute('aria-pressed') !== 'true') { + fireEvent.click(agentButton); + } + return screen.findByLabelText('发送给画布 Agent'); +} + +function enableEditorAgentSidebarForTest() { + vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'true'); +} + describe('ImageCanvasEditorView', () => { setupImageCanvasEditorViewTestLifecycle({ generateEditorImageMock, @@ -167,6 +246,7 @@ describe('ImageCanvasEditorView', () => { }); beforeEach(() => { + vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'false'); listEditorAgentConversationsMock.mockResolvedValue([]); createEditorAgentConversationMock.mockResolvedValue({ ...createEditorAgentConversationSummary({ @@ -197,9 +277,11 @@ describe('ImageCanvasEditorView', () => { deleteEditorAgentConversationMock.mockReset(); streamEditorAgentMessageMock.mockReset(); getPlatformProfileDashboardMock.mockReset(); + vi.unstubAllEnvs(); }); it('loads the project from projectid query before falling back to recent project', async () => { + listEditorAgentConversationsMock.mockResolvedValue([]); loadEditorProjectMock.mockResolvedValueOnce({ projectId: 'editor-project-query', title: '查询项目', @@ -481,6 +563,7 @@ describe('ImageCanvasEditorView', () => { }); it('does not inject built-in mock assets when the persisted library is empty', async () => { + listEditorAgentConversationsMock.mockResolvedValue([]); loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({ projectId: 'editor-project-empty', title: '空画布', @@ -939,10 +1022,10 @@ describe('ImageCanvasEditorView', () => { }); it('moves focus from the Agent input back to the canvas layer before handling shortcuts', async () => { + enableEditorAgentSidebarForTest(); render(); - fireEvent.click(screen.getByRole('button', { name: '画布 Agent' })); - const agentInput = await screen.findByLabelText('发送给画布 Agent'); + const agentInput = await ensureAgentConversationOpen(); agentInput.focus(); expect(document.activeElement).toBe(agentInput); @@ -1341,6 +1424,74 @@ describe('ImageCanvasEditorView', () => { expect(screen.getByRole('button', { name: '添加拼图素材' })).toBeTruthy(); }); + it('hides the right Agent conversation entry by default', () => { + render(); + + expect(screen.queryByRole('button', { name: '画布 Agent' })).toBeNull(); + expect( + screen.queryByRole('button', { name: '打开画布 Agent' }), + ).toBeNull(); + expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull(); + expect(listEditorAgentConversationsMock).not.toHaveBeenCalled(); + }); + + it('opens the right Agent conversation panel by default when enabled', async () => { + enableEditorAgentSidebarForTest(); + render(); + + expect(await screen.findByLabelText('发送给画布 Agent')).toBeTruthy(); + expect( + screen.queryByRole('complementary', { name: '画布任务列表' }), + ).toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '画布 Agent' })); + + expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull(); + }); + + it('keeps the left resource sidebar independent from right-side task and Agent panels', async () => { + enableEditorAgentSidebarForTest(); + render(); + + const assetSidebar = openAssetSidebar(); + expect(within(assetSidebar).getByText('素材')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '任务列表' })); + expect( + screen.getByRole('complementary', { name: '画布任务列表' }), + ).toBeTruthy(); + expect( + screen.getByRole('complementary', { name: '图片资源栏' }), + ).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '打开图层' })); + const layerSidebar = screen.getByRole('complementary', { + name: '图片资源栏', + }); + expect(within(layerSidebar).getByText('图层')).toBeTruthy(); + expect( + screen.getByRole('complementary', { name: '画布任务列表' }), + ).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '画布 Agent' })); + expect( + screen.getByRole('complementary', { name: '图片资源栏' }), + ).toBeTruthy(); + expect( + screen.queryByRole('complementary', { name: '画布任务列表' }), + ).toBeNull(); + expect(await screen.findByLabelText('发送给画布 Agent')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '任务列表' })); + expect( + screen.getByRole('complementary', { name: '图片资源栏' }), + ).toBeTruthy(); + expect( + screen.getByRole('complementary', { name: '画布任务列表' }), + ).toBeTruthy(); + expect(screen.queryByLabelText('发送给画布 Agent')).toBeNull(); + }); + it('adds assets from the sidebar and supports zoom buttons', () => { render(); @@ -1557,6 +1708,7 @@ describe('ImageCanvasEditorView', () => { }); it('renders Agent generation thumbnails as passive previews', async () => { + enableEditorAgentSidebarForTest(); const detail = createEditorAgentDetailWithGeneration('resource-puzzle'); listEditorAgentConversationsMock.mockResolvedValueOnce([ createEditorAgentConversationSummary(), @@ -1565,7 +1717,7 @@ describe('ImageCanvasEditorView', () => { render(); - fireEvent.click(screen.getByRole('button', { name: '画布 Agent' })); + await ensureAgentConversationOpen(); const messageLog = await screen.findByRole('log', { name: '画布 Agent 消息流', }); @@ -1578,6 +1730,7 @@ describe('ImageCanvasEditorView', () => { }); it('refreshes canvas and asset library when Agent generation finishes', async () => { + enableEditorAgentSidebarForTest(); let showGeneratedAsset = false; loadEditorAssetLibraryMock.mockImplementation(async () => ({ folders: [ @@ -1692,7 +1845,7 @@ describe('ImageCanvasEditorView', () => { render(); - fireEvent.click(screen.getByRole('button', { name: '画布 Agent' })); + await ensureAgentConversationOpen(); await waitFor(() => { expect(getEditorAgentConversationMock).toHaveBeenCalledWith( 'editor-agent-conv-test', diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index cce084c0f..687740c30 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -117,6 +117,12 @@ type ImageCanvasEditorViewProps = { onProjectAccessLost?: () => void; }; +function resolveEditorAgentSidebarEnabled( + rawValue = import.meta.env.VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR, +): boolean { + return rawValue === 'true' || rawValue === '1'; +} + function isCanvasStartupTool(value: string | null): value is CanvasStartupTool { return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool); } @@ -1161,44 +1167,49 @@ export function ImageCanvasEditorView({ applyProjectSnapshot: applyGeneratedProjectSnapshot, onWalletBalanceMayHaveChanged: refreshEditorWalletBalance, }); - const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(false); + const isAgentConversationEnabled = resolveEditorAgentSidebarEnabled(); + const [isAgentConversationOpen, setIsAgentConversationOpen] = useState( + () => isAgentConversationEnabled, + ); + const effectiveIsAgentConversationOpen = + isAgentConversationEnabled && isAgentConversationOpen; const toggleAgentConversation = useCallback(() => { + if (!isAgentConversationEnabled) { + return; + } const nextOpen = !isAgentConversationOpen; - if (nextOpen) { - setActiveSidebarPanel(null); - if (generationSurface.isTaskSidebarOpen) { - generationSurface.toggleTaskSidebar(); - } + if (nextOpen && generationSurface.isTaskSidebarOpen) { + generationSurface.toggleTaskSidebar(); } setIsAgentConversationOpen(nextOpen); - }, [generationSurface, isAgentConversationOpen, setActiveSidebarPanel]); + }, [generationSurface, isAgentConversationEnabled, isAgentConversationOpen]); const toggleTaskSidebar = useCallback(() => { - if (!generationSurface.isTaskSidebarOpen) { - setActiveSidebarPanel(null); + if ( + !generationSurface.isTaskSidebarOpen && + effectiveIsAgentConversationOpen + ) { setIsAgentConversationOpen(false); } generationSurface.toggleTaskSidebar(); - }, [generationSurface, setActiveSidebarPanel]); + }, [effectiveIsAgentConversationOpen, generationSurface]); const toggleCanvasSidebarPanel = useCallback( (panel: SidebarPanel) => { - setIsAgentConversationOpen(false); - if (generationSurface.isTaskSidebarOpen) { - generationSurface.toggleTaskSidebar(); - } toggleSidebarPanel(panel); }, - [generationSurface, toggleSidebarPanel], + [toggleSidebarPanel], ); useEffect(() => { - if (generationSurface.isTaskSidebarOpen && isAgentConversationOpen) { + if ( + (!isAgentConversationEnabled || generationSurface.isTaskSidebarOpen) && + isAgentConversationOpen + ) { setIsAgentConversationOpen(false); } - }, [generationSurface.isTaskSidebarOpen, isAgentConversationOpen]); - useEffect(() => { - if (activeSidebarPanel && isAgentConversationOpen) { - setIsAgentConversationOpen(false); - } - }, [activeSidebarPanel, isAgentConversationOpen]); + }, [ + generationSurface.isTaskSidebarOpen, + isAgentConversationEnabled, + isAgentConversationOpen, + ]); const { setQuickEditPanel, setCropExpandPanel, @@ -1923,7 +1934,8 @@ export function ImageCanvasEditorView({ isToolbarGuideVisible, taskListRefreshKey: generationSurface.taskListRefreshKey, isTaskSidebarOpen: generationSurface.isTaskSidebarOpen, - isAgentConversationOpen, + isAgentConversationEnabled, + isAgentConversationOpen: effectiveIsAgentConversationOpen, generateDialog, cropExpandPanel: generationSurface.cropExpandPanel, cropExpandSourceLayer: generationSurface.cropExpandSourceLayer, diff --git a/src/components/image-editor/ImageCanvasPanelDockView.test.tsx b/src/components/image-editor/ImageCanvasPanelDockView.test.tsx index 5810d71a1..d033bf634 100644 --- a/src/components/image-editor/ImageCanvasPanelDockView.test.tsx +++ b/src/components/image-editor/ImageCanvasPanelDockView.test.tsx @@ -18,6 +18,7 @@ function renderPanelDock( isZoomMenuOpen: false, isBackgroundSettingsOpen: false, activeSidebarPanel: null, + isAgentConversationEnabled: true, isAgentConversationOpen: false, isMinimapOpen: false, minimapModel: null, @@ -91,6 +92,12 @@ describe('ImageCanvasPanelDockView', () => { expect(props.onToggleMinimap).toHaveBeenCalledTimes(1); }); + it('hides the Agent entry when the Agent sidebar is disabled', () => { + renderPanelDock({ isAgentConversationEnabled: false }); + + expect(screen.queryByRole('button', { name: '画布 Agent' })).toBeNull(); + }); + it('renders zoom and background settings with callback wiring', () => { const props = renderPanelDock({ viewport: { x: 0, y: 0, scale: 0.5 }, diff --git a/src/components/image-editor/ImageCanvasPanelDockView.tsx b/src/components/image-editor/ImageCanvasPanelDockView.tsx index 9c7c041d7..38143ee91 100644 --- a/src/components/image-editor/ImageCanvasPanelDockView.tsx +++ b/src/components/image-editor/ImageCanvasPanelDockView.tsx @@ -35,6 +35,7 @@ type ImageCanvasPanelDockViewProps = { isZoomMenuOpen: boolean; isBackgroundSettingsOpen: boolean; activeSidebarPanel: SidebarPanel | null; + isAgentConversationEnabled: boolean; isAgentConversationOpen: boolean; isMinimapOpen: boolean; minimapModel: StageMinimapModel | null; @@ -62,6 +63,7 @@ export function ImageCanvasPanelDockView({ isZoomMenuOpen, isBackgroundSettingsOpen, activeSidebarPanel, + isAgentConversationEnabled, isAgentConversationOpen, isMinimapOpen, minimapModel, @@ -300,13 +302,15 @@ export function ImageCanvasPanelDockView({ pressed={activeSidebarPanel === 'layers'} onClick={() => onToggleSidebarPanel('layers')} /> - + {isAgentConversationEnabled ? ( + + ) : null} - + {isAgentConversationEnabled ? ( + + ) : null} {isToolbarGuideVisible ? (
{ if (disposed) { return; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 81879c63e..0cf010431 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,7 @@ interface ImportMetaEnv { readonly VITE_DEBUG_MODE?: string; + readonly VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR?: string; } interface Window {