Feat/适配小红书小工具导出 #630
Reference in New Issue
Block a user
Delete Branch "feat/adapt-xhs-skill"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
- 里程碑证据:Rust export:: 改为 49 例(export::draft::xhs_minitool 33 例)、export_bindings 115 例、前端 vitest 15 例,并把构建收尾判据的六条映射逐条列出(此前把「脚本非零退出/产物缺失」写成已有测试覆盖,实际只有代码分支,现补齐)。 - 实施计划:命令 DTO 口径改为 XHSMiniToolExportRunResult { outputTail } 并说明成功出口不带 exitCode 的理由;提交清单与已验证命令更新到最新;验收文案的「stderr 尾部」改为「输出尾部」(stdout 与 stderr 已合并)。- 指令正文不再写死任何契约字符串:工作目录、产物、注册表、三个脚本落点全从常量组合(原先 .export/xhs-minitool.pack.mjs 在正文里硬编码了三处,常量形同注释)。 - 前端用例不再抄一份字面量:期望值一律从契约常量推——拷贝清单改成遍历 XHS_MINITOOL_COPIED_SCRIPTS(漏列一条就红),game/ 落点写成 ../${ARTIFACT},下载相对路径、打包输出、提示匹配器同理。 - 新增 tests/xhsMinitoolContract.test.ts:读 Rust 的 export/mod.rs 与 draft/xhs_minitool/layout.rs,逐条要求前端常量等于权威值。副本没有校验就是会漂移的注释——改 Rust 漏改前端时,两边的用例原本各自都还绿。 - 两边都验过钉得住:把 Rust 的 ARTIFACT_FILE_NAME 改成 xhs-minitool-v2.zip、把前端 EXPORT_WORK_RELATIVE_DIR 改成 .exports,这条用例各自报出具体哪个常量对不上。 - ts-rs 不能绑 const 的结论在这里落成实际方案:值只由 Rust 说,前端副本的存在性由契约用例说,正文有没有用上常量由指令用例说。- 原来的 artifact_relative_path_stays_inside_the_export_directory 断言的是 format!("{EXPORT_WORK_RELATIVE_DIR}/{ARTIFACT_FILE_NAME}") 本身,只重复实现、挡不住任何真实改动 - 改成:前端字符串相对路径与宿主 PathBuf 必须指向同一文件;相对路径必须项目内、单层、无 .. 且不出现反斜杠(跨平台的线上字符串);注册表与产物共用一个项目根锚定的 flat 目录;脚本名保住 build:<后缀> 形式且不许退化成裸 build(否则跑掉的是项目自己的构建) - 逐条反证:把产物路径改成 .export/xhs-minitool/ 子目录、把 export_dir 挪进子目录、把 SCRIPT_NAME 改成 build,对应用例分别变红;export:: 50 → 53 例- `XHSMiniToolExportError` 一个大枚举拆成读 / 存 / 构建三份,每条命令只承载自己出得了的变体; - 三条命令的返回类型跟着换成本命令那份,策略拒绝(`commandDenied`)与宿主故障(`exportUnavailable`)在各自主语里分开; - 关键分支补 `app_log!`:命令入口、策略拒绝、脚本非零退出、产物缺失、注册表坏都留一行现场,出错时不必只靠前端转述; - 重新生成 ts-rs 绑定:删掉 `XHSMiniToolExportError.ts`,新增 `XHSMiniToolExport{Read,Save,Build}Error.ts`。- `xhsMinitoolExport`:删掉对生成文案的恒真断言(`toContain('…')` 那批)与只为它们存在的用例,改成行为断言与 IPC 调用断言; - 新增「帮我填」两条:落盘一定发生在把指令交给 agent 之前(顺序用调用表钉住)、表单不合法也照样交出去(agent 正是来收拾这份表单的); - 打包那颗按钮的用例按新行为改:没有脚本时点下去要开适配弹窗,而不是变灰; - `xhsMinitoolContract` 只留一条:工作目录、脚本名、注册表与产物路径与 Rust 权威逐条一致。- 新增内置 MCP 工具 agc_install_skill_resource(skillName/relativePath/destinationPath),宿主直接读 AGC_SKILL_PACK_FILES 已审核字节 - 组装成 agc_write_file 的 {path,content} 转发客户端工具桥,复用同一份合同、lease、项目写锁与保护面校验 - read/install 共用 skill_resource_relative_path_is_unsafe;模型自带正文无旁路,正文不过模型上下文,不依赖原生 cp 审批 - direct-tools.json 增补工具描述与 destinationPath,direct.json engineering 指引改用本工具复制 Skill 文件 - 新增两条 install_skill_resource 测试并登记工具目录;同步技术方案与 decision-log1.
scripts/check-generated-bindings.mjs:56(documentation·low)chat/generated+services/generated+features/ui-editor/types。targets实际是 5 个目录(shared 1 个 + AGC 4 个,含export/generated);而第 75 行又写「五个生成目录」,头部与实现、与自身下游注释都矛盾。export/generated。677c477f32.
apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs:504(performance·medium).export只登记在(原)PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS,索引、checkpoint、git 检查走的是通用口径,看不到它。.agent/checkpoints/.../files/;git_inspect把.export当未跟踪文件、变成可提交内容。.export,而不是只从同步路径排除;同时保留 Agent 的可读写(写入门禁不能一起拒)。should_skip_project_derived_path(快照口径 +.export),索引、checkpoint 恢复与清单、git_inspect两处过滤全部改用它;常量改名PROJECT_DERIVED_EXCLUDED_COMPONENTS;project_snapshot测试补断言——写入门禁仍放行.export,索引 / checkpoint / 数据口径排除它。commit065a193e43.
apps/ai-game-creator-shell/src/App.tsx:1763-1767(maintainability·low)localProject?.projectPath || projectPath,面板 prop 用空值判断?? projectPath ?? null;两处判据不一致。Boolean(projectPath)/.trim()当有效判据,会出现「能打开但用不了」的半开状态。(localProject?.projectPath ?? projectPath ?? '').trim() || null。exportProjectPath,onRequestExport与ArtifactsPanel的projectPath都用它(DirectProjectChatView的 prop 保持原样,避免动聊天容器)。commit411771af24.
apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/registry.rs:46-49(bug·medium)save先load_or_create读当前内容、比较base_hash,再调共享的crate::export::save无条件写回;比较与写入是两步,无锁。base_hash(用户保存 vs code agent 改.export/xhs-minitool.json)都能通过比较,后一个rename静默盖掉前一个。save,或按项目文件串行化读改写。save的读与写之间加注释,写明这是「用户手动触发、且明知 code agent 在改」的已知窗口,明确接受、不做 CAS;并写明不做 CAS 的成本原因(会改所有导出目标的写契约)。commit158fae5875.
apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs:154-155(maintainability·low)log_failure用format!("{error:?}")把完整 Debug 落进app_log!,并原样存进LAST_FAILURE去重表。CommandFailed的 Debug 里带最多 4,000 字符的脚本输出尾部,一次失败就能把 AppData 日志撑大 4KB;去重表也长期留这份长字符串。bounded_for_log按字符截到 512 并补省略号(保证不切坏 UTF-8),保留原有「同一失败只记一次」语义。commit9722b42696.
apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs:31-34(performance·low)async fn,但内部read_content/read_content_hash/save_form直接做同步文件 IO。spawn_blocking(或异步 fs)。offload,用tokio::task::spawn_blocking包住三个同步函数;JoinError收口成ExportUnavailable,走各命令既有失败表,不需要新失败变体 / 不需要改 ts-rs 绑定。commit63b7ef3667.
apps/ai-game-creator-shell/src-tauri/src/export/registry.rs:148-154(bug·medium).{name}.{pid}.tmp,同进程内所有并发写共用同一个临时文件。persist与「打包」前的 flush 可能同时在写同一个注册表:一个任务会截断另一个正在写的临时文件,rename也会互相抢(甚至移走半截内容)。.{name}.{pid}.{seq}.tmp)。串行化属于第 4 条,留给你。commit32d77a0cc8.
apps/ai-game-creator-shell/src-tauri/src/export/registry.rs:67-69(bug·low)fs::read_to_string对非 UTF-8 字节返回ErrorKind::InvalidData,落到load_or_create的通用Io分支。ExportUnavailable(宿主 / 路径故障),而契约上「内容不是期望形状」应该是 fail-closed 的Malformed;这会把用户和 agent 往「查权限」方向带。InvalidData映射成RegistryError::Malformed。InvalidData分支返回Malformed,并补rejects_non_utf8_content_as_malformed用例。commitc9d2ed0419.
apps/ai-game-creator-shell/src-tauri/src/export/registry.rs:139-145(bug·low)write_atomically只对临时文件sync_all,随后rename换名,但没有 fsync 父目录。rename只改目录条目;崩溃后数据虽然在磁盘上,目录条目可能还是旧的(甚至没有),注释里声称「已关掉崩溃窗口」并不成立。rename之后 fsync 父目录。rename成功后File::open(parent)再sync_all(Windows 打不开目录句柄,if let Ok自然跳过)。commit9c48db76b10.
apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx:229(bug·low)hydrateClientServerSelection()在withAuthCheckTimeout起 15s 时钟之前被 await;它自己吞异常、不会 reject。read_workspace_preferencesinvoke 一旦卡住,认证 UI 会永远停在「刷新登录状态」,既无超时提示也无重试路径。withAuthCheckTimeout围起来。withAuthCheckTimeout(..., AUTH_CHECK_REQUEST_TIMEOUT_MS, '读取服务器偏好超时,请重试或重启客户端')。commite96d38a0111.
apps/ai-game-creator-shell/src/view/project-development/export/components/TargetTabs.tsx:29-30(other·low)role="tablist"/role="tab"/aria-selected,但导出目录里没有任何role="tabpanel"/aria-controls/ 方向键 / rovingtabIndex。tablist/tab角色,退回普通按钮。role="group",选中态改aria-current;Tab/Enter 行为不变。commitc7629606112.
apps/ai-game-creator-shell/src-tauri/src/export/registry.rs:127(security·low)file名字 + pid),且用fs::File::create打开——它会跟随符号链接。create_new(true)(O_EXCL)打开,遇到已存在条目(链接或残留)直接失败。OpenOptions::new().write(true).create_new(true)。配合第 7 条的序号,正常路径不会误撞。commit07cc0aa5f14.
apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectChatHeader/DirectProjectChatHeader.tsx:31-38(bug·medium)disabled={busy};改成onRequestExport后这道门丢了。disabled={busy}。disabled={busy};设置入口不受影响,禁用样式沿用现有.project-chat-publish-trigger:disabled。commit226a339ce15.
apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts:236-239(bug·high)manifest?.projectId为依赖,但只重置文本字段;cover/screenshots/uploadedAssetCacheRef/objectUrlsRef会跨项目存活。cover/screenshots/ 上传缓存,并回收全部登记过的预览地址;抽出revokeTrackedObjectUrls供重置与卸载共用。commit3061762aa16.
apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts:577-578(bug·low)handlePackageAndPublish里pkg = await onEnsurePackage()没有 try/catch。void handlePackageAndPublish()调用;回调一旦 reject 就是未处理的 promise rejection,用户只看到按钮弹回、没有任何失败说明,与文档承诺的「失败留在表单」不符(现有实现恰好自己吞错,但契约没保证)。reportError(...)。这里故意没用评审建议的setError:新架构里左栏内联红字已退役,「发布提示」弹窗才是错误出口,reportError才会驱动它。commitf4b1a6ad917.
apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts:349-350(bug·low)setCover(null)移除封面、AI 生成覆盖,都没有释放上一张的URL.createObjectURL预览地址;只有截图删除和卸载会 revoke。handleCoverSelected里单点打补丁,而是新增previousCoverRef+ 跟随cover的 effect,统一覆盖「换 / 移除 / AI 覆盖」三条路径;只回收确实登记在objectUrlsRef里的 blob 地址。commit10ca2341a18.
apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx:118-122(maintainability·low)resolveMetadataHint(pending, ready)或在 return 前先算好。resolveMetadataHint纯函数。commita3af8a60a19.
apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishFormView.tsx:399-409(maintainability·low)resolvePublishActionLabel(...)。resolvePublishActionLabel纯函数(if/else 从上到下即优先级)。commit2b8bcd68c20.
apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs:79-83(bug·medium).export/xhs-minitool.zip是否存在且非空,不验证是不是这一次跑出来的。remove_file才能让 no-op 用例失败。NotFound),或记录本次开始时间并比较 mtime。run()在跑脚本前删除旧产物;不存在则跳过,其它删除失败则 fail closed(留着旧产物继续判成功更糟)。conclude仍是纯函数,原有单测不受影响。commitdc639d57121.
apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs:81(maintainability·low)std::fs::metadata会跟随符号链接,与本模块其它地方(script.rs、validation.rs、resolve_local_project_path)的 fail-closedsymlink_metadata口径不一致。symlink_metadata并拒绝链接 / 非普通文件。symlink_metadata,并要求is_file() && !is_symlink(),补a_symlinked_artifact_does_not_count(仅 unix)用例。commiteee12b28022.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/common/PaneButton.tsx:30-35(maintainability·low)variant建查表。TONE_CLASS,const tone = TONE_CLASS[variant]。commit761dced5623.
apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts:203-208(bug·medium)ensurePackage的await回来后只检查mountedRef.current。export_local_project_package仍会把setPackageResult/setProgress写进当前面板,发布表单会消费错项目的包。useDirectProjectManifest.refresh,用projectPathRef把捕获的路径和当前路径对账。projectPathRef与isCurrentProject(),成功与失败两条路径都先对账再写状态。commit6c540e06724.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/common/PaneButton.tsx:36-41(maintainability·low).eslintrc.cjs里并没有no-nested-ternary规则(我查过 rules 段)。可读性问题本身成立。size建查表。SIZING_CLASS,const sizing = SIZING_CLASS[size]。commitb77ebbc1b25.
apps/ai-game-creator-shell/src/view/project-development/export/state/useXhsMinitoolExport.ts:189-194(bug·medium)refresh先setForm(content.form)再取指纹;内容读成功、指纹读失败时baselineRef留在旧值,而下一次轮询因sameForm早返回,指纹永远补不上。baseHash,被宿主当成外部改动——要么假saveConflict,要么字段恰好相同被静默吸收。hashPendingRef;指纹读失败置真并原样 rethrow(失败仍照常上报),早返回条件加!needsHashRetry(hashPending && adoptable),下一次轮询跳过它把配套指纹补上,用户打字中(adoptable为 false)仍不覆盖输入;保存命令的返回值直接落定基线并清 pending。补一条回归测试:指纹失败后重读补上,保存带回新hash-2。commitfba3805af26.
apps/ai-game-creator-shell/src/view/project-development/export/state/useXhsMinitoolExport.ts:344-352(maintainability·low)runExport由 hook 返回但生产侧无人调用(失败弹窗刻意没有重试按钮),只有xhsMinitoolExport.test.tsx在调;且它setRunning(true)前不查重入。runExport与返回项,buildOnce现在只由packageAll调用(packageAll自己持有 running 门);测试改用packageAll()驱动构建路径,删掉对应 mock 与断言。commit91b664b6627.
apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFields.ts:47-52(maintainability·low)XHS_MINITOOL_FIELD_LABELS全仓没有任何 import,只被定义;标签又与XHS_MINITOOL_FORM_FIELDS重复。5a202b09128.
apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolInstruction.ts:91(style·low)${JSON.stringify(...)}之后多一个逗号,输出成 JSON 块与下一条要点之间单独一行,。ee8ad27c229.
apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolDownload.ts:48-50(other·low)saveXhsMinitoolFileToDisk把save_local_project_asset_file的失败只用console.warn吞掉,写盘失败与用户取消在返回值上无法区分。'saved' | 'cancelled' | 'failed';ArtifactsPane在'failed'时于表单上方给一行可重试提示,成功与取消保持静默;补一条「宿主拒绝写盘时提示可见」测试。commit7f9c686b330.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FailureDialog.tsx:45-47(maintainability·low)'让陶泥儿帮我调通' | '让陶泥儿来修',后面又用agentAction === '让陶泥儿帮我调通' ? onAdapt : onRepair反查 handler。agentAction改为{ label, onClick },点击直接用agentAction.onClick。commit01b068dec31.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ConflictCard.tsx:116-122(other·low)name,只靠受控checked工作。name传进Choice。Choice新增nameprop 并透传给input,同字段两个选项共用meta.field。commit433baadb432.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FormCard.tsx:124-126(maintainability·low)FieldRow声明、解构并渲染action?: ReactNode,但本文件没有任何调用方传它。ReactNode导入都是死重量,还会诱使后来者以为有现成插槽。actionprop、渲染分支与ReactNode导入。commitf27aface733.
apps/ai-game-creator-shell/src/view/project-development/export/state/useTaonierTab.ts:92-97(bug·high)packageResult/progress/blockedMessage在projectPath变化时从不重置,而面板跨项目切换不卸载。packageResult会活到项目 B;发布表单复用现成的packageResult、不再调onEnsurePackage,于是项目 B 可能被用项目 A 的packageRelativePath发布;旧项目的进度 / 阻断弹窗也会继续挂着。projectPath为依赖的 effect 清掉这三项,和上面的灰度重置同一口径。abd27834d34.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx:85-88(maintainability·medium)form ? <FormCard/> : !readSettled ? <p>正在读取…</p> : failure ? null : <p>没能读到…</p>的四路链式三元。return之前用if/else算好formSlot,JSX 里只留{formSlot}。formSlot(if / else if四路),JSX 只剩一个插槽。注:这条原本因为该文件当时有你未提交的本地改动而打算留给你;你在 19:02 把它提交成
499b24723之后工作区干净了,我就按建议补做了。commit1897bf54d35.
apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/CopyButton.tsx:30(style·low)${text}${label},例如「已复制小工具名称」。${text} ${label},并同步更新xhsMinitoolExport.test.tsx里 6 处无障碍名断言(否则 3 个用例会红)。commitb9c0cd36936.
apps/ai-game-creator-shell/src/components/game-distribution/useGameDistributionPublishForm.ts:408-410(bug·medium)createPanelScreenshotEntry直接调buildPanelImagePreviewUrl(file),不像封面路径那样objectUrlsRef.current.add(previewUrl)。removeScreenshot里的 revoke 分支永远命中不了,卸载清理也只遍历objectUrlsRef——截图 blob URL 会泄漏到页面关闭。if (previewUrl) objectUrlsRef.current.add(previewUrl)。commit49d6f8db237.
apps/ai-game-creator-shell/src/view/project-development/export/state/useXhsMinitoolExport.ts:189-194(bug·medium)refresh的早返回跳过了紧随其后的setFailure(null);读来源的失败(registryMalformed/exportUnavailable)只有采纳表单时才会清。setHasScript之后用函数式更新setFailure((previous) => ...)只清registryMalformed/exportUnavailable;用previous避开「读failure会闭包过期」,不必加 ref、也不用改 deps,构建类失败不受影响。补一条回归测试:读恢复后失败就地清掉(构造「表单值不变」的早返回路径,确保不是被构建顺带清掉的)。commitfba3805af- FailureDialog 的 agentAction 改为 { label, onClick },不再按中文文案反查 onAdapt / onRepair- DirectProjectChatHeader 给导出按钮恢复 disabled={busy},与旧发布入口一致 - 设置入口不受影响,禁用样式沿用现有 .project-chat-publish-trigger:disabledexport/state/useTaonierTab.ts:换项目重置效果补上 setDraft([]) 与 setRuntimeNotice(''),旧项目没发出去的输入不再出现在新项目输入框。