小红书导出面板的行为层:命令入口、指令正文、失败说法与状态 hook

- 新增 state/xhsMinitoolApi.ts:三个命令的 JS 入口(命令名与 camelCase 参数名唯一收口),结构化失败装成 XhsMinitoolFailure,真 Error 原样抛出。
- 新增 state/xhsMinitoolInstruction.ts:首次适配与按失败修复两段指令正文 + 契约常量(脚本名、产物路径、注册表形状、要显式拷贝的示例脚本与落点),每条都能在 Rust 侧找到对应硬校验。
- 新增 state/xhsMinitoolFailure.ts:七个失败变体各一句人话与下一步动作,穷尽性由 expectNever 在编译期钉住。
- 新增 state/xhsMinitoolFields.ts:三个字段的标签、占位与字数上限;字段身份直接用 ts-rs 的 XHSMiniToolExportField,与表单键一一对应。
- 新增 state/xhsMinitoolDownload.ts:icon / zip 经原生保存对话框落盘,复用素材保存命令的既有门禁。
- 新增 state/useXhsMinitoolExport.ts:读现场、防抖自动保存(带 baseHash)、字段级冲突、每次重新导出,以及只有用户显式点「适配 / 让 agent 修」才入队的指令发送。
- .eslintrc.cjs 把 export/generated 加入 ignorePatterns,与其余 ts-rs 生成目录同口径,禁止 eslint --fix 二次改写。
This commit is contained in:
2026-10-05 16:01:49 +08:00
parent 3a1999e115
commit d694c183d9
7 changed files with 614 additions and 0 deletions
+1
View File
@@ -167,6 +167,7 @@ module.exports = {
// ts-rs 生成绑定:不接受 eslint --fix 二次改写,必须与原始输出逐字节一致
'packages/shared/src/contracts/generated/**',
'apps/ai-game-creator-shell/src/view/project-development/chat/generated/**',
'apps/ai-game-creator-shell/src/view/project-development/export/generated/**',
'apps/ai-game-creator-shell/src/services/generated/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
// 审核 Skill Pack:内容按 SHA-256 定址并经 include_bytes! 编译进客户端,
@@ -0,0 +1,253 @@
/**
* 小红书小工具导出 tab 的状态:注册表读写、表单自动保存、冲突、构建、把任务交给 code agent。
*
* 挂在面板根(`usePanel` → `useXiaohongshuTab`)上,不挂在 tab 页里:切到别的 tab 时页面组件
* 会卸载,但「读到了什么、有什么冲突、正在跑什么」必须留在面板上。
*
* 两条边界写死在这里:
* - agent 调用只有用户显式点「适配 / 让 agent 修」才会发生,保存与导出**不触发任何** agent 调用;
* - `hasScript` 永远来自宿主的现算结果,前端不自己读 `package.json`、也不缓存这个事实。
*/
import { useCallback, useEffect, useState } from 'react';
import { resolveTauriInvoke } from '../../../../app/tauri';
import type { UserContentPart } from '../../chat/generated/UserContentPart';
import type { FormInvalid } from '../generated/FormInvalid';
import type { SaveConflict } from '../generated/SaveConflict';
import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError';
import type { XHSMiniToolExportForm } from '../generated/XHSMiniToolExportForm';
import {
asXhsMinitoolFailure,
readXhsMinitoolExport,
runXhsMinitoolExportBuild,
saveXhsMinitoolExportForm,
} from './xhsMinitoolApi';
import {
describeXhsMinitoolFailure,
type XhsMinitoolFailureNotice,
} from './xhsMinitoolFailure';
import {
buildXhsMinitoolAdaptationInstruction,
buildXhsMinitoolRepairInstruction,
XHS_MINITOOL_ARTIFACT_RELATIVE_PATH,
} from './xhsMinitoolInstruction';
export type XhsMinitoolExportState = ReturnType<typeof useXhsMinitoolExport>;
/**
* 一次失败的完整现场:给用户看的说法,加上原始载荷(「让 agent 修」要用它拼指令)。
*/
export type XhsMinitoolFailureState = {
notice: XhsMinitoolFailureNotice;
error: XHSMiniToolExportError;
};
/** 输入停下多久之后自动保存:够长到不打断打字,够短到用户切走的时候不丢字。 */
const AUTOSAVE_DELAY_MS = 600;
const NO_HOST_NOTICE =
'需要在客户端内打开导出面板,宿主才能读写注册表并运行构建脚本';
/** 把结构化失败收成现场:给用户看的说法 + 原始载荷。 */
function failureState(error: XHSMiniToolExportError): XhsMinitoolFailureState {
return { notice: describeXhsMinitoolFailure(error), error };
}
export function useXhsMinitoolExport({
enabled,
projectPath,
submitTurn,
}: {
/** 进过一次小红书 tab 且项目路径就绪后才读注册表:进入前不产生任何文件访问。 */
enabled: boolean;
projectPath: string | null;
/** 往当前项目会话里发一条用户消息(`chat.submit`);返回值是「这条草稿能不能清」。 */
submitTurn: (content: UserContentPart[]) => Promise<boolean>;
}) {
const [form, setForm] = useState<XHSMiniToolExportForm | null>(null);
const [hasScript, setHasScript] = useState(false);
const [contentHash, setContentHash] = useState('');
const [fieldError, setFieldError] = useState<FormInvalid | null>(null);
const [conflict, setConflict] = useState<SaveConflict | null>(null);
const [failure, setFailure] = useState<XhsMinitoolFailureState | null>(null);
const [notice, setNotice] = useState('');
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [running, setRunning] = useState(false);
const [dirty, setDirty] = useState(false);
/** 读现场:注册表(宿主缺则补空表单)+ 脚本是否存在。刷新按钮走的就是这一条。 */
const load = useCallback(async (): Promise<void> => {
if (!projectPath) return;
const invoke = resolveTauriInvoke();
if (!invoke) {
setNotice(NO_HOST_NOTICE);
return;
}
setLoading(true);
try {
const state = await readXhsMinitoolExport(invoke, projectPath);
setForm(state.form);
setHasScript(state.hasScript);
setContentHash(state.contentHash);
setFieldError(null);
setConflict(null);
setFailure(null);
setDirty(false);
} catch (error) {
const structured = asXhsMinitoolFailure(error);
if (structured) setFailure(failureState(structured));
else setNotice(error instanceof Error ? error.message : String(error));
} finally {
setLoading(false);
}
}, [projectPath]);
useEffect(() => {
if (!enabled) return;
void load();
}, [enabled, load]);
const persist = useCallback(
async (next: XHSMiniToolExportForm, baseHash: string): Promise<void> => {
if (!projectPath) return;
const invoke = resolveTauriInvoke();
if (!invoke) {
setNotice(NO_HOST_NOTICE);
return;
}
setSaving(true);
try {
const state = await saveXhsMinitoolExportForm(invoke, {
projectPath,
form: next,
baseHash,
});
setHasScript(state.hasScript);
setContentHash(state.contentHash);
setFieldError(null);
setConflict(null);
setFailure(null);
setDirty(false);
} catch (error) {
// 三种结果都要停下重试,等用户或下一次输入推着走:字段不合法等用户改、冲突等用户选、
// 其余按 typed 失败给建议。共同点是 `dirty` 归零,避免同一份输入反复打宿主。
setDirty(false);
const structured = asXhsMinitoolFailure(error);
if (!structured) {
setNotice(error instanceof Error ? error.message : String(error));
return;
}
if (structured.type === 'formInvalid') setFieldError(structured);
else if (structured.type === 'saveConflict') setConflict(structured);
else setFailure(failureState(structured));
} finally {
setSaving(false);
}
},
[projectPath],
);
useEffect(() => {
if (!enabled || !form || !dirty || conflict || fieldError) return;
const timer = setTimeout(() => {
void persist(form, contentHash);
}, AUTOSAVE_DELAY_MS);
return () => clearTimeout(timer);
}, [enabled, form, dirty, conflict, fieldError, contentHash, persist]);
/** 改一个字段。改哪个字段就清哪个字段的旧报错——别让已经改好的红字继续挂着。 */
const updateField = useCallback(
(field: keyof XHSMiniToolExportForm, value: string): void => {
setForm((previous) =>
previous ? { ...previous, [field]: value } : previous,
);
setFieldError((previous) =>
previous && previous.field === field ? null : previous,
);
setDirty(true);
},
[],
);
/** 用户逐字段选完之后落盘:带着冲突时读到的指纹重试,成功即自然解开冲突。 */
const resolveConflict = useCallback(
(resolved: XHSMiniToolExportForm): void => {
if (!conflict) return;
setForm(resolved);
void persist(resolved, conflict.contentHash);
},
[conflict, persist],
);
/** 导出:每次都重新跑脚本、重新产出 zip,没有"上次的产物还能用"这条捷径。 */
const runExport = useCallback(async (): Promise<void> => {
if (!projectPath) return;
const invoke = resolveTauriInvoke();
if (!invoke) {
setNotice(NO_HOST_NOTICE);
return;
}
setRunning(true);
setNotice('');
setFailure(null);
try {
await runXhsMinitoolExportBuild(invoke, projectPath);
setNotice(`已重新产出 ${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}`);
} catch (error) {
const structured = asXhsMinitoolFailure(error);
if (structured) setFailure(failureState(structured));
else setNotice(error instanceof Error ? error.message : String(error));
} finally {
setRunning(false);
}
}, [projectPath]);
const sendToAgent = useCallback(
async (instruction: string): Promise<void> => {
const handedOver = await submitTurn([
{ type: 'input_text', text: instruction },
]);
setNotice(
handedOver
? '已把任务交给 code agent,右侧会话里能看到它的进展'
: '这条任务没有发出去,原因在右侧会话里',
);
},
[submitTurn],
);
/** 一键适配:把首次适配指令发给同一个项目会话。 */
const adapt = useCallback((): void => {
if (!projectPath) return;
void sendToAgent(buildXhsMinitoolAdaptationInstruction(projectPath));
}, [projectPath, sendToAgent]);
/** 按当前失败现场让 agent 修。失败卡片上的那颗按钮走这里。 */
const repair = useCallback((): void => {
if (!projectPath || !failure) return;
setFailure(null);
void sendToAgent(
buildXhsMinitoolRepairInstruction(projectPath, failure.error),
);
}, [projectPath, failure, sendToAgent]);
return {
form,
hasScript,
contentHash,
fieldError,
conflict,
failure,
notice,
loading,
saving,
running,
dirty,
updateField,
load,
resolveConflict,
runExport,
adapt,
repair,
};
}
@@ -0,0 +1,82 @@
/**
* 小红书导出命令的 JS 侧入口。
*
* 只做两件事:把命令名与参数名(camelCase)拼对,把结构化失败装成可判别的 `Error`。命令名在这
* 里唯一收口,调用方不写字符串命令名、也不自己嗅探失败形状。
*/
import type { TauriInvoke } from '../../../../app/types';
import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError';
import type { XHSMiniToolExportForm } from '../generated/XHSMiniToolExportForm';
import type { XHSMiniToolExportRunResult } from '../generated/XHSMiniToolExportRunResult';
import type { XHSMiniToolExportState } from '../generated/XHSMiniToolExportState';
/**
* 命令失败在 JS 侧的载体:`error` 就是 ts-rs 生成的判别联合。
*
* 与入队失败(`EnqueueFailureWrapper`)同一套路:构造时把整份载荷 `JSON.stringify` 进
* `Error.message`,分流只看 `error.type`,**不解析任何文案**。
*/
export class XhsMinitoolFailure extends Error {
readonly error: XHSMiniToolExportError;
constructor(error: XHSMiniToolExportError) {
super(JSON.stringify(error));
this.name = 'XhsMinitoolFailure';
this.error = error;
}
}
/** 认得出是结构化失败就取出载荷,否则返回 `null`(传输失败、命令 panic、参数序列化失败)。 */
export function asXhsMinitoolFailure(
error: unknown,
): XHSMiniToolExportError | null {
return error instanceof XhsMinitoolFailure ? error.error : null;
}
export function readXhsMinitoolExport(
invoke: TauriInvoke,
projectPath: string,
): Promise<XHSMiniToolExportState> {
return guardStructuredFailure(() =>
invoke<XHSMiniToolExportState>('read_xhs_minitool_export', { projectPath }),
);
}
export function saveXhsMinitoolExportForm(
invoke: TauriInvoke,
input: {
projectPath: string;
form: XHSMiniToolExportForm;
baseHash: string;
},
): Promise<XHSMiniToolExportState> {
return guardStructuredFailure(() =>
invoke<XHSMiniToolExportState>('save_xhs_minitool_export_form', {
projectPath: input.projectPath,
form: input.form,
baseHash: input.baseHash,
}),
);
}
export function runXhsMinitoolExportBuild(
invoke: TauriInvoke,
projectPath: string,
): Promise<XHSMiniToolExportRunResult> {
return guardStructuredFailure(() =>
invoke<XHSMiniToolExportRunResult>('run_xhs_minitool_export_build', {
projectPath,
}),
);
}
async function guardStructuredFailure<T>(call: () => Promise<T>): 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);
}
}
@@ -0,0 +1,47 @@
/**
* 把导出产物(icon / zip)经原生保存对话框落到用户选定的位置。
*
* 复用素材保存的既有链路 `save_local_project_asset_file`:路径门禁、敏感文件拒绝、自我覆盖保护
* 都在宿主那一侧,前端不自己读文件、不拼绝对路径、也不写第二份保存实现。
*/
import { save as saveNativeFileDialog } from '@tauri-apps/plugin-dialog';
import { resolveTauriInvoke } from '../../../../app/tauri';
/** 返回给用户看的一句话;用户取消对话框时返回空串(不打扰)。 */
export async function saveXhsMinitoolFileToDisk(input: {
projectPath: string;
/** 项目内相对路径,交给宿主的既有门禁复核。 */
relativePath: string;
/** 对话框里的默认文件名。 */
defaultFileName: string;
title: string;
}): Promise<string> {
const invoke = resolveTauriInvoke();
if (!invoke) return '需要在客户端内打开导出面板才能保存文件';
let destination: string | null = null;
try {
destination = await saveNativeFileDialog({
defaultPath: input.defaultFileName,
title: input.title,
});
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
if (!destination) return '';
try {
const result = await invoke<{ destinationPath: string }>(
'save_local_project_asset_file',
{
input: {
projectPath: input.projectPath,
relativePath: input.relativePath,
destinationPath: destination,
},
},
);
return `已保存到 ${result.destinationPath}`;
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}
@@ -0,0 +1,79 @@
/**
* 结构化失败 → 用户看得懂的一句话 + 下一步动作。
*
* 与入队失败同一口径:分流只看 `error.type`,句子按变体写死,**不从载荷里"读"出文案**。载荷
* 字段只是拼句子的原料(路径、退出码、解析器原文)。穷尽性由 `expectNever` 在编译期钉住。
*/
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';
export type XhsMinitoolFailureNotice = {
/** 一句话说清哪儿不对。 */
message: string;
/** 宿主给的现场(路径、退出码、输出尾部、解析器原文);没有就是空串。 */
detail: string;
action: XhsMinitoolFailureAction;
};
export function describeXhsMinitoolFailure(
error: XHSMiniToolExportError,
): XhsMinitoolFailureNotice {
switch (error.type) {
case 'registryMalformed':
return {
message: '导出注册表被改成了宿主认不出的形状,宿主拒绝猜测它的内容。',
detail: error.cause,
action: 'adapt',
};
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 'commandFailed':
return {
message:
error.exitCode === null
? '构建脚本没能启动(沙箱 / 运行环境层面就失败了)。'
: `构建脚本没有正常退出(exit code ${error.exitCode})。`,
detail: error.outputTail,
action: 'adapt',
};
case 'artifactMissing':
return {
message: `构建脚本跑完了,但 ${error.path} 不在。多数是把 zip 写到了别处。`,
detail: '',
action: 'adapt',
};
case 'exportUnavailable':
return {
message: '宿主这次没能执行导出。',
detail: error.cause,
action: 'retry',
};
default:
expectNever(error);
return {
message: '导出失败,原因不在已知失败表里。',
detail: '',
action: 'retry',
};
}
}
@@ -0,0 +1,47 @@
/**
* 三个表单字段的展示元数据:标签、占位文案、字数上限。
*
* 字段身份直接用 ts-rs 生成的 `XHSMiniToolExportField`(`"name" | "introduction" | "iconPath"`):
* 它与表单对象的键一一对应,所以 `form[field]` 天然成立,不需要再维护一份字段映射。
*/
import type { XHSMiniToolExportField } from '../generated/XHSMiniToolExportField';
export type XhsMinitoolFieldMeta = {
field: XHSMiniToolExportField;
label: string;
placeholder: string;
/** 宿主硬校验的字数上限;icon 不是文本字段,没有这一项。 */
maxChars?: number;
multiline: boolean;
};
/** 表单展示顺序。加字段时只改这里,保存与复制都按字段身份走。 */
export const XHS_MINITOOL_FORM_FIELDS: readonly XhsMinitoolFieldMeta[] = [
{
field: 'name',
label: '名称',
placeholder: '最多 14 个字',
maxChars: 14,
multiline: false,
},
{
field: 'introduction',
label: '简介',
placeholder: '最多 14 个字',
maxChars: 14,
multiline: true,
},
{
field: 'iconPath',
label: 'icon 文件',
placeholder: '项目内相对路径,例如 assets/icon.png',
multiline: false,
},
];
export const XHS_MINITOOL_FIELD_LABELS: Record<XHSMiniToolExportField, string> =
{
name: '名称',
introduction: '简介',
iconPath: 'icon 文件',
};
@@ -0,0 +1,105 @@
/**
* 交给 code agent 的指令正文:首次适配,以及按失败现场修复。
*
* 纯函数 + 契约常量:宿主只认「npm 脚本名 + 产物路径 + 注册表形状」,其余(用哪版 vite 配置、
* 中间目录叫什么、怎么调 pack.mjs)都留给 agent 在用户项目里实验。这里写的每条契约都能在
* `src-tauri/src/export/draft/xhs_minitool/` 里找到对应的硬校验,改一边必须改另一边。
*/
import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError';
import { describeXhsMinitoolFailure } from './xhsMinitoolFailure';
/** 目标名,与 `model.ts` 的 `TARGET_META` 人话一致。 */
export const XHS_MINITOOL_TARGET_LABEL = '小红书小工具';
/** npm 脚本名:冒号形式是宿主 `command.exec` 白名单的要求,不是审美选择。 */
export const XHS_MINITOOL_SCRIPT_NAME = 'build:xhs-minitool';
/** 交付产物:宿主与用户都按这个相对路径找它。 */
export const XHS_MINITOOL_ARTIFACT_RELATIVE_PATH = '.export/xhs-minitool.zip';
/** 注册表:宿主读写,形状固定。 */
export const XHS_MINITOOL_REGISTRY_RELATIVE_PATH = '.export/xhs-minitool.json';
/** 首次适配要读的 skill 名。 */
export const XHS_MINITOOL_SKILL_NAME = 'vite-export-xhs-minitool';
/** 注册表的规范形状:agent 可以填值,但不许增删键。 */
export const XHS_MINITOOL_REGISTRY_SHAPE =
'{"form":{"name":"","introduction":"","iconPath":""}}';
/** skill 示例脚本 → 用户项目里的落点:拷贝是显式要求,落点由宿主约定。 */
export const XHS_MINITOOL_COPIED_SCRIPTS: ReadonlyArray<{
from: string;
to: string;
}> = [
{ from: 'scripts/pack.mjs', to: '.export/xhs-minitool.pack.mjs' },
{ from: 'scripts/validate.mjs', to: '.export/xhs-minitool.validate.mjs' },
{
from: 'scripts/vite.config.xhs-minitool.mjs',
to: '.export/xhs-minitool.vite.config.mjs',
},
];
/**
* 首次适配指令。
*
* 三条硬要求按「宿主会拒绝什么」排:脚本名、产物路径、注册表形状;其余是做法与坑。
* 结尾要求把结论写回脚本顶部注释——下次改动靠它,不靠聊天记录。
*/
export function buildXhsMinitoolAdaptationInstruction(
projectPath: string,
): string {
const copied = XHS_MINITOOL_COPIED_SCRIPTS.map(
(entry) => ` - ${entry.from} → ${entry.to}`,
).join('\n');
return [
`【任务】把当前项目适配成能产出「${XHS_MINITOOL_TARGET_LABEL}」制品的项目,并真的跑通一次。`,
'',
`项目根:${projectPath}`,
'',
'宿主按下面三条硬契约校验,请逐条满足(做不到就直接说明卡在哪一条,不要糊一个假产物):',
`1. npm 包里声明脚本 \`${XHS_MINITOOL_SCRIPT_NAME}\`。npm 包根 = 声明 package.json 的那个目录(通常是 game/)。`,
`2. 跑完这个脚本,\`${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\` 必须存在且非空(相对项目根)。`,
`3. \`${XHS_MINITOOL_REGISTRY_RELATIVE_PATH}\` 是宿主的注册表,形状只能是 ${XHS_MINITOOL_REGISTRY_SHAPE}:`,
' 三个字段都是字符串,不要增删键、不要加状态字段。可以按产物实际情况填 name 与 introduction;',
' iconPath 是项目内相对路径,留空由用户在面板里选文件。',
'',
`读 \`${XHS_MINITOOL_SKILL_NAME}\` skill,并把它的示例脚本**显式拷贝**到 .export/ 下(这份拷贝属于用户项目,允许你按本项目情况改路径、入口与依赖):`,
copied,
'',
'两个必须避开的坑:',
'- 打包脚本的 `--out-dir` 只允许指向 vite 的构建输出目录:它会就地删掉该目录里白名单之外的扩展名文件。指向源码目录会删掉用户的代码。',
'- `.export/` 不进项目快照:换机或恢复版本后适配成果不跟随,需要重新适配。这不用你处理,知道即可。',
'',
'最后:在 npm 包根跑一次 `npm run ' +
XHS_MINITOOL_SCRIPT_NAME +
'`,确认 zip 真的产出;',
'然后把这次适配的结论追加到 `.export/xhs-minitool.pack.mjs` 顶部的注释里(不超过 20 行:',
'用了哪些文件、zip 里是什么、下次要改什么、哪些只是权宜之计)。那份注释是给下次的你和用户看的。',
].join('\n');
}
/**
* 按失败现场修复的指令。
*
* 只带宿主的失败事实与同一份契约,不猜原因:agent 自己能跑脚本、能读日志。
*/
export function buildXhsMinitoolRepairInstruction(
projectPath: string,
error: XHSMiniToolExportError,
): string {
const notice = describeXhsMinitoolFailure(error);
return [
`【任务】修复「${XHS_MINITOOL_TARGET_LABEL}」导出失败,修到跑通为止。`,
'',
`项目根:${projectPath}`,
`宿主刚刚报的失败:${notice.message}`,
...(notice.detail ? [``, '宿主给的现场:', notice.detail] : []),
'',
'同一份硬契约(宿主会按它校验):',
`1. npm 包根(声明 package.json 的目录,通常是 game/)里声明脚本 \`${XHS_MINITOOL_SCRIPT_NAME}\`。`,
`2. 跑完它,\`${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\` 必须存在且非空。`,
`3. 打包装脚本的 \`--out-dir\` 只能指向 vite 构建输出目录,绝不能指向源码目录。`,
'',
`上下文:适配说明与上次的结论在 .export/xhs-minitool.pack.mjs 顶部注释里,示例脚本来自 \`${XHS_MINITOOL_SKILL_NAME}\` skill。`,
'',
`请直接动手:在 npm 包根跑 \`npm run ${XHS_MINITOOL_SCRIPT_NAME}\` 复现 → 改脚本 → 再跑通;`,
'把这次的修复结论追加到 `.export/xhs-minitool.pack.mjs` 顶部注释(不超过 20 行)。',
].join('\n');
}