diff --git a/TRACKING.md b/TRACKING.md index 513cc772..5aae7418 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -63,3 +63,4 @@ - 2026-06-12 生成工具修正:移除拼图素材的生成图 mock 元数据,使其作为普通素材显示;底部生成工具改为先打开生成图片对话框,再进入生成中状态并把生成结果加入画布,生成结果保留元数据与修改入口。 - 2026-06-13 真实生图修正:`/api/editor/images/generations` 和 `/api/editor/images/edits` 统一走 api-server VectorEngine `gpt-image-2` BFF;前端不再创建 mock 成功图,生成 / 修改失败会留在对话框内显示错误;生成图右上角 `{}` 元数据按钮可直接点击打开元数据窗口。 - 2026-06-13 素材库修正:素材栏按文件夹分组,文件夹支持折叠和新建;上传入口可定向到当前文件夹,上传素材进入素材库并支持删除,内置素材只保留添加和重命名。 +- 2026-06-13 生图鉴权修正:编辑器工程和真实生图请求不再使用禁止 refresh 的局部鉴权策略,可通过 refresh cookie 静默补 access token;真实生图遇到 401 / 403 时弹窗显示“请先登录后再生成图片”,不再暴露后端 requestId 主文案。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 92398c1d..f2348490 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -34,7 +34,7 @@ - 图片文件本体继续走 OSS,浏览器读取私有 generated 对象仍经 `/api/assets/read-url` 换签。 - 资源表只保存资源元数据;图层位置、尺寸、缩放、层级和选中所需 ID 保存在 `editor_project` 的布局 JSON。 - 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。 -- 未登录用户可以使用本地演示态,但不触发工程自动保存。 +- 未登录用户可以使用本地演示态,但不触发工程自动保存;真实图片生成 / 修改需要登录。编辑器 API 请求允许使用 refresh cookie 静默补 access token,但 401 / 403 只在编辑器局部提示登录,不清空整站登录态,也不把后端 requestId 直接作为生图弹窗主文案。 ## 后端接口 diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index ad9d7775..0a3e81d2 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ApiClientError } from '../../services/apiClient'; import { ImageCanvasEditorView } from './ImageCanvasEditorView'; const generateEditorImageMock = vi.hoisted(() => vi.fn()); @@ -341,6 +342,30 @@ describe('ImageCanvasEditorView', () => { expect(screen.queryByAltText(/画布图片:生成图片/)).toBeNull(); }); + it('asks the user to log in when real generation is unauthorized', async () => { + generateEditorImageMock.mockRejectedValueOnce( + new ApiClientError({ + message: '未授权访问(requestId: web-login-required)', + status: 401, + code: 'UNAUTHORIZED', + }), + ); + render(); + + fireEvent.click(screen.getByRole('button', { name: '生成工具' })); + fireEvent.change(screen.getByLabelText('生成提示词'), { + target: { value: '一张需要登录生成的图' }, + }); + fireEvent.click(screen.getByRole('button', { name: '生成' })); + + await waitFor(() => { + expect(screen.getByRole('alert').textContent).toBe( + '请先登录后再生成图片', + ); + }); + expect(screen.queryByText(/requestId/u)).toBeNull(); + }); + it('switches tools and restores the previous tool after holding Space', async () => { const user = userEvent.setup(); render(); diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index cfb47c65..4a1cf343 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -43,6 +43,7 @@ import { loadOrCreateRecentEditorProject, saveEditorProjectLayout, } from '../../services/image-editor/editorProjectClient'; +import { ApiClientError } from '../../services/apiClient'; type EditorAsset = { id: string; @@ -427,6 +428,19 @@ function getPointerId(event: ReactPointerEvent) { return Number.isFinite(nativeId) ? nativeId : -1; } +function resolveImageGenerationErrorMessage(error: unknown) { + if ( + error instanceof ApiClientError && + (error.status === 401 || error.status === 403) + ) { + return '请先登录后再生成图片'; + } + + return error instanceof Error && error.message.trim() + ? error.message + : '生成图片失败'; +} + export function ImageCanvasEditorView() { const canvasViewportRef = useRef(null); const uploadInputRef = useRef(null); @@ -1016,10 +1030,7 @@ export function ImageCanvasEditorView() { ...dialog, prompt: normalizedPrompt, status: 'failed', - errorMessage: - error instanceof Error && error.message.trim() - ? error.message - : '生成图片失败', + errorMessage: resolveImageGenerationErrorMessage(error), }); } }; diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index ce4b0fba..0ea9da11 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -40,7 +40,10 @@ describe('editorProjectClient', () => { '/api/editor/projects/recent', { method: 'GET' }, '读取图片画布工程失败', - expect.objectContaining({ authImpact: 'local' }), + expect.objectContaining({ + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, + }), ); }); @@ -70,7 +73,10 @@ describe('editorProjectClient', () => { body: JSON.stringify({ title: '未命名画布' }), }), '创建图片画布工程失败', - expect.objectContaining({ authImpact: 'local' }), + expect.objectContaining({ + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, + }), ); }); @@ -102,7 +108,10 @@ describe('editorProjectClient', () => { }), }), '保存图片画布工程失败', - expect.objectContaining({ authImpact: 'local' }), + expect.objectContaining({ + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, + }), ); }); @@ -149,7 +158,10 @@ describe('editorProjectClient', () => { }), }), '创建图片画布资源失败', - expect.objectContaining({ authImpact: 'local' }), + expect.objectContaining({ + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, + }), ); }); @@ -175,7 +187,10 @@ describe('editorProjectClient', () => { body: JSON.stringify({ title: '角色设定板' }), }), '创建图片画布工程失败', - expect.objectContaining({ authImpact: 'local' }), + expect.objectContaining({ + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, + }), ); }); @@ -204,7 +219,8 @@ describe('editorProjectClient', () => { }), '生成图片失败', expect.objectContaining({ - authImpact: 'local', + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), @@ -242,7 +258,8 @@ describe('editorProjectClient', () => { }), '修改图片失败', expect.objectContaining({ - authImpact: 'local', + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, timeoutMs: 1_200_000, retry: { maxRetries: 0 }, }), diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index 53db178b..b65a3542 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -5,7 +5,8 @@ const EDITOR_IMAGE_GENERATION_API = '/api/editor/images/generations'; const EDITOR_IMAGE_EDIT_API = '/api/editor/images/edits'; const DEFAULT_PROJECT_TITLE = '未命名画布'; const EDITOR_PROJECT_REQUEST_OPTIONS = { - authImpact: 'local' as const, + clearAuthOnUnauthorized: false, + notifyAuthStateChange: false, }; export type EditorCanvasViewport = {