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 = {