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 b27c7f41b..f17a752c2 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 @@ -229,14 +229,6 @@ export function useTaonierExport({ */ const [readSettled, setReadSettled] = useState(false); const [running, setRunning] = useState(false); - /** - * 这次会话里是否已经成功打出过分区:只有它为真,面板才给「下载产物」。 - * - * 宿主不报告磁盘上有没有产物,所以初值只能是 `false`(哪怕上一轮会话刚打过);成功构建置真、 - * 构建失败置假——上次成功的那份此刻可能已经被失败的那次覆盖或删掉,不给一个下不了的下载按钮。 - * 用户改表单不影响它:磁盘上那份仍是可下载的,只是内容旧了。 - */ - const [artifactReady, setArtifactReady] = useState(false); const [dirty, setDirty] = useState(false); /** * 「还没适配」的阻断提示被用户处理过一次就收起:无论选了「发给 code agent」还是「我自己来」, @@ -321,7 +313,6 @@ export function useTaonierExport({ setLoading(false); setReadSettled(false); setRunning(false); - setArtifactReady(false); setDirty(false); setAdaptPromptDismissed(false); setOnline(null); @@ -775,14 +766,12 @@ export function useTaonierExport({ setFailure(null); try { await runTaonierExportBuild(invoke, projectPath); - // 构建期间换了项目:这次产物属于上一个项目,不能让新项目出现可下载的按钮。 + // 构建期间换了项目:这次产物属于上一个项目,不能拿来给新项目发布。 if (projectPathRef.current !== projectPath) return false; - setArtifactReady(true); return true; } catch (error) { // 旧项目的构建失败不该记到新项目头上。 if (projectPathRef.current !== projectPath) return false; - setArtifactReady(false); const structured = asTaonierBuildFailure(error); if (!structured) { throwToErrorPool(String(error), error); @@ -955,7 +944,6 @@ export function useTaonierExport({ loading, readSettled, running, - artifactReady, adaptPromptDismissed, online, onlineState, diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/ArtifactsPane.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/ArtifactsPane.tsx index 9f91b7007..69d95fc86 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/ArtifactsPane.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/ArtifactsPane.tsx @@ -1,19 +1,10 @@ -import { type ReactNode, useState } from 'react'; +import type { ReactNode } from 'react'; import type { GameDistributionPublishResult } from '../../../../../services/gameDistributionPublish'; -import { - revealExportArtifactInManager, - saveExportArtifactToDisk, -} from '../../state/exportArtifactDownload'; -import { TAONIER_ARTIFACT_RELATIVE_PATH } from '../../state/taonierInstruction'; import type { TaonierExportState } from '../../state/useTaonierExport'; import { PaneButton } from '../common/PaneButton'; import { AdaptPromptDialog } from './AdaptPromptDialog'; import { ConflictCard } from './ConflictCard'; -import { - DownloadFailureDialog, - type TaonierDownloadFailure, -} from './DownloadFailureDialog'; import { FailureDialog } from './FailureDialog'; import { FormCard } from './FormCard'; @@ -27,10 +18,10 @@ export type TaonierPublishPanel = { /** * 陶泥儿工作台左半:发布表单(线上值跟着每个字段走)+ 唯一的「打包并发布」。 * - * 没有单独的「打包」步骤:打包是发布的第一步,由 `publishGame` 内部跑 `packageAll()`。用户填完表 - * 单点一次,就是「打包 + 提交游戏分发」。打包成功后给「下载发行包」——没打过就不给一个下不了的 - * 按钮。适配入口不在按钮行里——未配置好导出时由阻断弹窗(`AdaptPromptDialog`)给出「让陶泥儿帮我 - * 调通 / 我自己来」两个选择,失败现场由失败弹窗兜底。 + * 没有单独的「打包」步骤,也没有「下载发行包」这类本地产物出口:打包是发布的第一步,由 + * `publishGame` 内部跑 `packageAll()`,用户填完表单点一次,就是「打包 + 提交游戏分发」。适配入口 + * 不在按钮行里——未配置好导出时由阻断弹窗(`AdaptPromptDialog`)给出「让陶泥儿帮我调通 / 我自己 + * 来」两个选择,失败现场由失败弹窗兜底。 * * 线上值(`read_game_distribution_publication` 的只读投影)在 `FormCard` 里逐字段内联展示,草稿不 * 自动回填、不被覆盖;「沿用」是用户显式动作。发布入口按线上状态给「打包并发布 / 打包并更新」, @@ -48,14 +39,6 @@ export function ArtifactsPane({ publish: TaonierPublishPanel; }) { const { form, conflict, failure, online, onlineState } = taonier; - /** - * 下载失败现场:宿主拒绝写盘或对话框打不开时,用弹窗把「哪一份、为什么、下一步」一次说清, - * 并给出「在文件管理器中打开原件」这条兜底出口。 - */ - const [downloadFailure, setDownloadFailure] = - useState(null); - /** 「在文件管理器中打开」自身失败时的一句话;没失败时为空。 */ - const [revealFailure, setRevealFailure] = useState(''); /** * 未适配:`hasScript` 的初值是 `false`,注册表还没读回来(或读失败)时我们不能替项目下 * 「还没适配」的结论——那种情况只该显示读取中或失败现场。 @@ -73,47 +56,6 @@ export function ArtifactsPane({ */ const showAdaptPrompt = needsAdaptation && !taonier.adaptPromptDismissed; - const downloadArtifact = (): void => { - if (!projectPath) return; - const relativePath = TAONIER_ARTIFACT_RELATIVE_PATH; - void saveExportArtifactToDisk({ - projectPath, - relativePath, - fallbackFileName: 'taonier.zip', - title: '保存发行包', - }).then((result) => { - if (result.status !== 'failed') return; - setRevealFailure(''); - setDownloadFailure({ - kind: 'artifact', - relativePath, - reason: result.reason, - }); - }); - }; - - /** 兜底出口:让系统文件管理器定位原件;打不开就把原因留在弹窗里。 */ - const revealDownloadedFile = (): void => { - if (!projectPath || !downloadFailure) return; - void revealExportArtifactInManager({ - projectPath, - relativePath: downloadFailure.relativePath, - }).then((result) => { - if (result.status === 'failed') { - setRevealFailure(result.reason); - return; - } - // 文件管理器已经弹出:收起弹窗,别挡着用户去看。 - setDownloadFailure(null); - setRevealFailure(''); - }); - }; - - const dismissDownloadFailure = (): void => { - setDownloadFailure(null); - setRevealFailure(''); - }; - const publishDisabled = taonier.running || publish.publishing || @@ -212,16 +154,6 @@ export function ArtifactsPane({ > {publishLabel} - {/* 下载入口只在打包成功之后出现:宿主不报告磁盘上有没有产物,没打过就不给一个下不了的按钮。 */} - {taonier.artifactReady ? ( - - 下载发行包 - - ) : null} {publish.result ? (

- - ); } diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/DownloadFailureDialog.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/DownloadFailureDialog.tsx deleted file mode 100644 index d2599e3a1..000000000 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/taonier/DownloadFailureDialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { ThemedModal } from '../../../../../components/modal/ThemedModal'; -import { PaneButton } from '../common/PaneButton'; - -/** 哪一份产物没存下来。当前只有发行包一份,留成对象是为了以后加封面之类时有位置放。 */ -export type TaonierDownloadFailure = { - kind: 'artifact'; - /** 项目内相对路径:用户要在文件管理器里打开的就是它。 */ - relativePath: string; - /** 保存失败的原因(宿主给的中文说明,或一句通用说明)。 */ - reason: string; -}; - -/** - * 下载失败弹窗:一句「哪一份没存下来」+ 原因 + 兜底出口。 - * - * 保存失败不再只留一行会被滚出视野的小字:左半本来就矮,而这是用户点过下载之后必须看见的结果。 - * 弹窗给两条路:关掉,或者让系统文件管理器打开原件所在位置自己复制——宿主拒绝写盘时(权限、 - * 目标不存在、路径被门禁拦下),用户至少还能把项目里那份原件拿出去。 - * - * 与失败弹窗(`FailureDialog`)同一种阻断层:背景点击与 Esc 不生效。宿主的原始错误不在这里展开, - * `reason` 只用调用方收好的那一句人话。 - */ -export function DownloadFailureDialog({ - failure, - revealFailure, - onReveal, - onDismiss, -}: { - /** 没有下载失败现场时为 `null`,弹窗关着。 */ - failure: TaonierDownloadFailure | null; - /** 「在文件管理器中打开」自身失败时的原因;空串表示没有。 */ - revealFailure: string; - onReveal: () => void; - onDismiss: () => void; -}) { - return ( - -

- {failure ? '没能把发行包保存到你选的位置。' : null} -

- {failure ? ( -

- {failure.reason} -

- ) : null} -

- 也可以让系统在文件管理器里打开它原来的位置,自己复制过去。 -

- {revealFailure ? ( -

- {revealFailure} -

- ) : null} -
- 关闭 - - 在文件管理器中打开 - -
- - ); -} diff --git a/apps/ai-game-creator-shell/tests/taonierExport.test.tsx b/apps/ai-game-creator-shell/tests/taonierExport.test.tsx index 04b61c970..1106ad2c6 100644 --- a/apps/ai-game-creator-shell/tests/taonierExport.test.tsx +++ b/apps/ai-game-creator-shell/tests/taonierExport.test.tsx @@ -273,6 +273,12 @@ describe('交给 code agent 的指令', () => { path: TAONIER_ARTIFACT_RELATIVE_PATH, }), ).toBe('导出跑完了,但没有生成代码包。可以让陶泥儿看看。'); + expect( + describeTaonierFailure({ + type: 'projectUnavailable', + cause: 'package.json 中的 build:taonier 脚本已变化', + }), + ).toBe('这个项目的导出配置对不上,需要先按提示调整项目后再试。'); }); it('分类的判据是要改的东西在不在项目代码里', () => { @@ -309,6 +315,12 @@ describe('交给 code agent 的指令', () => { cause: '项目根不可用:找不到目录', }), ).toBe(TaonierFailureRepairability.OnlyHumanRepairable); + expect( + classifyTaonierFailure({ + type: 'projectUnavailable', + cause: 'package.json 中的 build:taonier 脚本已变化', + }), + ).toBe(TaonierFailureRepairability.CodeAgentRepairable); }); it('字段红字交给 agent 的指令带上红字原话、当前值与落点', () => { @@ -419,6 +431,10 @@ describe('命令失败的载体', () => { }, { type: 'commandDenied', commandId: 'command.exec' }, { type: 'artifactMissing', path: TAONIER_ARTIFACT_RELATIVE_PATH }, + { + type: 'projectUnavailable', + cause: 'package.json 中的 build:taonier 脚本已变化', + }, { type: 'exportUnavailable', cause: '项目根不可用' }, ]; for (const payload of buildCases) { @@ -911,7 +927,7 @@ describe('hook 状态', () => { }); describe('面板左半', () => { - it('只有一颗「打包并发布」入口,发行包只在打包成功后给下载入口', () => { + it('只有一颗「打包并发布」入口,没有打包或下载产物这类旁路', () => { const publish = vi.fn(); renderPane(fakeTaonier({ online: ONLINE_FIRST_PUBLISH_VALUES }), { publishing: false, @@ -920,13 +936,10 @@ describe('面板左半', () => { }); // 没有单独的「打包」步骤:打包是发布的第一步,由发布入口内部跑。 expect(screen.queryByRole('button', { name: '打包' })).toBeNull(); + // 没有本地产物出口:产物只服务发布,不给用户单独下载。 + expect(screen.queryByRole('button', { name: '下载发行包' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '打包并发布' })); expect(publish).toHaveBeenCalledTimes(1); - expect(screen.queryByRole('button', { name: '下载发行包' })).toBeNull(); - - cleanup(); - renderPane(fakeTaonier({ artifactReady: true })); - screen.getByRole('button', { name: '下载发行包' }); }); it('线上值读取失败时不留手动入口,提示正在自动重试', () => { @@ -1014,6 +1027,25 @@ describe('面板左半', () => { ).toBeTruthy(); // 要改的东西不在项目代码里,不给陶泥儿入口。 expect(screen.queryByRole('button', { name: '让陶泥儿来修' })).toBeNull(); + + // 项目侧预检没过(脚本漂移等):要改的东西在项目代码里,仍给陶泥儿入口。 + cleanup(); + renderPane( + fakeTaonier({ + failure: { + type: 'projectUnavailable', + cause: 'package.json 中的 build:taonier 脚本已变化', + } as TaonierCardFailure, + repair, + }), + ); + expect( + screen.getByText( + '这个项目的导出配置对不上,需要先按提示调整项目后再试。', + ), + ).toBeTruthy(); + fireEvent.click(screen.getByRole('button', { name: '让陶泥儿来修' })); + expect(repair).toHaveBeenCalledTimes(2); }); it('字段被限制挡下时给出「交给陶泥儿」', () => { @@ -1100,7 +1132,6 @@ function fakeTaonier( loading: false, readSettled: true, running: false, - artifactReady: false, adaptPromptDismissed: false, online: null, onlineState: 'ready', diff --git a/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md b/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md index f38c5c05e..aeb679c08 100644 --- a/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md +++ b/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md @@ -189,7 +189,7 @@ - `export/state/useTaonierExport.ts`:自动保存(baseHash)、2s 轮询、冲突、失败、`packageAll`、`adapt`、`repair`、`fillForm`、`handFieldErrorsToAgent`、`online` 与「沿用线上值」动作;返回面与 xhs hook 同构。 **Step 3 UI** -- `tabs/taonier/`:`Page.tsx`(2 列)、`ArtifactsPane.tsx`(表单槽/读取中/重试/打包/下载产物/冲突/线上值卡/发布链接/三个 dialog)、`FormCard.tsx`(逐字段红字 + 帮我填 + 交给陶泥儿)、`OnlineValuesCard.tsx`(线上值只读 + 沿用操作)、`ConflictCard.tsx`、`FailureDialog.tsx`、`AdaptPromptDialog.tsx`、`DownloadFailureDialog.tsx`。 +- `tabs/taonier/`:`Page.tsx`(2 列)、`ArtifactsPane.tsx`(表单槽/读取中/重试/冲突/唯一的「打包并发布」入口/两个 dialog)、`FormCard.tsx`(逐字段红字 + 帮我填 + 交给陶泥儿 + 线上值逐字段内联沿用)、`ConflictCard.tsx`、`FailureDialog.tsx`、`AdaptPromptDialog.tsx`。 - 复用 `common/PaneButton`、`ThemedModal`,去掉旧 `PublishPane` 对 `GameDistributionPublishFormView` 的依赖。 **Step 4 发布媒体**