前端按命令的错误变体分流,卡片与出口只说事实
- `xhsMinitoolApi` / `xhsMinitoolFailure` 改成读三份载体,`expectNever` 钉死穷尽性,仍不对任何文案做判断; - 失败卡片与冲突面板按变体分派按钮,文案改成用户看得懂的说法(注册表 → 这份导出信息、文件里的 → 项目里的); - 输出尾部与另存为的句子不再出现实现词,复制按钮的成功 / 失败仍是按钮自己说。
This commit is contained in:
+76
-24
@@ -3,11 +3,16 @@
|
||||
*
|
||||
* 只做两件事:把命令名与参数名(camelCase)拼对,把结构化失败装成可判别的 `Error`。命令名在这
|
||||
* 里唯一收口,调用方不写字符串命令名、也不自己嗅探失败形状。
|
||||
*
|
||||
* **一条命令一个载体**:读、保存、构建各有自己的失败类型与 `as*` 守卫,`instanceof` 就是命令
|
||||
* 边界。这样调用方拿到的失败集合天然被命令限定,不需要再按命令挑变体。
|
||||
*/
|
||||
import type { TauriInvoke } from '../../../../app/types';
|
||||
import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError';
|
||||
import type { XHSMiniToolExportBuildError } from '../generated/XHSMiniToolExportBuildError';
|
||||
import type { XHSMiniToolExportForm } from '../generated/XHSMiniToolExportForm';
|
||||
import type { XHSMiniToolExportReadError } from '../generated/XHSMiniToolExportReadError';
|
||||
import type { XHSMiniToolExportRunResult } from '../generated/XHSMiniToolExportRunResult';
|
||||
import type { XHSMiniToolExportSaveError } from '../generated/XHSMiniToolExportSaveError';
|
||||
import type { XHSMiniToolExportState } from '../generated/XHSMiniToolExportState';
|
||||
|
||||
/**
|
||||
@@ -16,21 +21,53 @@ import type { XHSMiniToolExportState } from '../generated/XHSMiniToolExportState
|
||||
* 与入队失败(`EnqueueFailureWrapper`)同一套路:构造时把整份载荷 `JSON.stringify` 进
|
||||
* `Error.message`,分流只看 `error.type`,**不解析任何文案**。
|
||||
*/
|
||||
export class XhsMinitoolFailure extends Error {
|
||||
readonly error: XHSMiniToolExportError;
|
||||
export class XhsMinitoolReadFailure extends Error {
|
||||
readonly error: XHSMiniToolExportReadError;
|
||||
|
||||
constructor(error: XHSMiniToolExportError) {
|
||||
constructor(error: XHSMiniToolExportReadError) {
|
||||
super(JSON.stringify(error));
|
||||
this.name = 'XhsMinitoolFailure';
|
||||
this.name = 'XhsMinitoolReadFailure';
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export class XhsMinitoolSaveFailure extends Error {
|
||||
readonly error: XHSMiniToolExportSaveError;
|
||||
|
||||
constructor(error: XHSMiniToolExportSaveError) {
|
||||
super(JSON.stringify(error));
|
||||
this.name = 'XhsMinitoolSaveFailure';
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export class XhsMinitoolBuildFailure extends Error {
|
||||
readonly error: XHSMiniToolExportBuildError;
|
||||
|
||||
constructor(error: XHSMiniToolExportBuildError) {
|
||||
super(JSON.stringify(error));
|
||||
this.name = 'XhsMinitoolBuildFailure';
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/** 认得出是结构化失败就取出载荷,否则返回 `null`(传输失败、命令 panic、参数序列化失败)。 */
|
||||
export function asXhsMinitoolFailure(
|
||||
export function asXhsMinitoolReadFailure(
|
||||
error: unknown,
|
||||
): XHSMiniToolExportError | null {
|
||||
return error instanceof XhsMinitoolFailure ? error.error : null;
|
||||
): XHSMiniToolExportReadError | null {
|
||||
return error instanceof XhsMinitoolReadFailure ? error.error : null;
|
||||
}
|
||||
|
||||
export function asXhsMinitoolSaveFailure(
|
||||
error: unknown,
|
||||
): XHSMiniToolExportSaveError | null {
|
||||
return error instanceof XhsMinitoolSaveFailure ? error.error : null;
|
||||
}
|
||||
|
||||
export function asXhsMinitoolBuildFailure(
|
||||
error: unknown,
|
||||
): XHSMiniToolExportBuildError | null {
|
||||
return error instanceof XhsMinitoolBuildFailure ? error.error : null;
|
||||
}
|
||||
|
||||
/** 读内容:面板渲染与自动刷新反复走的就是这一条,它拿不到注册表指纹。 */
|
||||
@@ -38,8 +75,12 @@ export function readXhsMinitoolExport(
|
||||
invoke: TauriInvoke,
|
||||
projectPath: string,
|
||||
): Promise<XHSMiniToolExportState> {
|
||||
return guardStructuredFailure(() =>
|
||||
invoke<XHSMiniToolExportState>('read_xhs_minitool_export', { projectPath }),
|
||||
return guardStructuredFailure(
|
||||
() =>
|
||||
invoke<XHSMiniToolExportState>('read_xhs_minitool_export', {
|
||||
projectPath,
|
||||
}),
|
||||
XhsMinitoolReadFailure,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,8 +94,9 @@ export function readXhsMinitoolExportHash(
|
||||
invoke: TauriInvoke,
|
||||
projectPath: string,
|
||||
): Promise<string> {
|
||||
return guardStructuredFailure(() =>
|
||||
invoke<string>('read_xhs_minitool_export_hash', { projectPath }),
|
||||
return guardStructuredFailure(
|
||||
() => invoke<string>('read_xhs_minitool_export_hash', { projectPath }),
|
||||
XhsMinitoolReadFailure,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,12 +109,14 @@ export function saveXhsMinitoolExportForm(
|
||||
baseHash: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
return guardStructuredFailure(() =>
|
||||
invoke<string>('save_xhs_minitool_export_form', {
|
||||
projectPath: input.projectPath,
|
||||
form: input.form,
|
||||
baseHash: input.baseHash,
|
||||
}),
|
||||
return guardStructuredFailure(
|
||||
() =>
|
||||
invoke<string>('save_xhs_minitool_export_form', {
|
||||
projectPath: input.projectPath,
|
||||
form: input.form,
|
||||
baseHash: input.baseHash,
|
||||
}),
|
||||
XhsMinitoolSaveFailure,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,20 +124,28 @@ export function runXhsMinitoolExportBuild(
|
||||
invoke: TauriInvoke,
|
||||
projectPath: string,
|
||||
): Promise<XHSMiniToolExportRunResult> {
|
||||
return guardStructuredFailure(() =>
|
||||
invoke<XHSMiniToolExportRunResult>('run_xhs_minitool_export_build', {
|
||||
projectPath,
|
||||
}),
|
||||
return guardStructuredFailure(
|
||||
() =>
|
||||
invoke<XHSMiniToolExportRunResult>('run_xhs_minitool_export_build', {
|
||||
projectPath,
|
||||
}),
|
||||
XhsMinitoolBuildFailure,
|
||||
);
|
||||
}
|
||||
|
||||
async function guardStructuredFailure<T>(call: () => Promise<T>): Promise<T> {
|
||||
async function guardStructuredFailure<T, Payload>(
|
||||
call: () => Promise<T>,
|
||||
FailureClass: new (error: Payload) => Error,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await call();
|
||||
} catch (error) {
|
||||
// 真 `Error` 不是结构化拒绝:`JSON.stringify(new Error(...))` 只会得到 `{}`,
|
||||
// 装进 wrapper 就把真实原因丢了。原样抛出,交给调用方的 `instanceof` 分界。
|
||||
if (error instanceof Error) throw error;
|
||||
throw new XhsMinitoolFailure(error as XHSMiniToolExportError);
|
||||
// 非对象同理(命令没注册、参数序列化失败时 Tauri 会拒绝一个裸字符串):装进 wrapper 只会
|
||||
// 得到一份没有 `type` 的假变体,调用方按变体分流时会走到不该到的分支。
|
||||
if (typeof error !== 'object' || error === null) throw error;
|
||||
throw new FailureClass(error as Payload);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export async function saveXhsMinitoolFileToDisk(input: {
|
||||
title: string;
|
||||
}): Promise<string> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) return '需要在客户端内打开导出面板才能保存文件';
|
||||
if (!invoke) return '要在客户端里打开导出面板才能保存文件';
|
||||
let destination: string | null = null;
|
||||
try {
|
||||
destination = await saveNativeFileDialog({
|
||||
|
||||
+73
-78
@@ -1,98 +1,93 @@
|
||||
/**
|
||||
* 结构化失败 → 用户看得懂的一句话 + 下一步动作。
|
||||
* 结构化失败 → 两件互不派生的事:给用户看的一句话,和这次该由谁修。
|
||||
*
|
||||
* 与入队失败同一口径:分流只看 `error.type`,句子按变体写死,**不从载荷里"读"出文案**。载荷
|
||||
* 字段只是拼句子的原料(路径、退出码、输出尾部、解析器原文)。穷尽性由 `expectNever` 在编译期钉住。
|
||||
* 输入是**会出现在失败卡片上的失败**([`XhsMinitoolCardFailure`])——它按命令把三条失败表拼起来,
|
||||
* 但不含 `formInvalid`(字段红字)与 `saveConflict`(冲突面板):那两种有专属 UI,到不了卡片,
|
||||
* 也就不该在这张表里再分一次类。
|
||||
*
|
||||
* 这里是**纯映射**,不判「要不要上报」:上报判据写在 `useXhsMinitoolExport` 的 catch 里
|
||||
* (策略拒绝留在面板,宿主侧事实故障给完现场后原样抛出进错误池)。它与
|
||||
* [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`]「不抽 presenter」的差异是刻意的:
|
||||
* 那条针对只有一个消费方的认证 catch,而这份映射有两个消费方——面板卡片与交给 agent 的修复指令
|
||||
* (两者必须说同一句话,否则用户在面板里看到的和 agent 拿到的会分叉)。
|
||||
* 拆成两个纯函数,各答一件事:
|
||||
* - `describeXhsMinitoolFailure` 只产用户侧的说法(一个字符串),句子按变体写死,**不从载荷里
|
||||
* "读"文案**;
|
||||
* - `classifyXhsMinitoolFailure` 只回答「谁有能力修」(`XhsMinitoolFailureRepairability`),这是
|
||||
* 事实判断,不是按钮长什么样。
|
||||
*
|
||||
* 分流只看 `error.type`,穷尽性由 `expectNever` 在编译期钉住。
|
||||
*
|
||||
* 要交给 agent 的现场不从这里出——原始错误本身就是最好的现场,调用方直接 `JSON.stringify(error)`
|
||||
* 把它塞进指令;别再抽一份「给 agent 的版本」,两份说法迟早会分叉。
|
||||
*/
|
||||
import { expectNever } from '../../../../app/expectNever';
|
||||
import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError';
|
||||
import { XHS_MINITOOL_FIELD_LABELS } from './xhsMinitoolFields';
|
||||
import { describeOutputTail } from './xhsMinitoolOutputTail';
|
||||
import type { XHSMiniToolExportBuildError } from '../generated/XHSMiniToolExportBuildError';
|
||||
import type { XHSMiniToolExportReadError } from '../generated/XHSMiniToolExportReadError';
|
||||
import type { XHSMiniToolExportSaveError } from '../generated/XHSMiniToolExportSaveError';
|
||||
|
||||
/**
|
||||
* 失败之后最合适的下一步,也就是那颗按钮干什么:
|
||||
* - `adapt`:项目还没适配(没有构建脚本),该入队的是首次适配指令;
|
||||
* - `repair`:适配过了但这次没跑通(脚本非零退出、产物不在、注册表被改坏),该入队的是修复指令;
|
||||
* - `retry`:不用 agent 掺和,用户自己改或原样重试。
|
||||
* 会出现在失败卡片上的失败:读注册表、保存表单、跑构建三条命令里,除去有专属 UI 的两种。
|
||||
*
|
||||
* `formInvalid` 由保存命令在写盘前挡下,走 `FormCard` 的字段红字;`saveConflict` 走冲突面板。
|
||||
* 两者都不是「失败卡片」的现场,所以不在这张表里。
|
||||
*/
|
||||
export type XhsMinitoolFailureAction = 'adapt' | 'repair' | 'retry';
|
||||
export type XhsMinitoolCardFailure =
|
||||
| XHSMiniToolExportReadError
|
||||
| XHSMiniToolExportBuildError
|
||||
| Exclude<
|
||||
XHSMiniToolExportSaveError,
|
||||
{ type: 'formInvalid' } | { type: 'saveConflict' }
|
||||
>;
|
||||
|
||||
export type XhsMinitoolFailureNotice = {
|
||||
/** 一句话说清哪儿不对。 */
|
||||
message: string;
|
||||
/** 宿主给的现场(路径、退出码、输出尾部、解析器原文);没有就是空串。 */
|
||||
detail: string;
|
||||
action: XhsMinitoolFailureAction;
|
||||
};
|
||||
/**
|
||||
* 谁有能力修这次失败。判据只有一条:**要改的东西在不在用户项目的代码里**。
|
||||
* - `CodeAgentRepairable`:毛病在项目代码 / 构建脚本 / 产物 / 导出信息文件里——还没适配、脚本跑不通、
|
||||
* 产物没产出、注册表被改坏。用户改不动,只有 code agent 能碰。
|
||||
* - `OnlyHumanRepairable`:要改的东西不在项目代码里,叫 agent 来它也没得改:
|
||||
* - `commandDenied`:用户在项目权限策略(`.agent/policy.json` 的 `denied_commands`)里明确拉黑了
|
||||
* `command.exec`。这是用户自己的配置,而且 agent 受同一条策略约束——叫它来跑脚本正是策略禁止的事。
|
||||
* - `exportUnavailable`:宿主没跑起来,改代码没有用,只能重试。
|
||||
*/
|
||||
export enum XhsMinitoolFailureRepairability {
|
||||
CodeAgentRepairable,
|
||||
OnlyHumanRepairable,
|
||||
}
|
||||
|
||||
export function describeXhsMinitoolFailure(
|
||||
error: XHSMiniToolExportError,
|
||||
): XhsMinitoolFailureNotice {
|
||||
export function classifyXhsMinitoolFailure(
|
||||
error: XhsMinitoolCardFailure,
|
||||
): XhsMinitoolFailureRepairability {
|
||||
switch (error.type) {
|
||||
case 'registryMalformed':
|
||||
return {
|
||||
message: '导出注册表被改成了宿主认不出的形状,宿主拒绝猜测它的内容。',
|
||||
detail: error.cause,
|
||||
action: 'repair',
|
||||
};
|
||||
case 'formInvalid':
|
||||
return {
|
||||
message: `「${XHS_MINITOOL_FIELD_LABELS[error.field]}」不符合平台要求:${error.reason}`,
|
||||
detail: '',
|
||||
action: 'retry',
|
||||
};
|
||||
case 'buildScriptMissing':
|
||||
return {
|
||||
message: `项目里还没有 ${error.script} 脚本——这就是「还没适配」的判据。`,
|
||||
detail: `宿主找过:${error.searched.join('、')}`,
|
||||
action: 'adapt',
|
||||
};
|
||||
case 'saveConflict':
|
||||
return {
|
||||
message: '注册表在别处被改过,而且改的字段和你的输入不一样。',
|
||||
detail: '',
|
||||
action: 'retry',
|
||||
};
|
||||
case 'commandDenied':
|
||||
return {
|
||||
// 句子在这里拼:宿主只给被拒的命令点位,不拼人话。
|
||||
message: `项目权限策略拒绝执行:${error.commandId}`,
|
||||
detail: '',
|
||||
action: 'retry',
|
||||
};
|
||||
case 'commandFailed':
|
||||
return {
|
||||
message:
|
||||
error.exitCode === null
|
||||
? '构建脚本没能启动(沙箱 / 运行环境层面就失败了)。'
|
||||
: `构建脚本没有正常退出(exit code ${error.exitCode})。`,
|
||||
detail: describeOutputTail(error),
|
||||
action: 'repair',
|
||||
};
|
||||
case 'artifactMissing':
|
||||
return {
|
||||
message: `构建脚本跑完了,但 ${error.path} 不在。多数是把 zip 写到了别处。`,
|
||||
detail: '',
|
||||
action: 'repair',
|
||||
};
|
||||
return XhsMinitoolFailureRepairability.CodeAgentRepairable;
|
||||
case 'commandDenied':
|
||||
case 'exportUnavailable':
|
||||
return {
|
||||
message: '宿主这次没能执行导出。',
|
||||
detail: error.cause,
|
||||
action: 'retry',
|
||||
};
|
||||
return XhsMinitoolFailureRepairability.OnlyHumanRepairable;
|
||||
default:
|
||||
expectNever(error);
|
||||
return {
|
||||
message: '导出失败,原因不在已知失败表里。',
|
||||
detail: '',
|
||||
action: 'retry',
|
||||
};
|
||||
return XhsMinitoolFailureRepairability.OnlyHumanRepairable;
|
||||
}
|
||||
}
|
||||
|
||||
export function describeXhsMinitoolFailure(
|
||||
error: XhsMinitoolCardFailure,
|
||||
): string {
|
||||
switch (error.type) {
|
||||
case 'registryMalformed':
|
||||
return '导出信息的内容被改坏了,读不出来。可以让陶泥儿重新生成一份。';
|
||||
case 'buildScriptMissing':
|
||||
// 用户不需要知道 npm 脚本叫什么、宿主在哪些目录里找过——那是内部实现。
|
||||
return '这个项目还没有配置好导出。先让陶泥儿帮你调通一次,之后就能直接打包了。';
|
||||
case 'commandDenied':
|
||||
return '当前项目的权限设置不允许执行这次导出。';
|
||||
case 'commandFailed':
|
||||
return error.exitCode === null
|
||||
? '导出没能启动,可能是运行环境的问题。'
|
||||
: '导出中途出错了,没能生成代码包。';
|
||||
case 'artifactMissing':
|
||||
return '导出跑完了,但没有生成代码包。可以让陶泥儿看看。';
|
||||
case 'exportUnavailable':
|
||||
return '这次没能执行导出,请稍后重试。';
|
||||
default:
|
||||
expectNever(error);
|
||||
return '导出失败,原因不在已知失败表里。';
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 构建输出尾部的显示文本:宿主只给两个事实(原文 + 省略了多少字符),句子在这里拼。
|
||||
*
|
||||
* 宿主不预拼用户可见文案([`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`]):省略量是事实,
|
||||
* 宿主不预拼用户可见文案:省略量是事实,
|
||||
* 「省略了多少」怎么说、放在前还是后、要不要提示「尾巴不完整」,都由拿得到现场的这一层决定。
|
||||
* 失败卡片与导出成功提示共用这一份,避免两处各写一句。
|
||||
*/
|
||||
@@ -11,6 +11,6 @@ export function describeOutputTail(input: {
|
||||
}): string {
|
||||
const tail = input.outputTail.trim();
|
||||
if (input.omittedCharacters <= 0) return tail;
|
||||
const note = `(日志过长,已省略前 ${input.omittedCharacters} 个字符)`;
|
||||
const note = `(内容较长,前面省略了 ${input.omittedCharacters} 个字符)`;
|
||||
return tail ? `${note}\n${tail}` : note;
|
||||
}
|
||||
|
||||
+7
-7
@@ -8,10 +8,10 @@ import { PaneButton } from './PaneButton';
|
||||
type Side = 'mine' | 'file';
|
||||
|
||||
/**
|
||||
* 字段级冲突面板:文件在别处被改过,且和用户手里的值不同。
|
||||
* 字段级冲突面板:这份信息在别处被改过,且和用户手里的值不同。
|
||||
*
|
||||
* 只列**真的不一样**的字段,每行二选一,默认「我的」——任何情况下都不静默覆盖用户输入,也不
|
||||
* 静默丢掉文件里的改动。宿主只是在保存时发现指纹对不上,判定与选择都在这一层。
|
||||
* 静默丢掉项目里的改动。宿主只是在保存时发现对不上,判定与选择都在这一层。
|
||||
*
|
||||
* 调用方用 `key={conflict.contentHash}` 挂载:换一次冲突就是一次全新的选择。
|
||||
*/
|
||||
@@ -40,14 +40,14 @@ export function ConflictCard({
|
||||
return (
|
||||
<section
|
||||
role="alertdialog"
|
||||
aria-label="注册表冲突"
|
||||
className="rounded-xl border border-[#c7653d] bg-[#fff5ef] p-3"
|
||||
aria-label="导出信息冲突"
|
||||
className="rounded-2xl border border-[#c7653d] bg-[#fff5ef] p-4"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-[#3d1f10]">
|
||||
注册表在别处被改过
|
||||
这份导出信息被改过
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-[#6f5848]">
|
||||
{`下面 ${differing.length} 个字段两边不一样,逐个选一个再保存;没列出来的字段两边一致。`}
|
||||
{`下面 ${differing.length} 项两边不一样,各选一个再保存;没列出来的两边一致。`}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
{differing.map((meta) => (
|
||||
@@ -70,7 +70,7 @@ export function ConflictCard({
|
||||
/>
|
||||
<Choice
|
||||
label={meta.label}
|
||||
caption="文件里的"
|
||||
caption="项目里的"
|
||||
selected={choice[meta.field] === 'file'}
|
||||
value={conflict.current[meta.field]}
|
||||
onSelect={() =>
|
||||
|
||||
+2
-2
@@ -32,10 +32,10 @@ export function CopyButton({ value, label }: { value: string; label: string }) {
|
||||
state === 'failed' ? `复制${label}失败,请手动复制` : `复制${label}`
|
||||
}
|
||||
disabled={!value}
|
||||
className={`inline-flex shrink-0 items-center gap-1 rounded-lg border px-2 py-1 text-xs transition disabled:cursor-not-allowed disabled:opacity-45 ${
|
||||
className={`inline-flex shrink-0 items-center gap-1 rounded-[10px] border px-2.5 py-1.5 text-xs font-semibold transition disabled:cursor-not-allowed disabled:opacity-45 ${
|
||||
state === 'failed'
|
||||
? 'border-red-300 bg-red-50 text-red-600'
|
||||
: 'border-[#e2cbb8] bg-[#fffaf5] text-[#6f5848] hover:border-[#c7653d] hover:text-[#c7653d]'
|
||||
: 'border-[#e2cbb8] bg-white/80 text-[#6f5848] hover:border-[#c7653d] hover:text-[#c7653d]'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setState('idle');
|
||||
|
||||
+30
-23
@@ -1,12 +1,21 @@
|
||||
import type { XhsMinitoolFailureState } from '../../state/useXhsMinitoolExport';
|
||||
import type { XhsMinitoolCardFailure } from '../../state/xhsMinitoolFailure';
|
||||
import {
|
||||
classifyXhsMinitoolFailure,
|
||||
describeXhsMinitoolFailure,
|
||||
XhsMinitoolFailureRepairability,
|
||||
} from '../../state/xhsMinitoolFailure';
|
||||
import { PaneButton } from './PaneButton';
|
||||
|
||||
/**
|
||||
* 失败卡片:宿主给的 typed 失败原文(有一句人话 + 现场),加一颗按变体选出来的按钮。
|
||||
* 失败卡片:一句用户看得懂的话 + 一颗按「谁有能力修」选出来的按钮。
|
||||
*
|
||||
* 三颗按钮对应三种下一步:项目还没适配就入队首次适配指令,适配过但没跑通就入队修复指令,
|
||||
* 宿主自身没跑起来(`exportUnavailable`)或用户能自己改的(表单、冲突)就只给重试——别把
|
||||
* agent 拉进来瞎猜。
|
||||
* 按钮由 `classifyXhsMinitoolFailure` 决定,不在这里按变体现猜:
|
||||
* - `CodeAgentRepairable`:项目还没适配就给「让陶泥儿帮我调通」,适配过但这次没跑通就给「让陶泥儿来修」;
|
||||
* - `OnlyHumanRepairable`(项目权限策略拉黑了 `command.exec`、宿主没跑起来):只给「重试」——
|
||||
* 要改的东西不在项目代码里,别把陶泥儿拉进来改一份没问题可改的代码。
|
||||
*
|
||||
* 卡片只收 [`XhsMinitoolCardFailure`]:表单校验与保存冲突有各自的 UI,到不了这里。
|
||||
* 宿主给的现场也不在这里展开:那是给 agent 的原始错误(`JSON.stringify` 进指令),不是给用户看的。
|
||||
*/
|
||||
export function FailureCard({
|
||||
failure,
|
||||
@@ -14,37 +23,35 @@ export function FailureCard({
|
||||
onRepair,
|
||||
onRetry,
|
||||
}: {
|
||||
failure: XhsMinitoolFailureState;
|
||||
failure: XhsMinitoolCardFailure;
|
||||
onAdapt: () => void;
|
||||
onRepair: () => void;
|
||||
onRetry: () => void;
|
||||
}) {
|
||||
const { notice } = failure;
|
||||
const repairability = classifyXhsMinitoolFailure(failure);
|
||||
return (
|
||||
<section
|
||||
role="alert"
|
||||
aria-label="导出失败"
|
||||
className="rounded-xl border border-[#c7653d] bg-[#fff5ef] p-3"
|
||||
className="rounded-2xl border border-[#c7653d] bg-[#fff5ef] p-4"
|
||||
>
|
||||
<p className="text-sm text-[#3d1f10]">{notice.message}</p>
|
||||
{notice.detail ? (
|
||||
<pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-all rounded-lg bg-white/70 p-2 text-xs text-[#6f5848]">
|
||||
{notice.detail}
|
||||
</pre>
|
||||
) : null}
|
||||
<p className="text-sm text-[#3d1f10]">
|
||||
{describeXhsMinitoolFailure(failure)}
|
||||
</p>
|
||||
<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>
|
||||
) : (
|
||||
{repairability ===
|
||||
XhsMinitoolFailureRepairability.OnlyHumanRepairable ? (
|
||||
<PaneButton variant="primary" onClick={onRetry}>
|
||||
重试
|
||||
</PaneButton>
|
||||
) : failure.type === 'buildScriptMissing' ? (
|
||||
<PaneButton variant="primary" onClick={onAdapt}>
|
||||
让陶泥儿帮我调通
|
||||
</PaneButton>
|
||||
) : (
|
||||
<PaneButton variant="primary" onClick={onRepair}>
|
||||
让陶泥儿来修
|
||||
</PaneButton>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user