未适配提示自带入队按钮,失败卡片按变体分派三颗按钮
- 未适配提示从一句话改成一张卡片:说明和「交给 code agent 适配」那颗按钮放在一起,用户不用按照提示去别处找按钮;按钮行里就不再重复一颗,同一屏同一件事只有一个入口。 - 这条提示的判据补上 form 与 failure:hasScript 初值是 false,注册表还没读回来(或读失败)时不能替项目下「还没适配」的结论;失败卡片带着宿主的现场和同一颗按钮,两张卡一起出现就是同一件事两个入口。 - 失败变体的 action 从两向扩成三向 adapt / repair / retry:buildScriptMissing 归 adapt(首次适配该发适配指令,原来发修复指令会让 agent 去读一份还不存在的适配说明),commandFailed / artifactMissing / registryMalformed 归 repair,其余仍是 retry。 - 前端用例 12 → 14:钉住「未适配时按钮行里没有适配按钮、卡片那颗只调 adapt」「读回来之前不下结论、读失败时保留按钮行的适配入口」,并把失败卡片用例扩成适配 / 修 / 重试三种变体。
This commit is contained in:
+10
-5
@@ -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 {
|
||||
|
||||
+35
-11
@@ -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="导出内容区"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<PaneButton
|
||||
variant={minitool.hasScript ? 'ghost' : 'primary'}
|
||||
onClick={minitool.adapt}
|
||||
>
|
||||
适配
|
||||
</PaneButton>
|
||||
{missingScript ? null : (
|
||||
<PaneButton
|
||||
variant={minitool.hasScript ? 'ghost' : 'primary'}
|
||||
onClick={minitool.adapt}
|
||||
>
|
||||
适配
|
||||
</PaneButton>
|
||||
)}
|
||||
<PaneButton
|
||||
variant={minitool.hasScript ? 'primary' : 'ghost'}
|
||||
disabled={!minitool.hasScript || minitool.running}
|
||||
title={
|
||||
minitool.hasScript
|
||||
? `重新运行 ${XHS_MINITOOL_SCRIPT_NAME}`
|
||||
: `项目里还没有 ${XHS_MINITOOL_SCRIPT_NAME},先点「适配」让 code agent 接入`
|
||||
: `项目里还没有 ${XHS_MINITOOL_SCRIPT_NAME},用下面卡片里的按钮让 code agent 接入`
|
||||
}
|
||||
onClick={() => void minitool.runExport()}
|
||||
>
|
||||
@@ -84,10 +99,18 @@ export function ArtifactsPane({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!minitool.hasScript ? (
|
||||
<p className="rounded-lg border border-dashed border-[#e2cbb8] bg-[#fffaf5] p-2 text-xs text-[#6f5848]">
|
||||
{`项目里还没有 ${XHS_MINITOOL_SCRIPT_NAME} 脚本。点「适配」把这项工作交给 code agent:它会在项目里实验出能产出平台制品的脚本,并把这套做法固化进项目。`}
|
||||
</p>
|
||||
{missingScript ? (
|
||||
<section
|
||||
aria-label="还没有构建脚本"
|
||||
className="flex flex-col items-start gap-2 rounded-xl border border-dashed border-[#e2cbb8] bg-[#fffaf5] p-3"
|
||||
>
|
||||
<p className="text-xs text-[#6f5848]">
|
||||
{`项目里还没有 \`${XHS_MINITOOL_SCRIPT_NAME}\` 脚本——这就是「还没适配」的判据。这件事只能由 code agent 在项目里实验完成(用哪版 vite 配置、入口在哪、怎么调打包脚本都由它定):`}
|
||||
</p>
|
||||
<PaneButton variant="primary" onClick={minitool.adapt}>
|
||||
交给 code agent 适配
|
||||
</PaneButton>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{form ? (
|
||||
@@ -137,6 +160,7 @@ export function ArtifactsPane({
|
||||
{minitool.failure ? (
|
||||
<FailureCard
|
||||
failure={minitool.failure}
|
||||
onAdapt={minitool.adapt}
|
||||
onRepair={minitool.repair}
|
||||
onRetry={() => void minitool.runExport()}
|
||||
/>
|
||||
|
||||
+9
-2
@@ -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}
|
||||
<div className="mt-2">
|
||||
{notice.action === 'adapt' ? (
|
||||
<PaneButton variant="primary" onClick={onAdapt}>
|
||||
交给 code agent 适配
|
||||
</PaneButton>
|
||||
) : notice.action === 'repair' ? (
|
||||
<PaneButton variant="primary" onClick={onRepair}>
|
||||
让 code agent 修
|
||||
</PaneButton>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user