diff --git a/apps/ai-game-creator-shell/tests/artifactsPanel.test.tsx b/apps/ai-game-creator-shell/tests/artifactsPanel.test.tsx deleted file mode 100644 index 30b178c3f..000000000 --- a/apps/ai-game-creator-shell/tests/artifactsPanel.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// @vitest-environment jsdom -/** - * 导出产物面板:卡片 → tab 的入口形态与关闭出口。 - * - * 面板状态(阶段 / 当前 tab)在面板根上,本用例只钉住「大卡进入 → 卡片收成 tab → 空 tab 页」 - * 这条可见链路,不覆盖右侧 chatbox 的订阅行为。 - */ -import { cleanup, fireEvent, render, screen } from '@testing-library/react'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -import ArtifactsPanel from '../src/view/project-development/export'; - -afterEach(() => { - cleanup(); -}); - -function renderPanel() { - const onClose = vi.fn(); - render( - true} - ensureConversationWriteAllowed={async () => true} - onClose={onClose} - />, - ); - return { onClose }; -} - -describe('导出产物面板', () => { - it('首屏用大卡片展示三个导出目标', () => { - renderPanel(); - - // 三个目标都以可点入口出现(`getBy*` 找不到就抛,不必再包一层恒真的断言)。 - screen.getByRole('button', { name: /陶泥儿/u }); - screen.getByRole('button', { name: /小红书小工具/u }); - screen.getByRole('button', { name: /TapTap H5 小游戏/u }); - }); - - it('点进目标后卡片收成顶部 tab,切到空目标只换 tab 页', () => { - renderPanel(); - - fireEvent.click(screen.getByRole('button', { name: /陶泥儿/u })); - screen.getByRole('tablist', { name: '导出目标' }); - screen.getByText('陶泥儿导出页尚未接入'); - // 切到另一个目标只换 tab 页,卡片不会重新长回来。 - expect(screen.queryByRole('button', { name: /陶泥儿/u })).toBeNull(); - - fireEvent.click(screen.getByRole('tab', { name: /TapTap H5 小游戏/u })); - screen.getByText('TapTap H5 小游戏导出页尚未接入'); - expect(screen.queryByText('陶泥儿导出页尚未接入')).toBeNull(); - }); - - it('小红书 tab 挂载的是主聊天同一个对话盒', () => { - renderPanel(); - - fireEvent.click(screen.getByRole('button', { name: /小红书小工具/u })); - - const surface = document.querySelector( - '.game-workbench-chat .project-chat-surface.is-direct-codex', - ); - expect(surface).not.toBeNull(); - expect(surface?.getAttribute('aria-label')).toBe('小红书小工具导出对话'); - // 对话盒里就是主聊天那套「顶栏 / 消息流 / 输入区」,不是另一份自造版式。 - expect(surface?.querySelector('.project-chat-conversation')).not.toBeNull(); - expect(surface?.querySelector('.project-chat-composer')).not.toBeNull(); - }); - - it('关闭按钮把销毁交给壳层', () => { - const { onClose } = renderPanel(); - - fireEvent.click(screen.getByRole('button', { name: '关闭导出面板' })); - expect(onClose).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx index 32288cc7b..4a4ea6d4a 100644 --- a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx +++ b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx @@ -86,8 +86,8 @@ describe('交给 code agent 的指令', () => { for (const { from, to } of XHS_MINITOOL_COPIED_SCRIPTS) { expect(instruction).toContain(`${from} → ${to}`); } - // pack.mjs 会删 --out-dir 里的非白名单文件,这条坑必须写进指令正文。 - expect(instruction).toContain('--out-dir'); + // pack.mjs 打包前会预检 --vite-built-dir:白名单外的文件让它整体失败,不会再就地删除。 + expect(instruction).toContain('--vite-built-dir'); // 包根在 game/ 时落点要回退一层,否则 zip 落在 game/.export/ 下,宿主必然报产物缺失。 expect(instruction).toContain(`../${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}`); }); diff --git a/docs/project-memory/plans/【实施计划】导出产物面板与小红书小工具导出-2026-10-05.md b/docs/project-memory/plans/【实施计划】导出产物面板与小红书小工具导出-2026-10-05.md index cc01faf88..3ff338113 100644 --- a/docs/project-memory/plans/【实施计划】导出产物面板与小红书小工具导出-2026-10-05.md +++ b/docs/project-memory/plans/【实施计划】导出产物面板与小红书小工具导出-2026-10-05.md @@ -14,7 +14,7 @@ - 复用而非新造:运行脚本走 `command_exec` 的 `resolve_project_command_spec_at → prepare/stage/spawn`(或 `run_project_verification_with_commit_at`),cwd 沿用 `project/export.rs` 的 `resolve_publish_build_cwd` 口径;icon 与 zip 下载复用 `save_local_project_asset_file`;不拿项目写锁、不推进 revision。 - 前端:`src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx`(表单 + 产物 + 冲突/失败卡片,只有「适配 / 导出」两颗显式按钮)、新增 `state/useXhsMinitoolExport.ts`(状态、防抖自动保存、按固定间隔自动重读内容;指纹只在 hook 内部的写回基线里,不进对外状态)、新增 `state/xhsMinitoolInstruction.ts`(适配与修复指令的纯函数 + 契约常量)、新增 `state/{xhsMinitoolApi,xhsMinitoolFailure,xhsMinitoolFields,xhsMinitoolDownload,xhsMinitoolOutputTail}.ts`、新增 `src/view/project-development/export/generated/`(ts-rs 产物)。宿主不预拼用户可见文案:输出尾部的「已省略前 N 个字符」与策略拒绝那句话都在前端拼(`xhsMinitoolOutputTail.ts` / `xhsMinitoolFailure.ts`)。失败分流按 ADR 两道口子:预期拒绝留在面板,宿主侧事实故障与认不出形状的拒绝先给现场再原样抛出(按指纹去重,面板 2 秒一轮重读不会把报告池的 `count` 刷成轮询次数)。 - 快照排除:`src-tauri/src/project/filesystem.rs` 新增 `PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS = [".export"]`;**不能**并进通用排除列表(`agent/direct_patch.rs` 直接拿它拒绝路径,`agc_apply_patch` 会拒 `.export/`,首次适配无法落地)。 -- skill:`resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs`(新增 `--zip-out `,相对 cwd 解析、父目录自动创建;原 `--zip ` 行为不变)、`scripts/vite.config.xhs-minitool.mjs` 注释点明 `outDir` 与 `--out-dir` 必须一致、`SKILL.md` 补「被 AGC 导出面板调用时」三条硬契约;同步 `manifest.json` 指纹与 `skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。 +- skill:`resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs`(新增 `--zip-out `,相对 cwd 解析、父目录自动创建;原 `--zip ` 行为不变;`--out-dir` 更名为 `--vite-built-dir`,并把原来的「就地删除白名单外文件」改成打包前预检报错、绝不动磁盘)、`scripts/vite.config.xhs-minitool.mjs` 注释点明 `build.outDir` 与 `--vite-built-dir` 必须一致、`SKILL.md` 补「被 AGC 导出面板调用时」三条硬契约;同步 `manifest.json` 指纹与 `skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。 - 文档:主规范一节、本里程碑与实施计划、`docs/README.md` 入口、`docs/project-memory/shared-memory/decision-log.md` 一条。 - 契约常量的单一来源:Rust `export/mod.rs` 的 `EXPORT_WORK_RELATIVE_DIR` 与 `draft/xhs_minitool/layout.rs` 是权威,前端 `state/xhsMinitoolInstruction.ts` 只为插值留一份副本;这份副本由 `tests/xhsMinitoolContract.test.ts`(读上面两个 `.rs`)逐条钉住,两边任一侧改名都会红。前端指令用例只钉「正文有没有把契约写进去」,不再抄一份字面量。 - 明确不动:`GAME_CREATION_APP_COMMANDS`、`shared-contracts`、`server-rs`、SpacetimeDB、OpenAPI、`api-server`,以及现有 `project/export.rs` 的发布链路。 @@ -33,15 +33,16 @@ - `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell export::` - 真实起进程跑宿主链路(默认 `#[ignore]`,要本机 node/npm 与可用命令沙箱):`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- --ignored executes_the_project_script` - `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`,随后 `npm run check:generated-bindings` -- `npx vitest run apps/ai-game-creator-shell/tests/artifactsPanel.test.tsx apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx` +- `npx vitest run apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx` - `npm run ai-game-creator-shell:typecheck` - `npm run agc:skill-pack:sync` → `npm run agc:skill-pack:check` +- `node --test apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs`、同目录 `.validate.test.mjs` - `npm run check:doc-index`、`npm run check:encoding`、`git diff --check` - 真实运行时:打开一个 vite 项目 → 点一键适配 → agent 跑通后不用点刷新(2 秒内导出按钮自己亮起来)→ 点导出 → 确认 `.export/xhs-minitool.zip` 可下载、内容可交给平台;再次导出确认零 agent 调用;手改坏注册表、删掉脚本各验证一次 rich error 文案。 ## 风险与回滚点 -- `pack.mjs` 会就地删掉 `--out-dir` 内非白名单扩展名的文件;prompt 与 skill 都要求把 `--out-dir` 指向 vite 产物目录,绝不能指向项目源码目录。 +- `pack.mjs` 在打包前预检 `--vite-built-dir`:白名单外的扩展名直接报错退出,不再就地删除;prompt 与 skill 都要求把它指向 vite 产物目录并与 `build.outDir` 一致,配错只会失败,不会误删项目源码。 - `.export/` 排除出快照同步后适配成果不跟随恢复;prompt 与面板文案要说清「换机或恢复后需重新适配」,避免用户误以为已持久化。同机 checkpoint 仍含 `.export/`,是恢复适配脚本的兜底。 - `.export/` 一旦被并进通用排除口径,`agc_apply_patch` 会以「不得修改受保护或排除路径」拒绝它,首次适配直接卡死;两侧口径差别由 `project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials` 钉住。 - agent 写注册表是软约束(只靠 prompt 要求);宿主严格解析必须 fail closed,解析失败一律给 rich error 并引导重新适配,不做猜测性合并。 @@ -64,7 +65,7 @@ - 复制按钮改用 Tauri 剪贴板插件(原 `navigator.clipboard` 在 WebView 里可能根本没有,失败还会静默吞掉),失败态在按钮上可见。 - 指令正文里的契约字符串全部从常量组合(`XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH` + 三个脚本落点),用例的期望值也从常量推;`9d3018a49` / `6dd0dc00c` 之后:去掉刷新按钮,改成按固定间隔自动重读内容;内容与注册表指纹拆成两条命令(自动刷新拿不到写回基线);同步细节(指纹、保存中、待保存)退出前端对外状态与界面。 -已验证:Rust `export::` 53 例(`export::draft::xhs_minitool` 37 例)、`export_bindings` 116 例、`project_snapshot` 22 例、`check:generated-bindings`(118 个文件)、前端 vitest 25 例(新增 19 + 契约 2 + 既有 4)、`ai-game-creator-shell:typecheck`、`ai-game-creator-shell:check:rust:shell`(本机 4 个 shard 都跑到了底,唯一失败是 4 个 `process_session` 真 PTY 用例——与下面的已知环境噪声同一批:沙箱里的 npm 由另一个 node 版本执行、`Cannot find module '../lib/cli.js'`;本次改动涉及的 `export::` 与 `project_snapshot` 全绿)、`agc:skill-pack:check`、`check:doc-index`、`check:encoding`、`git diff --check`。 +已验证:Rust `export::` 53 例(`export::draft::xhs_minitool` 37 例)、`export_bindings` 116 例、`project_snapshot` 22 例、`check:generated-bindings`(118 个文件)、前端 vitest 21 例(新增 19 + 契约 2)、`ai-game-creator-shell:typecheck`、`ai-game-creator-shell:check:rust:shell`(本机 4 个 shard 都跑到了底,唯一失败是 4 个 `process_session` 真 PTY 用例——与下面的已知环境噪声同一批:沙箱里的 npm 由另一个 node 版本执行、`Cannot find module '../lib/cli.js'`;本次改动涉及的 `export::` 与 `project_snapshot` 全绿)、`agc:skill-pack:check`、`check:doc-index`、`check:encoding`、`git diff --check`。 未验证(唯一开口项):真实 vite 项目上的首轮适配闭环——agent 跑通 `build:xhs-minitool`、产出可上传 zip、二次导出零 agent 调用;以及 `.export/` 换机后重新适配的体感。宿主侧这一段已尽力自动化:`build.rs` 的 `executes_the_project_script_and_requires_a_non_empty_artifact_each_time`(`#[ignore]`)会真起进程执行项目脚本,覆盖「第一次产出 → 删掉产物再跑一次重新产出 → 脚本成功但没产出报 artifactMissing」。本机尝试运行被容器环境挡下,读日志时注意两点、别误判成 bug: - 子进程输出里的 `running 1 test` 是**预期现象**:命令沙箱的垫片就是当前可执行文件本身(`desktop.rs` 按 `--command-sandbox-trampoline` 分流),编译到测试态时 `sandbox_trampoline_arguments()` 会改成 `--exact ...trampoline_child_fixture --ignored`,所以垫片进程是测试二进制在跑那一个 fixture 用例。 diff --git a/docs/project-memory/plans/【里程碑】导出产物面板与小红书小工具导出-2026-10-05.md b/docs/project-memory/plans/【里程碑】导出产物面板与小红书小工具导出-2026-10-05.md index 157905695..02fc06f28 100644 --- a/docs/project-memory/plans/【里程碑】导出产物面板与小红书小工具导出-2026-10-05.md +++ b/docs/project-memory/plans/【里程碑】导出产物面板与小红书小工具导出-2026-10-05.md @@ -17,7 +17,7 @@ - 注册表 `.export/xhs-minitool.json`(首次自动创建且字段全空)与 flat 工作目录 `.export/`;共享 icon 放 `.export/` 根,多目标可指同一文件。 - 前端导出面板小红书 tab:表单展示/编辑/复制、icon 与 zip 下载、显式「适配 / 导出」两颗按钮(内容与脚本判据自动重读,没有刷新按钮)、冲突时逐字段二选一。 - 前端组装首次适配指令,经会话写权限门入队到项目主 Direct 会话。 -- `vite-export-xhs-minitool` skill 的脚本与示例配置按 `.export/` 布局适配(`pack.mjs` 新增 `--zip-out ` 指定确定落点、vite config 示例注释点明 `outDir` 与 `--out-dir` 必须一致)。 +- `vite-export-xhs-minitool` skill 的脚本与示例配置按 `.export/` 布局适配(`pack.mjs` 新增 `--zip-out ` 指定确定落点、`--out-dir` 更名为 `--vite-built-dir` 并在打包前预检该目录,vite config 示例注释点明 `build.outDir` 与它必须一致)。 - 主规范一节、本里程碑、实施计划与决策记录同步。 ## 不在范围内 @@ -52,8 +52,9 @@ - 自动化(已落): - Rust `cargo test ... --bin genarrative-ai-game-creator-shell export::` 53 例,其中 `export::draft::xhs_minitool` 37 例:注册表自动建空表单、格式漂移静默吸收、严格解析拒绝、表单首错、icon 越界/符号链接/扩展名、`hasScript` 现算、`contentHash` 冲突、产物路径守卫,以及构建收尾判据的逐条映射(脚本缺失、非零退出带退出码与输出尾部、超时无退出码、产物缺失、空产物不算、成功回输出尾部、截断只回省略量不回句子;layout 的 4 例只钉关系不抄实现——前端字符串相对路径与宿主 `PathBuf` 必须指向同一文件、相对路径不许跑出 `.export/`(含 Windows 反斜杠)、注册表与产物共用一个项目根锚定的 flat 目录、脚本名保住 `build:<后缀>` 形式且不许退化成裸 `build`)。 - `export_bindings` 116 例 + `npm run check:generated-bindings`(118 个生成文件)通过。 - - 前端 `npx vitest run tests/xhsMinitoolExport.test.tsx`(19 例)、`tests/xhsMinitoolContract.test.ts`(2 例,把前端契约常量的值逐条对到 Rust `layout.rs` / `export/mod.rs`)与既有 `tests/artifactsPanel.test.tsx`(4 例)通过;`npm run ai-game-creator-shell:typecheck` 通过。其中 19 例覆盖(期望值一律从契约常量推,不另抄字面量):指令正文三条硬契约与 game/ 落点、失败现场拼装、失败变体人话映射与三向 action 分派(适配 / 修 / 重试)、未适配卡片自带入队按钮且按钮行不重复、注册表读回来之前不下「还没适配」结论、表单复制、icon 与 zip 下载、冲突逐字段二选一、自动保存带回读到的 `baseHash`、自动重读只更新脚本判据而不动用户未保存的表单、agent 事后改注册表/加脚本不用点刷新就能带回来、导出把脚本输出尾部报给用户、失败变体的产出量与前端拼出的省略说明、认不出形状的拒绝原样抛出且不落成业务提示、宿主侧事实故障先给卡片再抛一次且同一故障只抛一次,复制失败在按钮上可见(不静默吞掉),以及「只有适配/修复才叫 agent」。 + - 前端 `npx vitest run tests/xhsMinitoolExport.test.tsx`(19 例)与 `tests/xhsMinitoolContract.test.ts`(2 例,把前端契约常量的值逐条对到 Rust `layout.rs` / `export/mod.rs`)通过;面板外壳原先那份 `tests/artifactsPanel.test.tsx`(4 例)整份删除——三个目标的列表与切 tab 后的文案都是存在性断言,关闭按钮只是把 `onClose` 转发一次,而它唯一有内容的「挂载的是主聊天同一个 `.project-chat-surface`」已被 `chatDialogFrameLayout.test.ts` 覆盖;`npm run ai-game-creator-shell:typecheck` 通过。其中 19 例覆盖(期望值一律从契约常量推,不另抄字面量):指令正文三条硬契约与 game/ 落点、失败现场拼装、失败变体人话映射与三向 action 分派(适配 / 修 / 重试)、未适配卡片自带入队按钮且按钮行不重复、注册表读回来之前不下「还没适配」结论、表单复制、icon 与 zip 下载、冲突逐字段二选一、自动保存带回读到的 `baseHash`、自动重读只更新脚本判据而不动用户未保存的表单、agent 事后改注册表/加脚本不用点刷新就能带回来、导出把脚本输出尾部报给用户、失败变体的产出量与前端拼出的省略说明、认不出形状的拒绝原样抛出且不落成业务提示、宿主侧事实故障先给卡片再抛一次且同一故障只抛一次,复制失败在按钮上可见(不静默吞掉),以及「只有适配/修复才叫 agent」。 - `.export/` 快照排除有定向用例:`project_snapshot::tests::project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials` 同时钉住「同步排除」与「通用口径不排除(Agent 仍可写)」。 - - `npm run agc:skill-pack:sync` → `agc:skill-pack:check` 通过(`manifest.json` 版本 `2026-08-26.42`,文件数不变,仍 36 条)。 + - `npm run agc:skill-pack:sync` → `agc:skill-pack:check` 通过(`manifest.json` 版本 `2026-08-26.43`,文件数不变,仍 36 条)。 + - `pack.mjs` 的 no-delete 预检有定向用例(`scripts/.pack.test.mjs` 5 例):`findUnsupportedFiles` 只报白名单外扩展名、存在不支持文件时整体失败且不删任何文件/不写 zip、干净目录仍能打包、CLI 拒绝旧名 `--out-dir` 不静默退回默认目录。 - 运行时(**未验证**,本里程碑唯一开口项):在真实 vite 项目上由 agent 完成首轮适配、产出可人工上传的 zip、二次导出零 agent 调用;`.export/` 换机后需重新适配的体感也需一并确认。宿主侧这一段已有默认 `#[ignore]` 的真起进程用例(`executes_the_project_script_and_requires_a_non_empty_artifact_each_time`:第一次产出 → 删掉产物重跑必须重新产出 → 脚本成功但没产出报 artifactMissing),一键复跑命令见实施计划;本次尝试被容器环境的 npm/沙箱解析挡下,不是代码问题。 - 边界(已覆盖):注册表损坏、脚本缺失、脚本非零退出、产物缺失、icon 越界与符号链接、目录遍历拒绝。