控制画布 Agent 入口默认关闭

新增 VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR 控制画布 Agent 入口显示。

默认 .env 关闭、本地 .env.local 开启,并补充示例和类型声明。

禁用时隐藏 dock 按钮和收起态 Agent 面板,开启时保留右侧任务列表互斥逻辑。

修正右侧 Agent 和任务列表切换不影响左侧资源栏。

补充画布 Agent 开关、面板独立性和 dock 隐藏测试。
This commit is contained in:
2026-07-06 18:51:11 +08:00
parent 7c449f0596
commit b8e6e9f3ef
12 changed files with 242 additions and 51 deletions
+2
View File
@@ -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=""
+4
View File
@@ -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.
+2
View File
@@ -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"
@@ -139,6 +139,7 @@ function createStageProps(): ImageCanvasStageViewProps {
editorAgentAssets: [],
taskListRefreshKey: 0,
isTaskSidebarOpen: false,
isAgentConversationEnabled: false,
isAgentConversationOpen: false,
generateDialog: null,
cropExpandPanel: null,
@@ -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<EditorAgentListConversations>
>[number];
type EditorAgentConversationDetail = Awaited<
ReturnType<EditorAgentGetConversation>
>;
const EDITOR_AGENT_SIDEBAR_ENV = 'VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR';
const listEditorAgentConversationsMock = vi.hoisted(() =>
vi.fn<
Parameters<EditorAgentListConversations>,
ReturnType<EditorAgentListConversations>
>(async () => []),
);
const createEditorAgentConversationMock = vi.hoisted(() =>
vi.fn<
Parameters<EditorAgentCreateConversation>,
ReturnType<EditorAgentCreateConversation>
>(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<EditorAgentGetConversation>,
ReturnType<EditorAgentGetConversation>
>(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<EditorAgentDeleteConversation>,
ReturnType<EditorAgentDeleteConversation>
>(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<EditorAgentStreamMessage>,
ReturnType<EditorAgentStreamMessage>
>(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> = {},
): 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(<ImageCanvasEditorView />);
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(<ImageCanvasEditorView />);
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(<ImageCanvasEditorView />);
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(<ImageCanvasEditorView />);
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(<ImageCanvasEditorView />);
@@ -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(<ImageCanvasEditorView />);
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(<ImageCanvasEditorView />);
fireEvent.click(screen.getByRole('button', { name: '画布 Agent' }));
await ensureAgentConversationOpen();
await waitFor(() => {
expect(getEditorAgentConversationMock).toHaveBeenCalledWith(
'editor-agent-conv-test',
@@ -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,
@@ -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 },
@@ -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')}
/>
<EditorIconButton
label="画布 Agent"
title="画布 Agent"
icon={MessageCircle}
pressed={isAgentConversationOpen}
onClick={onToggleAgentConversation}
/>
{isAgentConversationEnabled ? (
<EditorIconButton
label="画布 Agent"
title="画布 Agent"
icon={MessageCircle}
pressed={isAgentConversationOpen}
onClick={onToggleAgentConversation}
/>
) : null}
<EditorIconButton
label="切换小地图"
title="小地图"
@@ -157,6 +157,7 @@ function SidebarTabsHarness() {
isZoomMenuOpen={false}
isBackgroundSettingsOpen={false}
activeSidebarPanel={chrome.activeSidebarPanel}
isAgentConversationEnabled={false}
isAgentConversationOpen={false}
isMinimapOpen={false}
minimapModel={null}
@@ -62,6 +62,7 @@ export type ImageCanvasStageViewProps = {
isToolbarGuideVisible?: boolean;
taskListRefreshKey: number;
isTaskSidebarOpen: boolean;
isAgentConversationEnabled: boolean;
isAgentConversationOpen: boolean;
generateDialog: GenerateDialogState | null;
cropExpandPanel: CropExpandPanelState | null;
@@ -212,6 +213,7 @@ export function ImageCanvasStageView({
isToolbarGuideVisible = false,
taskListRefreshKey,
isTaskSidebarOpen,
isAgentConversationEnabled,
isAgentConversationOpen,
generateDialog,
cropExpandPanel,
@@ -441,6 +443,7 @@ export function ImageCanvasStageView({
isZoomMenuOpen={isZoomMenuOpen}
isBackgroundSettingsOpen={isBackgroundSettingsOpen}
activeSidebarPanel={activeSidebarPanel}
isAgentConversationEnabled={isAgentConversationEnabled}
isAgentConversationOpen={isAgentConversationOpen}
isMinimapOpen={isMinimapOpen}
minimapModel={minimapModel}
@@ -467,14 +470,16 @@ export function ImageCanvasStageView({
onFocusExternalTask={onFocusExternalTask}
/>
<EditorAgentConversationPanelView
projectId={projectId}
open={isAgentConversationOpen}
onToggleOpen={onToggleAgentConversation}
layers={layers}
assets={editorAgentAssets}
onGenerationResult={onEditorAgentGenerationResult}
/>
{isAgentConversationEnabled ? (
<EditorAgentConversationPanelView
projectId={projectId}
open={isAgentConversationOpen}
onToggleOpen={onToggleAgentConversation}
layers={layers}
assets={editorAgentAssets}
onGenerationResult={onEditorAgentGenerationResult}
/>
) : null}
{isToolbarGuideVisible ? (
<div
@@ -294,8 +294,7 @@ export function useEditorAgentConversation({
let disposed = false;
setIsLoadingConversations(true);
setErrorMessage(null);
client
.listConversations(normalizedProjectId)
Promise.resolve(client.listConversations(normalizedProjectId) ?? [])
.then(async (nextConversations) => {
if (disposed) {
return;
+1
View File
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_DEBUG_MODE?: string;
readonly VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR?: string;
}
interface Window {