陶泥儿移除发行包下载功能

- 删除左半面板的「下载发行包」按钮与下载失败弹窗接线
- 退役 useTaonierExport 的 artifactReady 状态及其构建收尾写入
- 删除 tabs/taonier/DownloadFailureDialog.tsx
- 同步陶泥儿面板测试与实施计划文档
This commit is contained in:
2026-10-07 14:34:04 +08:00
parent 0119a329aa
commit 61acaf6cea
5 changed files with 45 additions and 170 deletions
@@ -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,
@@ -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<TaonierDownloadFailure | null>(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}
</PaneButton>
{/* 下载入口只在打包成功之后出现:宿主不报告磁盘上有没有产物,没打过就不给一个下不了的按钮。 */}
{taonier.artifactReady ? (
<PaneButton
ariaLabel="下载发行包"
className="justify-center"
onClick={downloadArtifact}
>
下载发行包
</PaneButton>
) : null}
{publish.result ? (
<p
role="status"
@@ -247,13 +179,6 @@ export function ArtifactsPane({
onRepair={taonier.repair}
onDismiss={taonier.dismissFailure}
/>
<DownloadFailureDialog
failure={downloadFailure}
revealFailure={revealFailure}
onReveal={revealDownloadedFile}
onDismiss={dismissDownloadFailure}
/>
</div>
);
}
@@ -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 (
<ThemedModal
open={failure !== null}
ariaLabel="下载失败"
onClose={onDismiss}
closeOnBackdrop={false}
closeOnEscape={false}
panelClassName="w-full max-w-md rounded-2xl border border-(--platform-surface-border) p-4"
>
<p className="text-sm leading-relaxed text-(--platform-text-strong)">
{failure ? '没能把发行包保存到你选的位置。' : null}
</p>
{failure ? (
<p className="mt-2 text-xs leading-relaxed text-(--platform-text-base)">
{failure.reason}
</p>
) : null}
<p className="mt-2 text-xs leading-relaxed text-(--platform-text-base)">
也可以让系统在文件管理器里打开它原来的位置,自己复制过去。
</p>
{revealFailure ? (
<p role="status" className="mt-2 text-xs text-(--platform-warm-text)">
{revealFailure}
</p>
) : null}
<div className="mt-4 flex flex-wrap justify-end gap-2">
<PaneButton onClick={onDismiss}>关闭</PaneButton>
<PaneButton variant="primary" onClick={onReveal}>
在文件管理器中打开
</PaneButton>
</div>
</ThemedModal>
);
}
@@ -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',
@@ -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 发布媒体**