From 9d3018a49c0dd170311e7db0976455214bd3eccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 5 Oct 2026 17:54:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E9=80=82=E9=85=8D=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=87=AA=E5=B8=A6=E5=85=A5=E9=98=9F=E6=8C=89=E9=92=AE=EF=BC=8C?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=8D=A1=E7=89=87=E6=8C=89=E5=8F=98=E4=BD=93?= =?UTF-8?q?=E5=88=86=E6=B4=BE=E4=B8=89=E9=A2=97=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 未适配提示从一句话改成一张卡片:说明和「交给 code agent 适配」那颗按钮放在一起,用户不用按照提示去别处找按钮;按钮行里就不再重复一颗,同一屏同一件事只有一个入口。 - 这条提示的判据补上 form 与 failure:hasScript 初值是 false,注册表还没读回来(或读失败)时不能替项目下「还没适配」的结论;失败卡片带着宿主的现场和同一颗按钮,两张卡一起出现就是同一件事两个入口。 - 失败变体的 action 从两向扩成三向 adapt / repair / retry:buildScriptMissing 归 adapt(首次适配该发适配指令,原来发修复指令会让 agent 去读一份还不存在的适配说明),commandFailed / artifactMissing / registryMalformed 归 repair,其余仍是 retry。 - 前端用例 12 → 14:钉住「未适配时按钮行里没有适配按钮、卡片那颗只调 adapt」「读回来之前不下结论、读失败时保留按钮行的适配入口」,并把失败卡片用例扩成适配 / 修 / 重试三种变体。 --- .../export/state/xhsMinitoolFailure.ts | 15 ++- .../export/tabs/xiaohongshu/ArtifactsPane.tsx | 46 ++++++--- .../export/tabs/xiaohongshu/FailureCard.tsx | 11 ++- .../tests/xhsMinitoolExport.test.tsx | 93 +++++++++++++++---- 4 files changed, 130 insertions(+), 35 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts index aa0764118..bf59f3f4f 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts @@ -8,8 +8,13 @@ import { expectNever } from '../../../../app/expectNever'; import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError'; import { XHS_MINITOOL_FIELD_LABELS } from './xhsMinitoolFields'; -/** 失败之后最合适的下一步:交给 code agent 修,还是原样重试。 */ -export type XhsMinitoolFailureAction = 'adapt' | 'retry'; +/** + * 失败之后最合适的下一步,也就是那颗按钮干什么: + * - `adapt`:项目还没适配(没有构建脚本),该入队的是首次适配指令; + * - `repair`:适配过了但这次没跑通(脚本非零退出、产物不在、注册表被改坏),该入队的是修复指令; + * - `retry`:不用 agent 掺和,用户自己改或原样重试。 + */ +export type XhsMinitoolFailureAction = 'adapt' | 'repair' | 'retry'; export type XhsMinitoolFailureNotice = { /** 一句话说清哪儿不对。 */ @@ -27,7 +32,7 @@ export function describeXhsMinitoolFailure( return { message: '导出注册表被改成了宿主认不出的形状,宿主拒绝猜测它的内容。', detail: error.cause, - action: 'adapt', + action: 'repair', }; case 'formInvalid': return { @@ -54,13 +59,13 @@ export function describeXhsMinitoolFailure( ? '构建脚本没能启动(沙箱 / 运行环境层面就失败了)。' : `构建脚本没有正常退出(exit code ${error.exitCode})。`, detail: error.outputTail, - action: 'adapt', + action: 'repair', }; case 'artifactMissing': return { message: `构建脚本跑完了,但 ${error.path} 不在。多数是把 zip 写到了别处。`, detail: '', - action: 'adapt', + action: 'repair', }; case 'exportUnavailable': return { diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx index f60c33062..ac91fe6c4 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx @@ -28,6 +28,19 @@ export function ArtifactsPane({ }) { const [downloadNotice, setDownloadNotice] = useState(''); const { form, conflict } = minitool; + /** + * 未适配:那颗按钮搬进提示卡片,说明和入口放一起,用户不用在别处找。 + * + * 判据带上 `form`:`hasScript` 的初值是 `false`,注册表还没读回来(或读失败)时我们不能替项目 + * 下「还没适配」的结论——那种情况只该显示读取中或失败现场。 + * + * 判据带上 `failure`:失败卡片带着宿主的现场和同一颗按钮,两张卡一起出现就是同一件事两个入口。 + */ + const missingScript = + !minitool.loading && + form !== null && + !minitool.hasScript && + minitool.failure === null; const downloadIcon = (): void => { if (!projectPath || !form) return; @@ -55,19 +68,21 @@ export function ArtifactsPane({ aria-label="导出内容区" >
- - 适配 - + {missingScript ? null : ( + + 适配 + + )} void minitool.runExport()} > @@ -84,10 +99,18 @@ export function ArtifactsPane({
- {!minitool.hasScript ? ( -

- {`项目里还没有 ${XHS_MINITOOL_SCRIPT_NAME} 脚本。点「适配」把这项工作交给 code agent:它会在项目里实验出能产出平台制品的脚本,并把这套做法固化进项目。`} -

+ {missingScript ? ( +
+

+ {`项目里还没有 \`${XHS_MINITOOL_SCRIPT_NAME}\` 脚本——这就是「还没适配」的判据。这件事只能由 code agent 在项目里实验完成(用哪版 vite 配置、入口在哪、怎么调打包脚本都由它定):`} +

+ + 交给 code agent 适配 + +
) : null} {form ? ( @@ -137,6 +160,7 @@ export function ArtifactsPane({ {minitool.failure ? ( void minitool.runExport()} /> diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FailureCard.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FailureCard.tsx index 16cfeb3c8..287f4db2e 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FailureCard.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FailureCard.tsx @@ -4,15 +4,18 @@ import { PaneButton } from './PaneButton'; /** * 失败卡片:宿主给的 typed 失败原文(有一句人话 + 现场),加一颗按变体选出来的按钮。 * - * 「让 code agent 修」不是万能按钮:只有脚本/产物/注册表这类**要改项目**的失败才走它; - * 宿主自身没跑起来(`exportUnavailable`)就只给重试,别把 agent 拉进来瞎猜。 + * 三颗按钮对应三种下一步:项目还没适配就入队首次适配指令,适配过但没跑通就入队修复指令, + * 宿主自身没跑起来(`exportUnavailable`)或用户能自己改的(表单、冲突)就只给重试——别把 + * agent 拉进来瞎猜。 */ export function FailureCard({ failure, + onAdapt, onRepair, onRetry, }: { failure: XhsMinitoolFailureState; + onAdapt: () => void; onRepair: () => void; onRetry: () => void; }) { @@ -31,6 +34,10 @@ export function FailureCard({ ) : null}
{notice.action === 'adapt' ? ( + + 交给 code agent 适配 + + ) : notice.action === 'repair' ? ( 让 code agent 修 diff --git a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx index 4c7877430..031bed7ee 100644 --- a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx +++ b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx @@ -112,7 +112,7 @@ describe('交给 code agent 的指令', () => { exitCode: 1, outputTail: 'vite build failed', }), - ).toMatchObject({ action: 'adapt', detail: 'vite build failed' }); + ).toMatchObject({ action: 'repair', detail: 'vite build failed' }); expect( describeXhsMinitoolFailure({ type: 'exportUnavailable', @@ -169,7 +169,7 @@ describe('面板左半', () => { }); }); - it('三颗按钮各管一件事,没有脚本时导出不可点', () => { + it('没有脚本时导出不可点,适配入口在提示卡片里且只有一颗', () => { const minitool = fakeMinitool({ hasScript: false }); renderPane(minitool); @@ -177,7 +177,11 @@ describe('面板左半', () => { 'disabled', true, ); - fireEvent.click(screen.getByRole('button', { name: '适配' })); + // 按钮行里的「适配」让位给卡片里那颗:同一件事不在同一屏出现两个入口。 + expect(screen.queryByRole('button', { name: '适配' })).toBeNull(); + fireEvent.click( + screen.getByRole('button', { name: '交给 code agent 适配' }), + ); expect(minitool.adapt).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByRole('button', { name: '刷新' })); @@ -188,6 +192,25 @@ describe('面板左半', () => { renderPane(withScript); fireEvent.click(screen.getByRole('button', { name: '导出' })); expect(withScript.runExport).toHaveBeenCalledTimes(1); + // 有脚本时那颗按钮回到按钮行,卡片不再出现。 + expect(screen.getByRole('button', { name: '适配' })).not.toBeNull(); + expect( + screen.queryByRole('button', { name: '交给 code agent 适配' }), + ).toBeNull(); + }); + + it('注册表还没读回来时不下「还没适配」的结论', () => { + renderPane(fakeMinitool({ form: null, loading: true, hasScript: false })); + + expect(screen.queryByText(/还没有 .build:xhs-minitool. 脚本/u)).toBeNull(); + expect(screen.getByText('正在读取导出注册表…')).not.toBeNull(); + }); + + it('读失败时保留按钮行里的适配入口,不摆未适配卡片', () => { + renderPane(fakeMinitool({ form: null, loading: false, hasScript: false })); + + expect(screen.queryByText(/还没有 .build:xhs-minitool. 脚本/u)).toBeNull(); + expect(screen.getByRole('button', { name: '适配' })).not.toBeNull(); }); it('冲突面板逐字段二选一,按选择保存的合并结果交回上层', () => { @@ -209,31 +232,67 @@ describe('面板左半', () => { }); }); - it('失败卡片按变体给出「让 code agent 修」或「重试」', () => { - const failure: XhsMinitoolFailureState = { - notice: { - message: - '项目里还没有 build:xhs-minitool 脚本——这就是「还没适配」的判据。', - detail: '宿主找过:package.json、game/package.json', - action: 'adapt', - }, - error: { + it('失败卡片按变体给出「让 code agent 适配 / 修」或「重试」', () => { + // 还没适配:入队的必须是首次适配指令,修复指令会引用一份还不存在的适配说明。 + const neverAdapted = fakeMinitool({ + hasScript: false, + failure: failureOf({ type: 'buildScriptMissing', script: 'build:xhs-minitool', searched: ['package.json', 'game/package.json'], - }, - }; - const minitool = fakeMinitool({ hasScript: false, failure }); - renderPane(minitool); + }), + }); + renderPane(neverAdapted); expect( screen.getByText('宿主找过:package.json、game/package.json'), ).not.toBeNull(); + expect( + screen.queryByRole('button', { name: '让 code agent 修' }), + ).toBeNull(); + fireEvent.click( + screen.getByRole('button', { name: '交给 code agent 适配' }), + ); + expect(neverAdapted.adapt).toHaveBeenCalledTimes(1); + expect(neverAdapted.repair).not.toHaveBeenCalled(); + + // 适配过但没跑通:入队修复指令,带上这次的现场。 + cleanup(); + const broken = fakeMinitool({ + failure: failureOf({ + type: 'commandFailed', + exitCode: 1, + outputTail: 'vite build failed', + }), + }); + renderPane(broken); + fireEvent.click(screen.getByRole('button', { name: '让 code agent 修' })); - expect(minitool.repair).toHaveBeenCalledTimes(1); + expect(broken.repair).toHaveBeenCalledTimes(1); + expect(broken.adapt).not.toHaveBeenCalled(); + + // 宿主自身没跑起来:只给重试,不把 agent 拉进来瞎猜。 + cleanup(); + const unavailable = fakeMinitool({ + failure: failureOf({ + type: 'exportUnavailable', + cause: '项目权限策略拒绝执行:command.exec', + }), + }); + renderPane(unavailable); + + fireEvent.click(screen.getByRole('button', { name: '重试' })); + expect(unavailable.runExport).toHaveBeenCalledTimes(1); + expect(unavailable.repair).not.toHaveBeenCalled(); }); }); +function failureOf( + error: XhsMinitoolFailureState['error'], +): XhsMinitoolFailureState { + return { notice: describeXhsMinitoolFailure(error), error }; +} + describe('状态 hook', () => { it('读到现场后,改字段停手会自动保存并把 baseHash 带回宿主', async () => { const invoke = vi.fn(async (command: string) => {