移除资源画布顶部手动生成入口
Project CI / Repository checks (push) Successful in 6m9s
Project CI / Frontend tests (push) Successful in 6m19s
Project CI / Backend tests (push) Successful in 7m23s
Project CI / Native shell tests (push) Successful in 15m50s

删除资源画布顶部的生成视频、生成音效、生成背景音乐和新增 UI 设计按钮
移除仅服务于这些入口的前端 handler 与入口配置
保留 Agent 语义生成、已有资源编辑、生成动画和未完成任务恢复能力
补充顶部入口消失与画布级操作保留的定向回归测试
同步更新 AGC 配置白名单、技术方案与项目决策记录
This commit is contained in:
2026-08-24 18:29:43 +08:00
parent 15a524e6e0
commit b00a3f8057
9 changed files with 59 additions and 146 deletions
@@ -127,6 +127,7 @@ const rustSharedContractSource = fs.readFileSync(
const allowedUncalledTauriCommands = [
'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource',
'open_game_creator_launcher_window',
'open_game_creator_workspace_window',
];
@@ -1547,13 +1548,11 @@ if (!viteConfigSource.includes('allow: [repoRoot]')) {
);
}
if (
!(
tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
)
) {
if (!(
tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
)) {
throw new Error(
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
);
@@ -106,8 +106,6 @@ import {
type ProjectResourceEditCapability,
resolveProjectCharacterAnimationCapability,
resolveProjectResourceEditCapability,
type ResourceGenerationKind,
resourceGenerationOptions,
} from './resourceEditModel';
import {
type ResourceEditSubmitInput,
@@ -213,12 +211,6 @@ type UiEditorRoute = {
resourceLabel: string;
};
type CreateUiDesignResourceResult = {
asset: { id: string };
manifest: GameCreationAppManifest;
committedProjectRevision: number;
};
type DeriveLocalProjectResourceResult = {
operationId: string;
sourceResourceId: string;
@@ -1606,8 +1598,7 @@ export default function ProjectDevelopmentView({
}
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
| ResourceCategory
| undefined;
ResourceCategory | undefined;
if (!viewport || !category) {
return;
}
@@ -1626,8 +1617,7 @@ export default function ProjectDevelopmentView({
const event = rawEvent as WebKitGestureEvent;
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
| ResourceCategory
| undefined;
ResourceCategory | undefined;
if (!viewport || !category) {
return;
}
@@ -1739,8 +1729,7 @@ export default function ProjectDevelopmentView({
.querySelectorAll<HTMLElement>('[data-resource-section-scroll]')
.forEach((viewport) => {
const category = viewport.dataset.resourceSectionScroll as
| ResourceCategory
| undefined;
ResourceCategory | undefined;
if (!category) {
return;
}
@@ -1760,8 +1749,7 @@ export default function ProjectDevelopmentView({
?.querySelectorAll<HTMLElement>('[data-resource-section-scroll]')
.forEach((viewport) => {
const category = viewport.dataset.resourceSectionScroll as
| ResourceCategory
| undefined;
ResourceCategory | undefined;
if (!category) {
return;
}
@@ -2258,34 +2246,6 @@ export default function ProjectDevelopmentView({
],
);
const createUiDesignResource = useCallback(async () => {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
setAssetCanvasNotice('UI 设计资源需要在客户端内创建');
return;
}
setAssetCanvasNotice('正在创建 UI 设计…');
try {
const result = await invoke<CreateUiDesignResourceResult>(
'create_ui_design_resource',
{ projectPath, expectedProjectId: manifest.projectId },
);
if (result.manifest.projectId !== manifest.projectId) {
throw new Error('UI 设计资源登记结果与当前项目不一致');
}
onManifestChange?.(projectPath, result.manifest, {
projectId: result.manifest.projectId,
revision: result.committedProjectRevision,
source: 'asset-command',
});
setAssetCanvasNotice('UI 设计已创建,正在同步资源与布局…');
} catch (error) {
setAssetCanvasNotice(
error instanceof Error ? error.message : String(error),
);
}
}, [manifest.projectId, onManifestChange, projectPath]);
const resetResourceCanvasViewport = useCallback(() => {
const canvasSize = resourceCanvasElementSize(resourceCanvasRef.current);
setConstrainedResourceCanvasViewport(
@@ -2635,39 +2595,6 @@ export default function ProjectDevelopmentView({
[advanceFocusGeneration],
);
const openCreateResource = useCallback(
(kind: ResourceGenerationKind) => {
canvasOpenEpochRef.current += 1;
advanceFocusGeneration();
const flowId = crypto.randomUUID();
activeFocusFlowIdRef.current = flowId;
pendingResourceFocusRef.current = null;
setAssetCanvasRoute(null);
setAssetCanvasNotice('');
const sourceMediaType = kind === 'video' ? 'video/mp4' : 'audio/mpeg';
setResourceEditorRoute({
flowId,
operationId: crypto.randomUUID(),
idempotencyKey: crypto.randomUUID(),
resource: null,
capability: {
route: 'derive',
editKind: kind,
sourceMediaType,
semanticNotice:
kind === 'background-music'
? '将生成全新的背景音乐。'
: kind === 'sound-effect'
? '将生成全新的音效。'
: '将生成全新的视频。',
generationMode: 'create',
},
generationMode: 'create',
});
},
[advanceFocusGeneration],
);
const cancelResourceEditor = useCallback(() => {
if (resourceEditorRoute) {
resourceEditorRevisionRef.current.delete(resourceEditorRoute.operationId);
@@ -3277,16 +3204,16 @@ export default function ProjectDevelopmentView({
dependencyLayoutSettled: dependencyLayout.settled,
dependencyPositioned: Boolean(
intent.resourceId &&
dependencyLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
dependencyLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
),
typeLayoutSettled: typeLayout.settled,
typePositioned: Boolean(
intent.resourceId &&
typeLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
typeLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
),
visible: targetVisible,
domRendered: Boolean(card),
@@ -3457,23 +3384,6 @@ export default function ProjectDevelopmentView({
: `管理未完成编辑 (${pendingResourceEdits.length})`}
</button>
) : null}
{resourceGenerationOptions.map((option) => (
<button
key={option.kind}
type="button"
onClick={() => openCreateResource(option.kind)}
>
<Sparkles size={15} aria-hidden="true" />
{option.label}
</button>
))}
<button
type="button"
onClick={() => void createUiDesignResource()}
>
<Sparkles size={15} aria-hidden="true" />
UI
</button>
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
@@ -32,20 +32,6 @@ export type ProjectResourceEditCapability =
generationMode?: 'create' | 'derive';
};
export type ResourceGenerationKind = Exclude<
LocalProjectResourceEditKind,
'image-reference' | 'svg' | 'text' | 'agent-result' | 'version'
>;
export const resourceGenerationOptions: Array<{
kind: ResourceGenerationKind;
label: string;
}> = [
{ kind: 'video', label: '生成视频' },
{ kind: 'sound-effect', label: '生成音效' },
{ kind: 'background-music', label: '生成背景音乐' },
];
const extensionMediaTypes: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
@@ -1800,6 +1800,14 @@ export function registerProjectWorkbenchFoundationTests() {
expect(
screen.getByText('美术资源计划已完成,尚未生成或登记图片'),
).not.toBeNull();
for (const label of [
'生成视频',
'生成音效',
'生成背景音乐',
'新增 UI 设计',
]) {
expect(screen.queryByRole('button', { name: label })).toBeNull();
}
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
const searchInput = screen.getByLabelText(
@@ -1855,7 +1863,14 @@ export function registerProjectWorkbenchFoundationTests() {
'resources.focused.document',
);
expect(screen.getByLabelText('搜索项目资源')).not.toBeNull();
expect(screen.getByRole('button', { name: '生成视频' })).not.toBeNull();
for (const label of [
'生成视频',
'生成音效',
'生成背景音乐',
'新增 UI 设计',
]) {
expect(screen.queryByRole('button', { name: label })).toBeNull();
}
expect(screen.getByRole('button', { name: '按依赖' })).not.toBeNull();
expect(
screen
@@ -9409,8 +9424,7 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
});
let directTurnUpdateHandler:
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
((event: { payload: Record<string, unknown> }) => void) | null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -10368,8 +10382,7 @@ export function registerProjectAgentStatusTests() {
},
);
let runtimeUpdateHandler:
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
((event: { payload: Record<string, unknown> }) => void) | null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -700,15 +700,14 @@ describe('project resource live canvas integration', () => {
const sourceCard = await screen.findByRole('button', {
name: /source-art\.png/,
});
expect(
(
screen.getByRole('button', {
name: '生成视频',
}) as HTMLButtonElement
).disabled,
).toBe(false);
expect(screen.getByRole('button', { name: '生成音效' })).not.toBeNull();
expect(screen.getByRole('button', { name: '生成背景音乐' })).not.toBeNull();
for (const label of [
'生成视频',
'生成音效',
'生成背景音乐',
'新增 UI 设计',
]) {
expect(screen.queryByRole('button', { name: label })).toBeNull();
}
fireEvent.click(sourceCard);
const sourceDetails = screen.getByRole('dialog', {
name: /source-art\.png/,
@@ -718,7 +717,14 @@ describe('project resource live canvas integration', () => {
);
expect(sourceDetails.hasAttribute('aria-modal')).toBe(false);
expect(document.querySelector('.game-resource-focus-backdrop')).toBeNull();
expect(screen.getByRole('button', { name: '生成视频' })).not.toBeNull();
for (const label of [
'生成视频',
'生成音效',
'生成背景音乐',
'新增 UI 设计',
]) {
expect(screen.queryByRole('button', { name: label })).toBeNull();
}
expect(
screen
.getByRole('button', { name: '按依赖' })
@@ -5,7 +5,6 @@ import {
resolveProjectCharacterAnimationCapability,
resolveProjectResourceEditCapability,
resourceEditPromptMaxLength,
resourceGenerationOptions,
} from '../src/view/project-development/resourceEditModel';
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
@@ -241,12 +240,4 @@ describe('全类型资源编辑能力模型', () => {
),
).toBeNull();
});
it('资源页提供新建视频、音效和背景音乐入口', () => {
expect(resourceGenerationOptions).toEqual([
{ kind: 'video', label: '生成视频' },
{ kind: 'sound-effect', label: '生成音效' },
{ kind: 'background-music', label: '生成背景音乐' },
]);
});
});
@@ -14457,3 +14457,9 @@
- 决策:将 External v1 `/api/external/v1/editor/images/background-removals` 通过 `agc_remove_background` 加入受控 `agc_tools`。工具只接受当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端负责正式 resourceId、画布/素材目录、稳定 operation/idempotency 身份、权限和错误脱敏,不向 Codex 暴露内部 BgFilter worker、凭据或任意 API。
- 约束:异步结果只投影有界队列状态,不允许模型自行构造源 URL 或在不确定提交后更换请求身份;External v1 负责 API Key、幂等接收与统一 operation 查询,客户端不得绕过该契约。
## 2026-08-24 AGC 资源自由画板顶部移除手动新建入口
- 背景:无源视频、音效、背景音乐和 UI 设计已经有 Agent/编辑器权威生成链路,资源自由画板顶部继续并列手动新建按钮会形成第二套普通用户入口,并挤占画布级操作空间。
- 决策:资源自由画板顶部移除“生成视频”“生成音效”“生成背景音乐”和“新增 UI 设计”;保留播放、未完成编辑恢复、排序、复位,以及已有资源详情中的编辑、图片“生成动画”和精修图片“修改”。底层生成、恢复和事务能力不因入口移除而退役。
- 验证:运行项目开发工作台与资源实时集成定向测试、AGC typecheck、编码检查和 `git diff --check`,并在桌面视口确认顶部无上述四个入口且画布级动作仍可见。
@@ -13,7 +13,7 @@
- 统一资源编辑命令新增 `generationMode=create|derive``create` 用于无源生成视频、音效和背景音乐,`derive` 保持现有“基于已有资源派生”。视频新建请求不再携带 `referenceVideoSrcs`;音效 / 背景音乐新建使用同款文本生成端点,但提示词前缀改为“生成新音频”。
- 图片资源新增“生成动画”派生入口:复用 `/api/external/v1/editor/character-animations/generations`(账号模式自动映射 `/api/editor/character-animations/generations`),以源图片的稳定 objectKey、真实宽高和固定 `seedance2.0-fast / 720p / same / 32帧 / 4秒` 参数提交;完成后下载 `previewVideoPath` 预览视频并以 `character-animation` 资源类型登记为本地 MP4/WebM 媒体,避免只保存首帧。
- 本地 manifest 资产模型新增 `imageSequenceFrames``imageSequenceDurationMs`:角色动画完成时把远端 `frames``durationSeconds` 随账本持久化,恢复或重新提交后仍写回 manifest,前端资源投影同步暴露。
- 资源页顶部新增“生成视频 / 生成音效 / 生成背景音乐”三个入口,图片聚焦态新增“生成动画”按钮;统一资源编辑面板按 `generationMode` 显示“新建媒体资源”或“编辑现有资源”。
- 资源页顶部不再展示“生成视频 / 生成音效 / 生成背景音乐”手动新建入口;无源资源生成继续由 Agent 语义工具调用统一资源生成能力。图片聚焦态保留“生成动画”按钮;统一资源编辑面板继续`generationMode` 区分“新建媒体资源”或“编辑现有资源”。
- 新建视频 / 音效 / 背景音乐时,先复用 `prepare_external_canvas_generation_context` 创建或匹配同名画板项目与素材库目录,并在生成请求中携带 `projectId / assetFolderId / canvasCompletion`,结果同时进入平台画布 / 素材库与本地 manifest。
- durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。
- 资源编辑请求指纹纳入 `generationMode`,同一 `operationId` 换生成模式重试时不再按旧账本模式恢复;旧账本继续通过 legacy 指纹兼容。
@@ -60,6 +60,7 @@
- 图片选中后的浮动工具栏整体退役。普通点击图片直接打开唯一的图片下方快速编辑卡;拖动不误触发,点击另一张图片切换卡片,点击画布空白关闭卡片。
- 快速编辑卡底部统一承载“删除”“设为最终图”“修改”。“删除”只删除当前绑定图层并进入画布 history;“设为最终图”只对完整候选媒体可用;“修改”创建新 generation,不覆盖来源图层。
- 页面级返回、导入、撤销和重做不属于图片浮动工具条,本次不以复制另一套图片操作条替代已退役工具条。
- 资源自由画板顶部不展示“生成视频”“生成音效”“生成背景音乐”和“新增 UI 设计”等手动新建入口;顶部只保留播放、未完成编辑恢复、排序与复位等画布级动作。无源资源生成由 Agent 语义工具进入既有生成合同,已有资源的编辑、图片“生成动画”和精修图片“修改”入口不受影响。
- refine `/api/external/v1/editor/images/edits` 严格遵循 `EditorImageEditRequest`,不得携带 `assetKind``sourceImageSrc``sourceResourceId`。Game Agent 本地 kind 在登记编辑器来源前映射到现役 canonical kind;服务端安全业务错误必须结构化返回,不能把全部 400 折叠为同一个“平台明确拒绝”。
- 生成提交后立即创建画布占位和任务侧栏记录;排队、运行、远端完成、下载、候选就绪、失败和对账是任务级状态,不得用全屏 `canvas.generating/canvas.failed` 遮挡或终结整张画布。重新进入时,关键提交事务由 `host.recover()` 在 hydrate 前收敛;草稿 hydrate 完成后立即进入 `canvas.editing`,耗时的 `recoverImages()` 转为后台任务投影恢复,只更新任务、候选和 notice,不能继续锁住画布或因后台恢复失败切换为全屏失败。真正阻断草稿/提交/恢复的错误使用挂到 `document.body` 的全窗口 portal,必须覆盖整个 Tauri WebView,而不是只覆盖左侧画布区域。
@@ -87,6 +88,7 @@
12. 明确失败任务可从占位或任务侧栏归档,重启后不恢复;`reconciliation-required` 不显示删除能力。任务列表具有与现役美术画布一致的独立按钮、双 Tab、状态图标与时间/进度信息;折叠后只保留按钮且不丢任务数据,新建任务自动展开。
13. 画布带运行中 generation 重新进入时,在远端恢复完成前已经可选图、平移和继续编辑;后台恢复继续推进原 operation,不重复 POST、不重新扣费。阻断性提交/恢复失败的遮罩覆盖整个 Tauri 窗口。
14. 已登记 refine 资产的候选设为最终图后保持原 `assetId`、原 `source.resourceId` 与新正式 PNG 路径;`revision-installed` 中断后重新加载可前向收敛为 committed。
15. 资源自由画板顶部不显示“生成视频”“生成音效”“生成背景音乐”或“新增 UI 设计”;播放、未完成编辑恢复、按依赖、按类型和复位仍可正常操作。
## PR 176 事务与宿主合同补充