From ab2af60d3051007304b514ff1868b356c8408cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 7 Oct 2026 11:41:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=94=A8=20packageAll=20?= =?UTF-8?q?=E8=90=BD=E5=AE=9A=E7=9A=84=E6=9D=83=E5=A8=81=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E5=90=88=E6=88=90=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useTaonierExport 维护 effectiveFormRef,refresh 采纳新表单与打包前 flush 成功时同步 - 暴露 getEffectiveForm;publishGame 在 await packageAll() 之后用它,避免旧快照与宿主读盘不一致 - 补两条单测:采纳新表单后权威表单更新、未保存输入先落盘再作为权威表单 --- .../export/state/useTaonierExport.ts | 14 ++++ .../export/state/useTaonierTab.ts | 5 +- .../tests/taonierExport.test.tsx | 68 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierExport.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierExport.ts index 1a64338fd..bf035e39a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierExport.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierExport.ts @@ -261,6 +261,14 @@ export function useTaonierExport({ /** 上一次抛进错误池的故障指纹:同一个只抛一次,见 [useTaonierExport.throwToErrorPool]。 */ const reportedFailureRef = useRef(''); const formRef = useRef(null); + /** + * 已经落定到磁盘的权威表单:`refresh` 采纳新内容、`packageAll` flush 成功后同步。 + * + * 发布链路在 `await packageAll()` 之后必须用它合成 metadata:`packageAll` 内部的 `refresh` + * 可能在用户无未保存输入时采纳一份更新的注册表表单,而宿主发布命令读的正是这份磁盘表单; + * 继续用 await 之前的快照会让文字字段与媒体字节对不上。 + */ + const effectiveFormRef = useRef(null); /** * 当前项目路径的即时值。异步结果(读内容、取指纹、保存、构建、读线上值)回来时用它判断结果还 * 属不属于当前项目:面板根跨项目不卸载,旧请求回来时绝不能把上一个项目的表单 / 基线 / 线上值写 @@ -309,6 +317,7 @@ export function useTaonierExport({ initialReadSettledRef.current = false; reportedFailureRef.current = ''; formRef.current = null; + effectiveFormRef.current = null; }, [projectPath]); // 脚本出现之后就把提示重新武装:它要是又被删掉,用户该再看到一次阻断提示。 @@ -379,6 +388,7 @@ export function useTaonierExport({ return content.hasScript; } setForm(content.form); + effectiveFormRef.current = content.form; setFieldErrors([]); setConflict(null); // 这里不再清 `failure`:上面的函数式清理只清读来源的失败,构建类失败是那次构建的现场, @@ -766,6 +776,8 @@ export function useTaonierExport({ if (dirty && form && !conflict && fieldErrors.length === 0) { const flushed = await persist(form, baselineRef.current); if (!flushed) return false; + // flush 成功后磁盘上的就是这份;发布链路要用它而不是调用前的旧快照。 + effectiveFormRef.current = form; } // 没解决的冲突 / 字段红字:盘上的内容和用户看到的不一致,先别打包。 if (conflict || fieldErrors.length > 0) return false; @@ -907,6 +919,8 @@ export function useTaonierExport({ adoptOnlineText, resolveConflict, packageAll, + /** `await packageAll()` 之后取「这次真正落定的表单」;还没落定时返回 `null`。 */ + getEffectiveForm: () => effectiveFormRef.current, adapt, repair, fillForm, diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts index 7f5151002..c0d6e79e3 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts @@ -235,7 +235,10 @@ export function useTaonierTab({ // 订阅失败只影响阶段条:发布本身照常进行,按钮上仍显示「正在发布…」。 } try { - const metadata = buildTaonierPublishMetadata(form, online); + // `packageAll` 内部可能采纳一份更新的注册表表单(并已落盘),宿主发布命令读的正是那份; + // 拿它合成 metadata,避免文字字段来自 await 之前的旧快照、与媒体字节对不上。 + const effectiveForm = taonier.getEffectiveForm() ?? form; + const metadata = buildTaonierPublishMetadata(effectiveForm, online); const next = await publishLocalProjectGame({ invoke, projectPath, diff --git a/apps/ai-game-creator-shell/tests/taonierExport.test.tsx b/apps/ai-game-creator-shell/tests/taonierExport.test.tsx index 11396d739..6a0d83fae 100644 --- a/apps/ai-game-creator-shell/tests/taonierExport.test.tsx +++ b/apps/ai-game-creator-shell/tests/taonierExport.test.tsx @@ -511,6 +511,74 @@ describe('hook 状态', () => { expect(result.current.failure?.type).toBe('commandFailed'); }); + it('packageAll 采纳新表单后,发布取到的权威表单跟着更新', async () => { + let registryForm: TaonierExportForm = FORM; + const invoke = vi.fn(async (command: string) => { + switch (command) { + case 'read_taonier_export': + return { form: registryForm, hasScript: true }; + case 'read_taonier_export_hash': + return CONTENT_HASH; + case 'read_game_distribution_publication': + return ONLINE_FIRST_PUBLISH; + case 'save_taonier_export_form': + return 'hash-2'; + case 'run_taonier_export_build': + return { outputTail: '', omittedCharacters: 0 }; + default: + return undefined; + } + }); + installTauriRuntime({ core: { invoke } }); + const { result } = renderHook(() => + useTaonierExport({ + enabled: true, + projectPath: PROJECT_PATH, + submitTurn: vi.fn(async () => true), + }), + ); + await waitFor(() => expect(result.current.form).toEqual(FORM)); + + // 面板背后注册表被改(例如 agent 重写 .export/taonier.json)。用户没有未保存输入时, + // packageAll 的 refresh 会采纳这份新表单;发布必须拿同一份合成 metadata,否则文字字段 + // 会停在 await 之前的旧快照,与宿主读盘拿到的媒体字节对不上。 + registryForm = { ...FORM, title: '项目里的新名字' }; + await act(async () => { + await result.current.packageAll(); + }); + + expect(result.current.form?.title).toBe('项目里的新名字'); + expect(result.current.getEffectiveForm()?.title).toBe('项目里的新名字'); + }); + + it('有未保存输入时 packageAll 先落盘,权威表单就是这份输入', async () => { + const invoke = createInvoke(); + installTauriRuntime({ core: { invoke } }); + const { result } = renderHook(() => + useTaonierExport({ + enabled: true, + projectPath: PROJECT_PATH, + submitTurn: vi.fn(async () => true), + }), + ); + await waitFor(() => expect(result.current.form).toEqual(FORM)); + + await act(async () => { + result.current.updateField('title', '刚敲的名字'); + }); + await act(async () => { + await result.current.packageAll(); + }); + + expect(invoke).toHaveBeenCalledWith( + 'save_taonier_export_form', + expect.objectContaining({ + form: expect.objectContaining({ title: '刚敲的名字' }), + }), + ); + expect(result.current.getEffectiveForm()?.title).toBe('刚敲的名字'); + }); + it('校验不过时停在表单:不落盘、不跑构建(校验不过不触宿主)', async () => { const invoke = createInvoke(); installTauriRuntime({ core: { invoke } });