改为运行时控制画板 Agent 入口

新增前端运行时配置接口下发画板 Agent 开关

将画板 Agent 入口改为读取后端运行时配置

把开关变量改为 GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR

补充前后端测试和生产环境变量文档
This commit is contained in:
2026-07-07 14:03:45 +08:00
parent 1d43783685
commit 2a98b60830
17 changed files with 227 additions and 23 deletions
@@ -41,7 +41,6 @@ type EditorAgentConversationSummary = Awaited<
type EditorAgentConversationDetail = Awaited<
ReturnType<EditorAgentGetConversation>
>;
const EDITOR_AGENT_SIDEBAR_ENV = 'VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR';
const listEditorAgentConversationsMock = vi.hoisted(() =>
vi.fn<
@@ -110,6 +109,7 @@ const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
const getPlatformProfileDashboardMock = vi.hoisted(() => vi.fn());
const loadFrontendRuntimeConfigMock = vi.hoisted(() => vi.fn());
vi.mock('../../services/image-editor/editorProjectClient', async () => {
const actual = await vi.importActual<
@@ -140,6 +140,10 @@ vi.mock('../../services/platform-entry/platformProfileClient', () => ({
getPlatformProfileDashboard: getPlatformProfileDashboardMock,
}));
vi.mock('../../services/frontendRuntimeConfigService', () => ({
loadFrontendRuntimeConfig: loadFrontendRuntimeConfigMock,
}));
vi.mock('../../services/image-editor/editorAgentClient', () => ({
createEditorAgentConversation: createEditorAgentConversationMock,
deleteEditorAgentConversation: deleteEditorAgentConversationMock,
@@ -214,7 +218,7 @@ function openMinimap() {
}
async function ensureAgentConversationOpen() {
const agentButton = screen.getByRole('button', { name: '画布 Agent' });
const agentButton = await screen.findByRole('button', { name: '画布 Agent' });
if (agentButton.getAttribute('aria-pressed') !== 'true') {
fireEvent.click(agentButton);
}
@@ -222,7 +226,9 @@ async function ensureAgentConversationOpen() {
}
function enableEditorAgentSidebarForTest() {
vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'true');
loadFrontendRuntimeConfigMock.mockResolvedValue({
imageEditorAgentSidebarEnabled: true,
});
}
describe('ImageCanvasEditorView', () => {
@@ -246,7 +252,9 @@ describe('ImageCanvasEditorView', () => {
});
beforeEach(() => {
vi.stubEnv(EDITOR_AGENT_SIDEBAR_ENV, 'false');
loadFrontendRuntimeConfigMock.mockResolvedValue({
imageEditorAgentSidebarEnabled: false,
});
listEditorAgentConversationsMock.mockResolvedValue([]);
createEditorAgentConversationMock.mockResolvedValue({
...createEditorAgentConversationSummary({
@@ -277,7 +285,7 @@ describe('ImageCanvasEditorView', () => {
deleteEditorAgentConversationMock.mockReset();
streamEditorAgentMessageMock.mockReset();
getPlatformProfileDashboardMock.mockReset();
vi.unstubAllEnvs();
loadFrontendRuntimeConfigMock.mockReset();
});
it('loads the project from projectid query before falling back to recent project', async () => {
@@ -1487,6 +1495,8 @@ describe('ImageCanvasEditorView', () => {
enableEditorAgentSidebarForTest();
render(<ImageCanvasEditorView />);
await screen.findByRole('button', { name: '画布 Agent' });
const assetSidebar = openAssetSidebar();
expect(within(assetSidebar).getByText('素材')).toBeTruthy();
@@ -1507,7 +1517,7 @@ describe('ImageCanvasEditorView', () => {
screen.getByRole('complementary', { name: '画布任务列表' }),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '画布 Agent' }));
fireEvent.click(await screen.findByRole('button', { name: '画布 Agent' }));
expect(
screen.getByRole('complementary', { name: '图片资源栏' }),
).toBeTruthy();
@@ -19,6 +19,7 @@ import {
loadEditorProject,
loadEditorGenerationPricing,
} from '../../services/image-editor/editorProjectClient';
import { loadFrontendRuntimeConfig } from '../../services/frontendRuntimeConfigService';
import { shouldShowRechargeEntry } from '../../services/payment/paymentPlatform';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import { useAuthUi } from '../auth/AuthUiContext';
@@ -117,12 +118,6 @@ 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);
}
@@ -327,6 +322,9 @@ export function ImageCanvasEditorView({
const showRechargeEntry = shouldShowRechargeEntry();
const currentEditorUserId = authUi?.user?.id ?? null;
const [isToolbarGuideVisible, setIsToolbarGuideVisible] = useState(false);
const [isAgentConversationEnabled, setIsAgentConversationEnabled] =
useState(false);
const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(false);
useEffect(() => {
let isMounted = true;
@@ -345,6 +343,28 @@ export function ImageCanvasEditorView({
isMounted = false;
};
}, []);
useEffect(() => {
let isMounted = true;
void loadFrontendRuntimeConfig()
.then((config) => {
if (!isMounted) {
return;
}
const enabled = config.imageEditorAgentSidebarEnabled === true;
setIsAgentConversationEnabled(enabled);
setIsAgentConversationOpen(enabled);
})
.catch(() => {
if (!isMounted) {
return;
}
setIsAgentConversationEnabled(false);
setIsAgentConversationOpen(false);
});
return () => {
isMounted = false;
};
}, []);
const selectedLayerIdsRef = useRef<string[]>([]);
const setQuickEditPanelRef = useRef<
Dispatch<SetStateAction<QuickEditPanelState | null>>
@@ -1167,10 +1187,6 @@ export function ImageCanvasEditorView({
applyProjectSnapshot: applyGeneratedProjectSnapshot,
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
});
const isAgentConversationEnabled = resolveEditorAgentSidebarEnabled();
const [isAgentConversationOpen, setIsAgentConversationOpen] = useState(
() => isAgentConversationEnabled,
);
const effectiveIsAgentConversationOpen =
isAgentConversationEnabled && isAgentConversationOpen;
const toggleAgentConversation = useCallback(() => {
@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const apiClientMocks = vi.hoisted(() => ({
requestJson: vi.fn(),
}));
vi.mock('./apiClient', async () => {
const actual =
await vi.importActual<typeof import('./apiClient')>('./apiClient');
return {
...actual,
requestJson: apiClientMocks.requestJson,
};
});
import { loadFrontendRuntimeConfig } from './frontendRuntimeConfigService';
describe('frontendRuntimeConfigService', () => {
beforeEach(() => {
vi.clearAllMocks();
apiClientMocks.requestJson.mockResolvedValue({
imageEditorAgentSidebarEnabled: false,
});
});
it('loads public frontend runtime config without auth side effects', async () => {
await expect(loadFrontendRuntimeConfig()).resolves.toEqual({
imageEditorAgentSidebarEnabled: false,
});
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
'/api/runtime/frontend-config',
{ method: 'GET' },
'读取前端运行时配置失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
});
});
@@ -0,0 +1,21 @@
import { requestJson } from './apiClient';
const FRONTEND_RUNTIME_CONFIG_API = '/api/runtime/frontend-config';
export type FrontendRuntimeConfig = {
imageEditorAgentSidebarEnabled: boolean;
};
export async function loadFrontendRuntimeConfig() {
return requestJson<FrontendRuntimeConfig>(
FRONTEND_RUNTIME_CONFIG_API,
{ method: 'GET' },
'读取前端运行时配置失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
}
-1
View File
@@ -2,7 +2,6 @@
interface ImportMetaEnv {
readonly VITE_DEBUG_MODE?: string;
readonly VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR?: string;
}
interface Window {