From bfecfffd2b75e63c75ef60831919c161f1b787dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 20 Aug 2026 11:39:31 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E6=96=B0=E5=A2=9E=E2=80=9C=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=B9=B6=E5=A4=8D=E5=88=B6=20JSON=20=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E2=80=9D"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 109bd00338bf05cbe113b5ff57c3c4d71466a959. --- .../src-tauri/capabilities/main.json | 3 +- .../src/view/ui-editor/index.tsx | 89 ++++-------------- .../src/view/ui-editor/useUiEditorPage.ts | 8 +- .../tauri-plugin-clipboard-manager.mock.ts | 2 - .../tests/uiEditorPage.test.ts | 94 ------------------- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 6 files changed, 26 insertions(+), 172 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/main.json b/apps/ai-game-creator-shell/src-tauri/capabilities/main.json index 63679f90d..2c0510b8c 100644 --- a/apps/ai-game-creator-shell/src-tauri/capabilities/main.json +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/main.json @@ -1,12 +1,11 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main", - "description": "AI 游戏创作主窗口允许读取系统剪贴板内容以粘贴素材,并写入 UI 设计 JSON。", + "description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件。", "windows": ["client"], "permissions": [ "clipboard-manager:allow-read-image", "clipboard-manager:allow-read-text", - "clipboard-manager:allow-write-text", "core:image:allow-rgba", "core:image:allow-size", "core:resources:allow-close", diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx index 3228c338f..31ceb4524 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx @@ -1,6 +1,5 @@ -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { ChevronLeft } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { ThemedModal } from '../../components/modal/ThemedModal'; import { @@ -49,29 +48,10 @@ export default function UiEditorPage({ const session = useUiEditorSession(projectPath, resourceId, resolvedStore); const [saveWarningOpen, setSaveWarningOpen] = useState(false); const [saveAfterReturn, setSaveAfterReturn] = useState(false); - const [saveAndCopyJson, setSaveAndCopyJson] = useState(false); - const [copyFeedback, setCopyFeedback] = useState<'copied' | 'failed' | null>( - null, - ); const [returnConfirmOpen, setReturnConfirmOpen] = useState(false); - useEffect(() => { - if (copyFeedback === null) return; - const timeoutId = window.setTimeout(() => setCopyFeedback(null), 2000); - return () => window.clearTimeout(timeoutId); - }, [copyFeedback]); - - async function save(afterReturn = false, copyJson = false) { - const savedSnapshot = await session.save.save(); - if (savedSnapshot) { - if (copyJson) { - try { - await writeText(JSON.stringify(savedSnapshot, null, 2)); - setCopyFeedback('copied'); - } catch { - setCopyFeedback('failed'); - } - } + async function save(afterReturn = false) { + if (await session.save.save()) { if (afterReturn) { setReturnConfirmOpen(false); onBack?.(); @@ -79,17 +59,16 @@ export default function UiEditorPage({ } } - function requestSave(afterReturn = false, copyJson = false) { + function requestSave(afterReturn = false) { if (!resourceId || session.save.isSaving || session.workflow.isAiRunning) { return; } setSaveAfterReturn(afterReturn); - setSaveAndCopyJson(copyJson); if (session.save.hasWarnings()) { setSaveWarningOpen(true); return; } - void save(afterReturn, copyJson); + void save(afterReturn); } function requestBack() { @@ -113,40 +92,20 @@ export default function UiEditorPage({ 返回资源 {resourceLabel ?? 'UI 设计'} -
- - -
+ ) : null} {session.save.saveError ? ( @@ -157,14 +116,6 @@ export default function UiEditorPage({ {session.save.saveError} ) : null} - {copyFeedback === 'failed' ? ( -
- 保存成功,但复制失败。 -
- ) : null} { setSaveWarningOpen(false); - void save(saveAfterReturn, saveAndCopyJson); + void save(saveAfterReturn); }} > 仍然保存 diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts index 60edee2c5..d58bcf6df 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts @@ -1000,7 +1000,7 @@ export function useUiEditorSession( persistedRevision === null || editor.isLocked ) { - return null; + return false; } setSaveError(null); setIsSaving(true); @@ -1014,16 +1014,16 @@ export function useUiEditorSession( ); if (result.status === 'conflict') { setSaveError('资源已在别处更新;请重新加载后再保存。'); - return null; + return false; } setPersistedRevision(result.revision); if (JSON.stringify(editor.state) === snapshotSignature) { setSavedStateSignature(snapshotSignature); } - return snapshot; + return true; } catch (error) { setSaveError(error instanceof Error ? error.message : String(error)); - return null; + return false; } finally { setIsSaving(false); } diff --git a/apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts b/apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts index b525221bc..51c4c8558 100644 --- a/apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts +++ b/apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts @@ -5,5 +5,3 @@ export async function readImage(): Promise { export async function readText(): Promise { return ''; } - -export async function writeText(_text: string): Promise {} diff --git a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts index 6395ad68f..07f4f7606 100644 --- a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts +++ b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts @@ -13,10 +13,6 @@ import { describe, expect, it, vi } from 'vitest'; vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() })); -vi.mock('@tauri-apps/plugin-clipboard-manager', () => ({ - writeText: vi.fn(), -})); - vi.mock('../src/components/AssetImporter', () => ({ AssetImporter: ({ open }: { open: boolean }) => open ? createElement('div', { role: 'dialog' }, '素材导入器') : null, @@ -28,7 +24,6 @@ vi.mock('../src/components/modal/ThemedModal', () => ({ })); import { invoke } from '@tauri-apps/api/core'; -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import type { Node as UiNode } from '../src/features/ui-editor/types/Node'; import type { State } from '../src/features/ui-editor/types/State'; @@ -211,95 +206,6 @@ describe('UiEditorPage', () => { }); }); - it('saves the warning-confirmed snapshot before copying its formatted JSON', async () => { - vi.mocked(writeText).mockClear(); - const state = structuredClone(EMPTY_SNAPSHOT.state); - const stateStore: UiDesignStateStore = { - load: vi.fn().mockResolvedValue({ revision: 0, state }), - save: vi.fn().mockResolvedValue({ - status: 'saved', - state, - revision: 1, - committedProjectRevision: 1, - }), - }; - vi.mocked(writeText).mockResolvedValue(); - render( - createElement(UiEditorPage, { - projectPath: '/tmp/ui-editor', - resourceId: 'ui-resource', - stateStore, - }), - ); - - fireEvent.click( - await screen.findByRole('button', { name: '保存并复制 JSON 结构' }), - ); - fireEvent.click(await screen.findByRole('button', { name: '仍然保存' })); - - await waitFor(() => { - expect(stateStore.save).toHaveBeenCalledWith('ui-resource', 0, state); - expect(writeText).toHaveBeenCalledWith(JSON.stringify(state, null, 2)); - }); - expect(screen.getByRole('button', { name: '已保存并复制' })).toBeTruthy(); - }); - - it('does not copy JSON when saving fails', async () => { - vi.mocked(writeText).mockClear(); - const stateStore: UiDesignStateStore = { - load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)), - save: vi.fn().mockRejectedValue(new Error('临时存储不可用')), - }; - render( - createElement(UiEditorPage, { - projectPath: '/tmp/ui-editor', - resourceId: 'ui-resource', - stateStore, - }), - ); - - fireEvent.click( - await screen.findByRole('button', { name: '保存并复制 JSON 结构' }), - ); - fireEvent.click(await screen.findByRole('button', { name: '仍然保存' })); - - await screen.findByRole('alert'); - expect(writeText).not.toHaveBeenCalled(); - }); - - it('keeps a successful save when JSON copying fails', async () => { - vi.mocked(writeText).mockClear(); - const state = structuredClone(EMPTY_SNAPSHOT.state); - const stateStore: UiDesignStateStore = { - load: vi.fn().mockResolvedValue({ revision: 0, state }), - save: vi.fn().mockResolvedValue({ - status: 'saved', - state, - revision: 1, - committedProjectRevision: 1, - }), - }; - vi.mocked(writeText).mockRejectedValue(new Error('剪贴板不可用')); - render( - createElement(UiEditorPage, { - projectPath: '/tmp/ui-editor', - resourceId: 'ui-resource', - stateStore, - }), - ); - - fireEvent.click( - await screen.findByRole('button', { name: '保存并复制 JSON 结构' }), - ); - fireEvent.click(await screen.findByRole('button', { name: '仍然保存' })); - - await waitFor(() => expect(stateStore.save).toHaveBeenCalledTimes(1)); - expect((await screen.findByRole('status')).textContent).toContain( - '保存成功,但复制失败。', - ); - expect(screen.queryByRole('alert')).toBeNull(); - }); - it('renders split input tools over a real empty State', () => { render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' })); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index f5b84297d..55cafc47b 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1163,7 +1163,7 @@ game-project/ - UI 编辑器复用现有 manifest `kind: "UI"`、`mediaType: "application/json"` 资源,不增加平行 asset kind。资源文件固定为严格 `game-creator-ui-design-state.v1` JSON envelope:`projectId`、`assetId`、每资源 `revision` 和 Rust 唯一源 `State`;旧空对象、未知字段、身份错配、超限、无效内部引用和不安全相对路径均失败关闭。内部引用校验同时覆盖 `Image.target_graphic -> sprite_assets` 与 `Text.font -> font_assets`,可选引用非空时必须命中同一 State 内已登记资源。 - Tauri 专用 load/save command 只接受项目路径、期望项目 ID、manifest asset ID 和(保存时)资源 revision;Rust 按 manifest 解析受控本地路径并在项目写锁内做 CAS。相同 `State` 返回 unchanged 且不推进 project revision;不同内容安装并回读一致后才推进 revision,后续推进失败返回 `reconciliation-required`,不伪装为完整保存。 - UI State 原子安装保留最近一个可解析、canonical 的 `.previous` 恢复候选,作为最佳努力恢复来源;写入主文件前不把完整 State 语义校验重复执行一遍。主文件损坏时,恢复候选仍必须通过同一严格 schema、project/asset identity、revision、引用和 State 校验后才能安装;恢复安装与保存共用项目写锁,并在持锁后重新读取主文件,已有并发保存的有效新版本时直接返回而不安装旧副本。任一候选均不可信则停在加载错误,前端禁编辑和保存。新建 UI 资源先登记并安装合法 envelope,任一步失败补偿 manifest/文件,避免把空 JSON 留给资源卡。 -- 图片路径只需是安全项目相对路径,不要求外部图片仍存在或已登记为 manifest asset;缺失媒体只导致 preview 占位。`imageOrder`、当前选择、缩放、面板开关和 preview URL 不写入 State,加载后由 State 派生。保存冻结提交快照,保存期间的新编辑继续保持 dirty;AI state lock 和加载期间禁保存。资源编辑页的“保存并复制 JSON 结构”仅在同一冻结快照成功保存后,通过 Tauri 原生剪贴板复制 2 空格缩进的纯 `State` JSON;复制失败不回滚保存,并明确提示保存成功、复制失败。 +- 图片路径只需是安全项目相对路径,不要求外部图片仍存在或已登记为 manifest asset;缺失媒体只导致 preview 占位。`imageOrder`、当前选择、缩放、面板开关和 preview URL 不写入 State,加载后由 State 派生。保存冻结提交快照,保存期间的新编辑继续保持 dirty;AI state lock 和加载期间禁保存。 ## 2026-08-18 UI Editor 新建节点组件状态