diff --git a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx
index 1951814b6..2c5164ecf 100644
--- a/apps/admin-web/src/pages/AdminDashboardPage.test.tsx
+++ b/apps/admin-web/src/pages/AdminDashboardPage.test.tsx
@@ -93,6 +93,21 @@ test('Dashboard 默认加载今日指标并支持运营汇总页签', async () =
expect(screen.getByText('访问模块分布')).toBeTruthy();
});
+test('Dashboard 默认日期使用北京时间', async () => {
+ vi.setSystemTime(new Date('2026-06-26T17:30:00Z'));
+
+ render();
+
+ await waitFor(() => {
+ expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', {
+ granularity: 'period',
+ anchor: undefined,
+ startDate: '2026-06-27',
+ endDate: '2026-06-27',
+ });
+ });
+});
+
test('Dashboard 选择本周时按今天填充整周范围', async () => {
const user = setupUser();
render();
diff --git a/apps/admin-web/src/pages/AdminDashboardPage.tsx b/apps/admin-web/src/pages/AdminDashboardPage.tsx
index 88fd78a15..d4f393504 100644
--- a/apps/admin-web/src/pages/AdminDashboardPage.tsx
+++ b/apps/admin-web/src/pages/AdminDashboardPage.tsx
@@ -36,7 +36,7 @@ export function AdminDashboardPage({
const [granularity, setGranularity] =
useState('day');
const [dateRange, setDateRange] = useState(() => {
- const today = formatDateInput(new Date());
+ const today = formatBeijingDateInput(new Date());
return { startDate: today, endDate: today };
});
const [activeTab, setActiveTab] = useState('metrics');
@@ -327,7 +327,7 @@ export function AdminDashboardPage({
) {
const nextRange = buildPresetDateRange(
nextGranularity,
- formatDateInput(new Date()),
+ formatBeijingDateInput(new Date()),
);
const rangeChanged =
nextRange.startDate !== dateRange.startDate ||
@@ -450,11 +450,13 @@ function formatNumber(value: number) {
return new Intl.NumberFormat('zh-CN').format(value);
}
-function formatDateInput(date: Date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
+function formatBeijingDateInput(date: Date) {
+ return new Intl.DateTimeFormat('en-CA', {
+ timeZone: 'Asia/Shanghai',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ }).format(date);
}
function rangePrefix(granularity: AdminDashboardGranularity) {
@@ -474,7 +476,7 @@ function buildPresetDateRange(
granularity: AdminDashboardGranularity,
selectedDate: string,
) {
- const fallbackDate = formatDateInput(new Date());
+ const fallbackDate = formatBeijingDateInput(new Date());
const anchor = parseDateValueAsUtc(selectedDate)
? selectedDate
: fallbackDate;
diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md
index e12ac6eb7..1f917b8d4 100644
--- a/docs/project-memory/shared-memory/pitfalls.md
+++ b/docs/project-memory/shared-memory/pitfalls.md
@@ -62,6 +62,14 @@
- 验证:画板生成 workflow 测试覆盖 queueState 持续 `running` 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`。
+## 画板参考图 objectKey 必须先做归属校验
+
+- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
+- 原因:Data URL 参考图可以直接解析,但 objectKey 是服务端私有对象引用;只校验 generated 前缀、mime 和大小不能证明它属于当前账号。
+- 处理:所有编辑器参考图入口统一走 `parse_editor_reference_image(state, owner_user_id, source)`;objectKey 分支必须先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key,再读取 OSS。快速编辑和图标素材额外参考图也必须真实传到 provider,不只写 metadata。
+- 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入快速编辑 / 图标生成请求。
+- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-client/src/assets.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`。
+
## 编辑器生成按钮显示泥点后仍要查真实钱包预扣
- 现象:画板生成按钮显示 `N泥点`,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。
diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md
index 97af23d9b..b986a9cd2 100644
--- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md
+++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md
@@ -75,7 +75,7 @@
- `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;角色生成可携带 `model`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`,宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2`。`nanobanana2` 参考图作为 `inline_data` 进入 `generateContent`,`gpt-image-2` 参考图进入 edits。普通重绘继续走该接口并把当前图层图片作为参考图;图片快速编辑不走该接口。请求可携带 `projectId`、`assetFolderId`、`assetKind`、`generationInputs` 和 `sourceResourceId`,后端生成成功后创建 project resource / 账号素材并在响应中返回 resource / asset 快照。
- `POST /api/editor/icon-spritesheets/generations`:按图标规范图和素材描述数组生成 spritesheet,再由后端切分为独立透明图标。请求支持 `model`、`aspectRatio`、`imageSize`、`priceMudPoints`、`projectId`、`assetFolderId` 和 `generationInputs`;`priceMudPoints` 必须来自编辑器生成计费配置中对应生图模型的尺寸档位(如 `nanobanana2` 的 `0.5K / 1K / 2K` 或 `gpt-image-2` 的 `1K / 2K`),后端用 `editor_generation_config` 校验后才调用上游;`nanobanana2` 走原生 `generateContent` 并写入 `generationConfig.imageConfig.aspectRatio/imageSize`,`0.5K` 传 `"512"`;`gpt-image-2` 走 `/v1/images/edits`。后端把 spritesheet 和拆分后的 icon 都保存为 project resource / 账号素材,并随响应返回对应快照。
- `POST /api/editor/ui-designs/assets/extractions`:以前端已绘入红色框选轮廓的 UI 设计图 Data URL 作为参考图,固定 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。` 生成素材 spritesheet,再按连通域自动拆分为 `素材 1..N`,返回结构复用图标 spritesheet 响应。请求必须携带 `aspectRatio: "1:1"`、`imageSize: "1K" | "2K"` 和 `priceMudPoints`;框选数量不超过 6 个时前端按 `1:1·1K` 与 gpt-image-2 1K 价格提交,超过 6 个时按 `1:1·2K` 与 2K 价格提交。后端必须在调用上游前校验比例、尺寸和泥点价格,只允许 `1:1 / 1K / 2K`。请求可携带 `projectId`、`assetFolderId`、`generationInputs` 和 `spritesheetLabel`,后端保存 spritesheet / 拆分素材并返回对应 resource / asset 快照;前端必须把 spritesheet 原图与拆分素材都加入画布。
-- `POST /api/editor/images/edits`:按提示词和当前图片 Data URL 调用 VectorEngine edits,返回新的生成图片元数据;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。
+- `POST /api/editor/images/edits`:按提示词、当前图片 Data URL 和最多 8 张额外参考图调用 VectorEngine edits,返回新的生成图片元数据;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。
- `POST /api/editor/videos/generations`:按视频描述、模型、比例、时长、分辨率、模式、声音、默认联网搜索标记和泥点价格生成视频。前端可选模型为 `seedance2.0-fast`、`seedance2.0`、`kling3.0`、`kling3.0-omni`,默认 `seedance2.0-fast`;后端必须将 `seedance2.0-fast` 映射到 `doubao-seedance-2-0-fast-260128`,将 `seedance2.0` 映射到 `doubao-seedance-2-0-260128`,两者不得混用。后端允许 6 类比例、4 到 15 秒整数、`480p / 720p / 1080p`,并拒绝 `seedance2.0-fast + 1080p`;`sound=on/off` 映射 Ark `generate_audio=true/false`。后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS,返回 `videoSrc`、尺寸、prompt、model、provider、taskId、durationSeconds、resolution 和 `priceMudPoints`。
所有写接口都必须校验 Bearer 登录态和 owner;接口只返回当前用户有权读取的工程与资源。
@@ -108,7 +108,7 @@
- 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景使用前端轻量去背并回写为新的工程资源快照;前端轻量去背必须复用共享 `chromaKey` 的绿幕 / 近白背景模型,不再只按四角颜色扩散,避免纯白角色主体或主体内部浅色区域被扣碎。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K` 和 `提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。`,生成的 spritesheet 原图和拆分后的独立素材都作为画布图层保留。
- 重绘生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图,且重绘面板保持打开。
- 快速编辑 / 重绘站内 public 示例图、历史 generated 图或 OSS generated 图时,前端先读取成 `data:image/*;base64,...` 再提交,后端不得再收到 `/creation-type-references/*`、`/generated-*` 或 OSS URL 作为 `referenceImageSrcs/sourceImageSrc`。
-- 快速编辑的额外参考图只进入输入快照和前端上下文,不再作为图片编辑请求的 `referenceImageSrcs` 提交;需要真正多参考图编辑时再扩展 `/api/editor/images/edits` 契约。
+- 快速编辑的额外参考图既进入输入快照,也作为 `/api/editor/images/edits` 的 `referenceImageSrcs` 提交给后端;原图仍只作为 `sourceImageSrc`,不占用额外参考图额度。
- 素材文件夹可以新建、折叠、重命名和删除;删除普通文件夹后,其素材移动到“项目素材”。普通上传默认落入“上传素材”文件夹;素材库缺少该文件夹时,前端在首次普通上传前创建一次并复用,拖到指定文件夹或点击指定文件夹上传时仍进入目标文件夹。
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;仅在无法解析尺寸时才使用上传兜底尺寸。
- 音频 / 视频素材卡和画布媒体图层必须提供稳定的非文字视觉预览:优先使用 `thumbnailSrc` / 视频 `poster`,没有真实首帧或音频封面时使用由媒体类型、素材名和地址派生的确定性视觉底图。视频图层使用原生 `