Compare commits

...

186 Commits

Author SHA1 Message Date
k88936 19e1883f71 合并 origin/master:策划附件导入接入策划聊天入口
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m55s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m54s
Project CI / Backend tests (pull_request) Successful in 5m4s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m18s
Project CI / Native shell tests (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m32s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m29s
Project CI / Repository checks (pull_request) Successful in 1m42s
- 保留 DirectProject 独立聊天容器与 Supervisor 退役结果,不为冲突恢复运行时兼容分支
- 策划工作区文件导入接入 PlanningChatView 输入盒,导入期间暂缓发送、阶段批准、澄清与重试
- 工作台壳按项目记录导入进行态,导入未完成时不切换游戏运行态并提示等待
- 首页策划附件导入结果落到工作台 fileImportNotice 提示条,可单独关闭
- 设计态输入盒样式选择器由 project-supervisor-* 收敛为 project-chat-*
- 策划导入回归用例适配退役后的 harness 与 chatProps 命名
2026-09-22 02:24:47 +08:00
lhk229 4e41bd3e56 修复预览发布测试状态文件恢复竞态
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m54s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m49s
Project CI / Native shell tests (pull_request) Successful in 6m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m45s
Project CI / Frontend tests (pull_request) Successful in 5m10s
Project CI / Repository checks (pull_request) Successful in 3m57s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m26s
Project CI / AI game creator shell Rust crates (push) Successful in 1m16s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m11s
Project CI / Backend tests (push) Successful in 4m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m50s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m2s
Project CI / Native shell tests (push) Successful in 7m2s
Project CI / Repository checks (push) Successful in 3m51s
Project CI / AI game creator shell web tests (push) Successful in 3m23s
Project CI / Frontend tests (push) Successful in 5m48s
deploy_and_uninstall 测试在断言状态文件恢复前,改为轮询等待持久化状态到达 Stopped

新增 wait_for_persisted_status 辅助函数,经 load_deployments 真实加载路径校验状态文件
2026-09-22 00:53:47 +08:00
k88936 5315b233c8 预览激活回接「运行」入口,聊天 @ 清单打开选择器时按需重读
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m15s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Frontend tests (pull_request) Successful in 3m8s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 1m56s
- App.tsx 新增 activateRunningPreview,运行入口先问 Rust 要活体预览(activate_local_game_preview),命中就只切客户端运行视图
- App.tsx 的 executeRunLocal 命中活体预览时经 DirectProject 聊天的 announce 播报「已切换到客户端运行视图」,不再重复调用 start_local_game_preview;stopped / 非本项目 / 权限位要求确认一律回落到重新启动
- check-config.mjs 从 native-only 白名单移除 activate_local_game_preview 并同步注释(该门禁要求 App invoke 与白名单互斥)
- DirectProjectComposer 新增 onReferencePickerOpen,工具条按钮与附件菜单两处选择器入口打开前先刷新清单
- DirectProjectChatView 用 useDirectProjectManifest 的 refresh 重读聊天自己那份 @ 清单,不新增壳到聊天的清单推送通道
- 新增 tests/previewActivation.test.tsx,钉住切视图不重启 / stopped 回落重启 / 权限确认回落重启三条路径
- decision-log 记录本轮两处回归的背景、决策、取舍与验证方式
2026-09-21 23:09:43 +08:00
k88936 a11cdf13d7 画布生成面板重开草稿与重试身份收敛到 canonical content
生成面板 state、重开草稿、重试身份与提交载荷统一为一份 content[]
重试身份改用 directCodexContentKey 内容指纹比对并删除 referenceIdsMatch
directCodexContentToLegacyContentDto 只在任务落账的出站边界派生
账本回到草稿的反向翻译收敛为 legacyContentDtoToContent 单一实现
token 扫描从 ResourceReferenceInput 提到 resourceReferences 并两处共用
无法解析的资源引用合成 unknown 占位而不再静默丢弃
出站提示词规范化收敛为 resourceCanvasAssetGenerationPromptText 供两处共用
directCodexContentToPromptText 去掉 trim,空白只在整条 content 上判定
同步决策记录与 canonical content 里程碑落地结果
2026-09-21 22:56:32 +08:00
lhk229 085181c795 修复策划附件导入工作区并解除资源清单耦合
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m55s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m21s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m40s
Project CI / Backend tests (pull_request) Successful in 5m37s
Project CI / Native shell tests (pull_request) Successful in 7m30s
Project CI / Repository checks (pull_request) Successful in 3m46s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m22s
Project CI / Frontend tests (pull_request) Successful in 4m54s
Project CI / AI game creator shell Rust crates (push) Successful in 2m57s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m51s
Project CI / Backend tests (push) Failing after 3m47s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m22s
Project CI / Native shell tests (push) Successful in 6m12s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m13s
Project CI / Frontend tests (push) Successful in 3m51s
Project CI / Repository checks (push) Successful in 2m44s
Project CI / AI game creator shell web tests (push) Successful in 2m25s
新增策划工作区文件导入命令,支持同名另存并保留文件字节
首页和聊天框附件直接导入策划工作区,脱离游戏资源清单与回合附件
导入期间暂缓发送、阶段操作和游戏制作切换,保留逐文件失败提示
补充导入、刷新与切换回归测试,同步技术方案和共享约定
2026-09-21 22:49:21 +08:00
k88936 8968762274 合并 origin/master:DirectProject 输入校验取回空白片段回归用例
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m47s
Project CI / Native shell tests (pull_request) Failing after 50s
Project CI / Frontend tests (pull_request) Failing after 1m50s
Project CI / Backend tests (pull_request) Successful in 4m45s
Project CI / Repository checks (pull_request) Successful in 2m1s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m31s
- validation.rs 保留本分支的 content_has_meaningful_input 与 Skill/附件独立上限,空输入报错取回 master 的用户可见文案“聊天内容不能为空”
- wire.rs 取回 master 新增的 text_projection / user_input_rejects / valid_references / nonempty_text 四条回归用例,测试导入合并两侧
- resourceReferenceInput.test.tsx 取 master 的注释口径:草稿 text 投影会 trim,提交用的 content 保留 chip 后的分隔空格
- pitfalls.md 与《AGC聊天素材引用》改为「任意非文本 part 均算有效内容」,与四类引用的实际校验对齐
- 本地私有改动的 .env 不进入本次合并
2026-09-21 21:42:41 +08:00
k88936 7745264990 同步canonical content里程碑落地结果与润色口径
里程碑与实施计划状态改为 implemented

记录润色按 token 原位恢复引用与附件、被改写 part 末尾补位

记录遗留 TODO:润色删掉 token 时无法反推原位置
2026-09-21 21:23:17 +08:00
k88936 295d4af384 润色回写按token原位恢复引用与附件
统一引用与附件的润色扫描口径,token 存活时原位换回真 part

token 被改写时剩余 part 补在末尾,附件不再丢出 canonical content

删除无调用方的 EMPTY_CHAT_COMPOSER_DRAFT

补充润色原位恢复与末尾补位两条回归用例
2026-09-21 21:22:04 +08:00
k88936 6981648796 合并 origin/master:Supervisor 永久退役,项目对话收敛为 DirectProject 与 Design Agent
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m48s
Project CI / Native shell tests (pull_request) Failing after 45s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Failing after 1m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m42s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m1s
- 解决 refactor/split-direct-project 与 origin/master 在 App.tsx、立项策划聊天视图、Direct composer/引用输入区、styles.css、Rust direct user item 与 appSurface 用例上的冲突,按「Supervisor 永久退役」口径保留 DirectProject 独立聊天容器与 Design Agent 两条产品路径
- 采纳 master 的策划 Agent V1/V2 退役:删除 GDD 审批卡、策划输入卡、planningLane、planningSessionV2、planningSessionContract、规划展示适配与 Rust planning_*_v2 命令、模块、契约及对应用例,不保留兼容别名或双跑路径
- 把 master「折叠思考显示单行预览」的目的落到当前结构:新增共享表现 chat/components/AgentReasoning/AgentReasoning.tsx(折叠态单行纯文本预览 + 箭头、展开态安全 Markdown),DirectProject 回合与策划回合共用,删掉两处写死的 pre 折叠实现
- 把 master「策划入口可选模型 / 推理档」的目的接到当前策划输入盒:复用 ConversationModelSelect 与 ComposerReasoningEffortSelect,配置写回仍走客户端配置通道
- App.tsx 删除只服务退役 Supervisor / 策划 V2 的 state、ref、effect、回调与死参数,并删除两条读路径都退役后的 workspaceProjectKind;openWorkspace 的工程类型入参保留为未使用契约
- Rust 侧保留本分支 canonical→wire 投影、无审计 Direct 回合与 direct user item 严格校验,并入 master 的 prepare_new_web_project_at 前置复核
- 更新 ADR 与 shared-memory 决策记录:策划当前只有 Design Agent、两条路径的共享表现清单,以及本次合并的口径、代价与验证证据
- 验证:AGC 与仓库 typecheck、check:encoding、check:doc-index、git diff --check、改动文件 eslint 0 error;AGC vitest 168 个文件中除 5 个 jsdom localStorage 环境失败文件与本分支既有 resourceTagStatsRefresh 失败外全绿,appSurface 198 passed / 13 skipped;Rust 定向用例 direct_codex_user_item、skill_pack、sessions 全过(整套分片在本容器受 /sbin -> usr/bin 触发沙箱预检失败,与本合并无关)
2026-09-21 21:09:34 +08:00
lhk229 2d61a4c057 Merge pull request 'Codex/fix direct image generation ci' (#452) from codex/fix-direct-image-generation-ci into master
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/452
2026-09-21 19:46:05 +08:00
lhk229 e2bdb1105b 合并最新 master 到图片生成 CI 修复分支
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m7s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m52s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m10s
Project CI / Repository checks (pull_request) Successful in 5m5s
Project CI / Frontend tests (pull_request) Successful in 5m40s
Project CI / Backend tests (pull_request) Successful in 8m16s
Project CI / Native shell tests (pull_request) Successful in 9m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m45s
合入最新 master 的客户端图标素材描述修复
保留图片生成通知测试的宿主执行会话初始化与失败断言
2026-09-21 11:20:53 +00:00
lhk229 71f5f36764 修复图片生成通知测试缺失宿主执行会话
复用真实宿主执行会话与交付合同夹具,补齐 Direct 工具桥测试前置状态
保留图片资源提交后的刷新通知断言,并核对空提示词拒绝原因
同步更新图片生成集成测试初始化说明
2026-09-21 11:15:39 +00:00
lhk229 f71361c292 修复客户端图标素材生成丢失用户描述 (#450)
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
删除图标生成的固定提示词包装、默认美术说明和两次截断
按服务端单条描述语义校验非空及200个Unicode字符,超限拒绝并保留原文
保留内部换行,直接以单条iconDescriptions提交用户描述
更新前端、原生入口和实际HTTP请求回归测试
同步工具说明、生成契约及项目排障文档

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/450
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-21 19:12:35 +08:00
lhk229 6e9e7edcf8 修复回显测试身份数据与 Linux 条件编译
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m2s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 6m12s
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
按实际用户输入生成 SHA-256 请求摘要,保留回显去重与历史记录断言
保留 Windows 安装身份辅助函数的平台限制,补齐两处 AppData 调用分支的条件编译
同步 Direct 回合测试与安装路径策略文档
2026-09-21 10:59:28 +00:00
lhk229 480fe995ed Merge remote-tracking branch 'origin/master' into codex/fix-agc-command-runtime-ci 2026-09-21 10:43:06 +00:00
lhk229 afd5b8d91f 修复 Linux 命令执行将孤儿僵尸误判为未回收进程
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m46s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m39s
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
命令主进程退出后区分存活成员与僵尸成员,保留进程组身份校验和失败关闭语义
处理 procfs 扫描期间进程退出及非 UTF-8 进程名,空进程组直接返回
新增隔离 subreaper 回归测试并补全命令 observation 断言诊断
同步 Runtime 技术方案与共享排障记录
2026-09-21 10:30:41 +00:00
kdletters c60d116af0 合并 codex/agc-godot-templates-20260921 到 master:新增 Godot 模板与按模板建项分流
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- 新增 Godot 模板源 godot-empty-2d / godot-empty-3d / godot-hello-world / godot-platformer-2d

- 按模板建项新增 Godot 分流:改写 project.godot 的 config/name 后走既有 Godot 导入并写入相对根

- Godot 工作区发现放宽:一层多命中按目录名排序取首个,project.godot 允许链接,内置插件行去掉手动启动

- 解决 decision-log.md 冲突:保留渠道安装身份与项目快照按渠道分区两条记录,并合入 Godot 两条记录
2026-09-21 18:17:33 +08:00
kdletters 3eb99f5124 合并 codex/agc-channel-install-identity 到 master:AGC 渠道安装身份隔离
- AGC 渠道安装身份隔离:不同渠道包体可在同一台设备并存
2026-09-21 18:17:12 +08:00
kdletters 34b1ad5d5c 新增 Godot 模板并补按模板建项的 Godot 分流
- 新增 4 个仓库内手写 Godot 4.7 模板源:godot-empty-2d、godot-empty-3d、godot-hello-world、godot-platformer-2d,entry 统一为 project.godot,各自带 meta.json 与 960×540 封面
- template_library.rs:复制模板后按 project.godot 识别 Godot 模板,改写工程显示名后走既有 Godot 导入并写入 godotProjectRoot,不再落到 Web 占位入口
- manifest.rs:新增 apply_godot_project_display_name,只改 [application] 段的 config/name 一行,其余字节逐字保留
- manifest.rs:放宽 Godot 工作区发现,一层子目录多命中按目录名排序取第一个,project.godot 允许是符号链接 / reparse point / 硬链接
- commands.rs:同步 Godot 导入错误文案,去掉「普通文件」限定
- RuntimeConfigDialog.tsx:内置插件行去掉手动启动 / 停止按钮,启动由前端自动启动承担、停止走该行启用开关
- import_tests.rs 与 tests/project.rs:补显示名改写、多命中确定性、链接标记与错误文案的定向回归
- 同步更新 PRD、模板包组织指南、模板库与模板建项技术方案、AGC Godot 编辑器插件接入方案、实施计划与共享决策记录
2026-09-21 17:25:34 +08:00
k88936 4698527025 完成DirectProject composer canonical content收口
输入区草稿只保留 Lexical content 数组

Direct 与 Planning 提交边界直接读取 canonical content

润色保留引用解析并保护附件节点

通过单向 DTO 适配 legacy text 引用与附件消费
2026-09-21 14:37:49 +08:00
k88936 ec408bfafd 删除 DirectProject 聊天 IPC 的死参数 prompt
Rust 侧删掉 chat_with_game_creator_direct_codex 的 prompt 入参:回合输入只由 canonical userItem 派生,该参数从未被读取

DirectProjectTurnInput 去掉 prompt 字段,提交与排队派发不再计算 @显示名 投影文本

界面断言改读 userItem.content,并删掉退役的附件 sidecar 用例与只断言「不存在」的终止用例

同步实施计划与附件路径映射两份技术方案里的 Direct 消息契约

共享记忆补记该死参数的口径与成因
2026-09-21 14:27:23 +08:00
k88936 ab52805d3c 收口DirectProject输入区canonical content唯一事实源
删除输入区 text/references 双轨草稿状态

Direct 与 Planning 改为从 Lexical content 读取

润色保留 chip 并保护附件 canonical content

增加 legacy content DTO 单向翻译与附件回归测试

补充 DirectProject composer canonical content 实施计划
2026-09-21 14:11:18 +08:00
k88936 94471ff191 补充共享记忆:受控输入区回写的 effect 时机与删订阅留死 prop 两条踩坑
- docs/project-memory/shared-memory/pitfalls.md 新增「受控 Lexical 输入区的回写用被动 effect 会用滞后渲染的 props 清空用户草稿」:现象、宿主回显晚于编辑器提交的原因、useLayoutEffect 的处理与 chat-composer 用例验证
- 新增「删链路只删订阅、没删它喂的 state」:design-agent-update 订阅被删后 designReasoning 恒为空、残留的订阅就绪 promise 是误删线索,以及恢复后的验证入口
2026-09-21 12:22:32 +08:00
k88936 f3097e1096 删除按已退役工作台壳聊天入口编写的预览与 Run 历史用例
- 删除 apps/ai-game-creator-shell/tests/appSurface/project-preview.suite.ts 及其下 preview-shortcuts.suite / run-history.suite 与全部辅助模块(fixtures、invoke-mock、message-bubble、四个 assert-* 流程)
- 删除 tests/appSurface.test.ts 里对应的 import 与注册
- 这些用例断言的是已退役的 Supervisor 工作台壳:项目摘要面板、开发环境、LLM 状态、Agent 状态、编排 Trace、`/runs` `/read` 聊天命令,以及 `chat_with_game_creator_agent`;这些对象在当前源码里都已不存在
2026-09-21 12:22:08 +08:00
k88936 f1294d4d6b 修正首页建项两用例对 DirectProject 首轮链路的过期假设
- apps/ai-game-creator-shell/tests/appSurface/home.suite.ts 两个首页建项用例补上 `get_local_game_manifest` 桩:首轮需求由 DirectProject 聊天容器认领,它要先读到项目清单(项目身份)才发送,不桩这条命令时清单读失败、首轮永不发出
- 「imports home attachments」用例改为 waitFor 断言首轮 invoke:发送发生在清单到位之后,不能在「工作台已出现」的同一帧断言
- 同用例的 `prompt` 期望由 `按这个角色做游戏` 改为 `按这个角色做游戏@角色参考.png`:附件以 `agc_attachment_reference` 进入 canonical content 后按 `@名称` 口径投影,与输入区、队列 chip 同一份派生
2026-09-21 12:22:02 +08:00
k88936 ba3bd59f24 恢复策划 Agent 实时事件订阅(876529e66 误删)
- apps/ai-game-creator-shell/src/App.tsx 重新订阅 `design-agent-update`:本轮正文与工具提示回到 transient reply,`reasoningText` 回到 `planningV2Reasoning`,`view` 回到 applyDesignAgentViewAfterTransient;Rust 侧一直照旧发射这些事件,前端却不再监听
- 补回 designAgentEventSubscriptionReadyRef 的建/解函数与 designAgentEventSubscriptionResolveRef,让 `executeDesignAgentTurn` 等待监听器挂好再发起回合,开局事件不丢
- 新增 designAgentReasoningTurnRef:回合结束(designAgentTurnRef 清空)后仍认本轮 reasoning,最后一条思考不会被视图落盘前的空隙吃掉;切项目时随 resetChatState 清空
2026-09-21 12:21:51 +08:00
k88936 317c8557f4 修复输入区受控回写用滞后渲染的 props 清空用户草稿
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx 的受控同步由 useLayoutEffect 承担:宿主对草稿的回显晚于编辑器提交,被动 effect 可能在上一次渲染的 props 上延迟执行,读到旧 value 加新编辑器状态后重建整份草稿,把刚输入的内容清掉并形成来回清空
- 补注释说明该 effect 必须与本次提交同帧执行,以及直接改成 content 单事实源前的行为约束
2026-09-21 11:59:17 +08:00
k88936 7b1f7bdf17 给 parseAgentRunTrace 与 formatAgentRunStatus 补上纯函数用例
- 新增 tests/agentRunTrace.test.ts,用旧 trace 夹具断言归一化结果(路径/工具数组/上限/任务图/通行计划)
- 覆盖 toolCalls 总数回填、taskGraph 缺失时回退种子任务图
- 覆盖畸形 trace 的拒绝路径(steps / schemaVersion / artifacts / taskGraph / dependencyWaves)
- 覆盖 formatAgentRunStatus 带与不带 lifecycleStatus 的两种文案
2026-09-21 11:36:02 +08:00
k88936 abb2988875 删除无调用方的 pick_local_file 与 delete_local_* 三条 Tauri 命令
- commands.rs 删除 pick_local_file、delete_local_project_file、delete_local_game_memory 三条命令实现
- main.rs 取消上述三条命令的注册
- Rust 用例只保留写命令推进 revision 的断言(删除两条已退役命令的断言并同步改名)
- check-config.mjs 移除这三条命令的白名单条目与旧说明
- check-config.mjs 把旧命令聊天剩下的 15 条无前端调用方命令登记为 native-only,并删掉随该死链失效的 guardrail 片段
2026-09-21 11:34:15 +08:00
k88936 912f9afcd2 删除按已退役 Supervisor 聊天入口编写的 appSurface 用例
- 删除 project-commands / agent-runtime 两个 100% 依赖已退役聊天入口的用例文件及其注册
- 删除 project-assets 里 21 条「from chat」用例(整个套件同为该入口),并移除注册
- 删除 project-conversation 里 7 条、project-development 里 2 条依赖 home 聊天框与斜杠命令的用例
- appSurface 失败数由 114 降到 10,剩余 10 条为 DirectProject/预览/首页链路自身的既有失败
2026-09-21 11:19:12 +08:00
k88936 0224ef7b9d 删除 Project Supervisor 聊天遗留的 pendingCommand 死链
- 删除 App.tsx 里 pendingCommand 状态、确认/取消处理与 21 个 execute* 分发分支
- 删除随该链路一起不可达的 21 个 execute* 实现与 requireChatProjectForUserAction
- 删除只服务该链路的 PendingCommand 类型、pendingCommandPresentation.ts 与 resolvePendingCommandProjectPath
- PlanningChatView 去掉 pendingCommand 面板相关 props、渲染分支与导入
- rememberCommand.test.ts 只保留现役 helper(path/remember/needsInitializedChatProject/agent 卡片)的用例
2026-09-21 11:15:02 +08:00
k88936 1c830e6fe6 修正钱包入口回归用例的断言目标
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 18s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
- apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts 的「keeps the wallet entry available when the workbench opens the UI editor」改为断言 game-workbench-layout-account 的两处落点(策划工作台与主工作台)以及主工作台的 !uiEditorRoute 门控
- 用例原先断言 game-workbench-chat-wallet 出现一次,但该槽位在当前源码里已不存在(钱包移到布局层),断言恒为 null 导致用例一直失败;注释同步改成当前落点
2026-09-19 23:37:25 +08:00
k88936 860f3f09d4 UI 编辑器打开时不再重复渲染工作台布局层的钱包入口
- apps/ai-game-creator-shell/src/view/project-development/index.tsx 主工作台布局里的 walletEntry 槽改为 !uiEditorRoute && walletEntry
- UI 编辑器路由下 UiEditorPage 自己会用同一份 walletEntry 渲染头部钱包,布局层再渲染一次会让同一个入口挂载两份、浮层与编辑器工具条钱包重叠
2026-09-19 23:36:57 +08:00
k88936 dc1cac1c15 给 Skill 引用补上每条消息的数量上限
- apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs 新增 MAX_DIRECT_CODEX_SKILL_REFERENCES=32 与 skill_count 计数,超出即拒绝
- 附件与素材引用已有上限,Skill 引用是唯一没有上限的非文本 part:每条都会触发一次 root/<name>/SKILL.md 文件探测并往 turn/start.input 里加一项,畸形超长 userItem 可以放大文件探测与模型输入
- 补上 inline_skill_reference_count_is_bounded_independently;cargo test direct_codex_user_item::validation:7 passed
2026-09-19 23:35:41 +08:00
k88936 c5a7386465 统一附件摘要的清洗口径
- apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs 抽出 attachment_reference_summary,turn 输入与 history/prompt 投影共用同一份摘要
- 原先 direct_codex_user_item_to_codex_turn_input 自己拼摘要,跳过了 sanitize_attachment_name/media_type/local_path,模型侧可能收到 C:\tmp\notes.md 这类原始路径名,而历史投影里是清洗过的
- cargo test user_item:24 passed;cargo check 通过
2026-09-19 23:32:55 +08:00
k88936 926fe9ae0f 复用 validate 返回的清单,去掉 wire 输入投影里重复的一次清单读取
- apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs 的 direct_codex_user_item_to_wire_input 直接用 validate_direct_codex_user_item 返回的 manifest
- 原先 validate 之后再 read_manifest_for_project 一次,每次投影都会多读一次项目清单(并重复 seed task 变更),与 direct_codex_user_item_to_codex_turn_input 的口径也不一致
- cargo test user_item:24 passed;cargo check 通过(本机 stale target 需先 touch shared-contracts/src/lib.rs)
2026-09-19 23:30:22 +08:00
k88936 e7967cf67b 删除 runtime-settings 用例里空的发布态设置注册函数
- 删除 apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts 里空的 registerPublishedRuntimeSettingsTests
- apps/ai-game-creator-shell/tests/appSurface.test.ts 去掉对应 import 与调用
- 它注册 0 条用例,留着会让人以为发布态运行时设置仍有覆盖
2026-09-19 23:20:25 +08:00
k88936 5aeaf38112 删除 project-development 用例里空的用户面边界注册函数
- 删除 apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts 里空的 registerUserSurfaceBoundaryTests
- apps/ai-game-creator-shell/tests/appSurface.test.ts 去掉对应 import 与调用
- 它注册 0 条用例,留着只会让人以为用户面边界仍有覆盖
2026-09-19 23:19:58 +08:00
k88936 6f5ca47385 删除空的 run-trace 用例文件与其注册
- 删除 apps/ai-game-creator-shell/tests/appSurface/project-preview/run-trace.suite.ts(只剩一个空的 registerProjectRunTraceTests)
- apps/ai-game-creator-shell/tests/appSurface/project-preview.suite.ts 去掉对应 import 与调用
- 说明:该文件被清空后 parseAgentRunTrace 与 formatAgentRunStatus 已没有用例覆盖,重新补测属于后续单独任务,本提交只清理空壳
2026-09-19 23:19:34 +08:00
k88936 3e3c3df41f 删除空的 run-status 用例文件与其注册
- 删除 apps/ai-game-creator-shell/tests/appSurface/project-preview/run-status.suite.ts(只剩一个空的 registerProjectRunStatusTests)
- apps/ai-game-creator-shell/tests/appSurface/project-preview.suite.ts 去掉对应 import 与调用
- 空注册函数挂在测试入口上会让人以为运行状态还有用例覆盖,实际一条都没有
2026-09-19 23:19:29 +08:00
k88936 051c21d97f 删掉立项策划用例里传给 App 的无效 agentDockVisible prop
- apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts 三处 React.createElement(App, {...}) 不再传 agentDockVisible
- 同 design-agent.suite:该 prop 属于 ProjectDevelopmentView,AppProps 未声明,传入不影响任何渲染,只会误导读者以为 Agent dock 被隐藏
2026-09-19 23:18:49 +08:00
k88936 21382926dc 删掉设计 Agent 用例里传给 App 的无效 agentDockVisible prop
- apps/ai-game-creator-shell/tests/appSurface/design-agent.suite.ts 四处 React.createElement(App, {...}) 不再传 agentDockVisible
- agentDockVisible 是 ProjectDevelopmentView(工作台)的 prop,AppProps 里没有它,传了也不会生效;它替换掉的旧 orchestrationMode/projectSupervisorOnly 曾经是真实的 App prop,留着会让人误以为这里会隐藏 Agent dock
2026-09-19 23:18:39 +08:00
k88936 62f1ec83bf 让工作台壳写入的运行态提示不被通用错误兜底覆盖
- apps/ai-game-creator-shell/src/features/agent-runtime/model.ts 的 projectRuntimeVisibleError 公共文案白名单补上「待回答问题已变更,请刷新策划状态」「需要在 Tauri App 内运行。」「请先初始化本地项目」
- 这三条是 App.tsx setProjectChatError 直接写入的给用户看的动作指引,此前不在白名单,经策划运行态窄条渲染时会被替换成通用的「策划 Agent 执行失败,请稍后重试」
2026-09-19 23:17:30 +08:00
k88936 263588af6a 把运行中回合的秒级计时下移到耗时文案组件
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx 新增 DirectTurnElapsed,自己持有 1 秒 tick 与已耗时渲染
- apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx 删除 turnUsageNow state 与其 interval,不再把每秒 tick 传给会话区
- 原先每秒 tick 触发的是整个聊天视图重渲染(含 Markdown 回合列表、工具调用组与队列芯片),现在只重渲染耗时那一小块
2026-09-19 23:16:42 +08:00
k88936 50291a5e77 订阅过期恢复后补跑被搁置的 consume
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectThreadChatSubscription.ts 在 consume 的 finally 之后补一次 consumeAgain 重排
- SUBSCRIPTION_EXPIRED 恢复期间到达的通知会因 consuming 仍为 true 只置位 consumeAgain,而 do/while 已因异常退出,原先这批事件要等下一次通知才会被消费
2026-09-19 23:15:48 +08:00
k88936 8aa737e032 「显示更早」读取失败时补上项目路径守卫
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts 的历史分页 catch 在写 statusNotice 前先比对 projectPathRef.current
- 与成功路径和首个屏幕加载一致;原先切换项目后旧项目在飞读取的失败会显示成新项目的错误
2026-09-19 23:15:15 +08:00
k88936 9243bb42a1 去掉 Direct 回合成功后多余的第二次清单刷新
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts 的 runTurn 不再在成功路径刷新清单,统一由 startTurn 的 finally 刷新
- 原先一轮成功会先后调用两次 get_local_game_manifest,finally 那次已覆盖成功与报错路径
2026-09-19 23:14:51 +08:00
k88936 67252d36b2 修掉写权限门拒绝后 Direct 发送队列永久卡住的问题
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts 在 ensureConversationWriteAllowed 返回 false 且未重跑时,主动放下忙态并继续 dispatchNextQueuedTurn
- 该轮已由 dispatchNextQueuedTurn 出队,原先 finally 只在 invoked 为真时继续出队,导致后续排队消息再也不发送且提示/队列芯片一直显示
2026-09-19 23:14:18 +08:00
k88936 97fc1918c4 给 Direct 回合的前置步骤补上错误出口
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts 在 startTurn 的异步 IIFE 里加 catch,按项目路径守卫后把错误交给 onRuntimeError
- 原先只有 try/finally:写权限门 rejection 会成为未处理拒绝,回合静默失败且已乐观追加的用户消息没有解释
2026-09-19 23:13:53 +08:00
k88936 034c369b88 排队发送后不再立刻抹掉「已加入发送队列」提示
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts 拆出 clearDraftInput(只清输入与附件),排队成功分支改用它
- 原先同一事件里先 enqueueTurn 写提示、紧接着 clearPendingInput 把 composerNotice 置空,React 批处理下用户永远看不到队列确认文案
2026-09-19 23:13:13 +08:00
k88936 884aabe6d7 Direct 聊天权限门改为必填,去掉默认放行
- apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx 删除 ALLOW_CONVERSATION_ACCESS 及两个权限门 prop 的默认值,改为必填
- 原先缺省实现恒返回 true,调用方漏传等于静默跳过工作台壳的读写权限判定;唯一调用方 App.tsx 本来就会传入两个门
2026-09-19 23:12:28 +08:00
k88936 f58249a75e 给 clientTurnId 的回退生成补上随机熵
- apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directCodexConversation.ts 在无 crypto.randomUUID 的回退分支追加 Math.random 派生的随机串
- 原先只有时间戳 + 页内序号,多个 WebView 在同一毫秒创建首轮会得到同一个 clientTurnId,而 Rust 侧用它做项目级唯一校验与历史关联键
- 结果仍是 ASCII 字母数字与连字符组成、长度在 6..160 之间,满足 native 校验
2026-09-19 23:12:02 +08:00
k88936 38fb9aa7ac 删除 Direct 输入区发送按钮里不可达的忙态分支
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx 发送按钮只在非 busy 分支渲染,去掉 '思考中' 文案、Loader2 转圈与 disabled 里的 busy 条件,并移除不再使用的 Loader2 import
- busy 时仍由既有 ComposerStopButton 承担停止入口,渲染结果不变
2026-09-19 23:11:14 +08:00
k88936 b5e10f3851 合并 Direct 输入区来自同一模块的两条 type import
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx 把 gameCreationApp 的 GameCreationAppAssetManifestEntry 与 GameIterationVersion 合并成一条 import type
- 不涉及任何运行时行为
2026-09-19 23:10:51 +08:00
k88936 c4e2793db1 把回合执行过程渲染的两层三目抽成 renderTurnProcess
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectTurn.tsx 新增 renderTurnProcess,负责空过程、运行中平铺与结束后折叠三种分支
- 组件主体不再嵌套三目,渲染结果与 data-testid="turn-process" 等 DOM 结构保持不变
2026-09-19 23:10:35 +08:00
k88936 79cb510891 把排队消息标题的嵌套三元改成提前返回
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/chatComposerQueue.ts 的 queuedChatTurnLabel 先处理空文本再判断截断
- 返回值不变
2026-09-19 23:10:01 +08:00
k88936 3160131889 给澄清问答卡提交路径补上 rejection 兜底
- apps/ai-game-creator-shell/src/view/project-development/planning/PlanningUserInputCard.tsx 在 Promise.resolve(onSubmit(...)).finally(...) 之间加 .catch(() => undefined)
- onSubmit 的契约允许返回 rejected promise,原先只挂 finally 会把 rejection 变成未处理拒绝;与 GddApprovalCard 的处理口径一致
2026-09-19 23:09:38 +08:00
k88936 c46687f0ef 把澄清问答卡提交按钮文案的嵌套三元改成顺序分支
- apps/ai-game-creator-shell/src/view/project-development/planning/PlanningUserInputCard.tsx 把 answerPrepared / controlBusy||submitting 的嵌套三元改成先取默认值再覆盖的 submitLabel
- 文案取值不变
2026-09-19 23:09:20 +08:00
k88936 137533484b 抽出策划视图共用的 Design 模式判据与回调兜底
- apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx 新增 designMode、designActionsBusy 与三个 Design 回调兜底常量
- DesignAgentPhaseStatus(顶部)与 DesignAgentPendingActions(底部)原先各自重复 designView||onDesignApprove、controlBusy||planGddDecisionBusy 和 ?? (()=>undefined),现在共用同一份取值
2026-09-19 23:09:00 +08:00
k88936 00063ba882 抽出策划输入区与提交按钮共用的禁用判据
- apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx 新增 composerDisabled,替换 ResourceReferenceInput 与提交按钮上逐字重复的四条件表达式
- 判据取值不变,后续改条件时不会只改到其中一处
2026-09-19 23:08:02 +08:00
k88936 cb3d452a44 把策划输入框提交按钮文案的嵌套三元改成顺序分支
- apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx 的 submitLabel 改为先给默认值再按 needsUserInput / controlBusy 覆盖
- 取值结果不变,只是去掉三层嵌套三元表达式
2026-09-19 23:07:42 +08:00
k88936 257e407df7 策划会话 GDD 展示的 agentId 改用 PLANNING_SESSION_AGENT_ID 常量
- apps/ai-game-creator-shell/src/view/project-development/planning/planningSessionV2.ts 的 gddDisplayFromArtifact 不再写死 'planning-agent-v2' 字面量
- 与同结构里的 rootAgentId 共用 planningSessionContract.ts 的常量,避免 id 变更时要改两处
2026-09-19 23:07:25 +08:00
k88936 0d3ab25499 删除输入区里只会在挂载时跑一次的草稿清空 effect
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx 删除依赖只有 resetPromptPolish(稳定 useCallback)的 effect
- 该 effect 读 liveDraftRef.current 却把它排除在依赖外,实际只在挂载时执行一次;清空草稿时重置润色状态的逻辑已由 OnChangePlugin 内联承担
2026-09-19 23:07:02 +08:00
k88936 2a8063a83a 让技能清单查询失败后可以重试
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx 在 list_agc_skill_catalog / list_client_extensions 的 catch 里放开 skillCatalogRequestedRef
- 原先标记在调用前就置位且失败时不回退,一次瞬时失败会让该输入区此后不再拉取技能候选
2026-09-19 23:06:05 +08:00
k88936 37c7477068 修正草稿文本把附件引用误回读成 $名称的问题
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx 的 readDraftFromNodes 改为按 part.type 显式分支:附件引用回读为 @名称,技能引用回读为 $名称,未知类型返回空串
- 该 text 会被受控同步比较(对照 directCodexContentToPromptText 展开的 value),附件口径不一致会让受控输入区每次都判定草稿变化而重建编辑器
- 顺带去掉该处嵌套三元,与 resourceReferences.ts 的渲染口径对齐
2026-09-19 23:05:34 +08:00
k88936 e259106267 拆出引用草稿键与 token 的取值 helper 消除嵌套三元
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx 新增 referenceDraftKey/referenceDraftToken,替换 readDraftFromNodes 里构造 labels 时的嵌套三元表达式
- 行为与原先一致,只是把「resource 取 id、skill 取 name、其余取 label」的规则收敛到一处
2026-09-19 23:04:47 +08:00
k88936 7a89a119d9 校正 check-config 免调用白名单中三条命令的说明
- apps/ai-game-creator-shell/scripts/check-config.mjs 为 delete_local_game_memory、delete_local_project_file、pick_local_file 补写准确的剩余调用方说明(前两条只剩 Rust 测试直接调用命令函数,pick_local_file 前端与 Rust 均无调用方)
- 修正原先「命令由 native Runtime、CLI swarm 与 Rust 测试使用」这一对三条命令不成立的笼统说明
2026-09-19 23:03:51 +08:00
k88936 57b28ce5cd 删除 ts-rs 生成器漂移产生的 DirectCodexUserMessageEnvelope 死文件
- 删除 apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectCodexUserMessageEnvelope.ts
- 该文件是 ts-rs 导出按本机依赖版本漂移时多出来的产物,前端无任何引用,按 project-memory/pitfalls.md 的口径不提交
2026-09-19 23:03:27 +08:00
k88936 876529e668 删除 Project Supervisor 前端链路并将项目对话收敛到 DirectProject 与立项策划
- 删除 ProjectSupervisorView、SupervisorChatOnlyView、ProjectWorkspaceChatPane、AgentConversationOverlay、DeveloperProjectPanels、DeveloperRuntimePanels 与 features/agent-runtime/panels.tsx
- 删除 Supervisor 独立调试窗口:windows.rs 的 supervisor_chat_window_url / open_project_supervisor_chat_window、main.rs 的 invoke 注册、?supervisor-chat 与 ?agent-chat 前端入口、developer.json capability 和对应 Rust 用例
- 删除工作台壳的开发者 Agent 面板:DeveloperAgentPanel、useDeveloperAgentPanel、useDeveloperAgentState、developerAgentControls
- App.tsx 删除只服务退役面板的 state/ref/effect/handler(agentConversation*、文件/记忆/资产/画板/预览面板处理、commandLog、llmConfigStatus、editorBaseUrl、回放历史与 trace 面板状态、selectedAgent 等)以及由此产生的空分支,并把 requestRuntimeConfigOpen 接回本地 RuntimeConfigDialog
- 立项策划独立成模块:Design Agent 与 Planning V2 的容器和表现移到 view/project-development/planning/(PlanningChatView、PlanningUserInputCard、GddApprovalCard、DesignAgentSurface、PlanningLaneRuntimeStrip、planningLane、planningSessionV2、planningSessionContract)
- 改名到中性概念:ProjectSupervisorComponentProps→ProjectChatComponentProps、WorkspaceLauncherShellProps.ProjectSupervisor→ProjectChat、ProjectDevelopmentView.supervisor→chat、orchestrationMode→agentDockVisible、initialSupervisorMessageClaims→initialTurnClaims、ProjectManifestSnapshotSource 'supervisor'→'chat'、CSS project-supervisor-*/project-planning-*→project-chat-*
- 删除 PROJECT_SUPERVISOR_AGENT_ID 与 PROJECT_SUPERVISOR_PLAN_SOURCE 常量
- 工作台壳自己订阅 game-creator-manifest-invalidated,用 revision→清单→revision 配对读(source: asset-event)并入 currentProjectContext,且不重新检查项目目录;测试夹具改为按监听器集合广播该事件
- 删除只钉住退役界面的 appSurface 用例(旧调试窗口、开发者面板、Agent 对话浮层、运行历史面板)
- 同步文档:ADR、DirectProject 聊天模块抽离实施计划与里程碑、Provider 推理里程碑、策划会话 RuntimeV2、AI 游戏创作实施计划,并在 decision-log 新增 2026-09-19 条
- 删除随退役面板失去调用方的模块与导出:projectSummaryCommands.ts(1054 行旧 Supervisor 斜杠命令处理器)、AGENT_RUN_HISTORY_* 常量、agent-runtime/model.ts 与 project-summary/agentPresentation.ts 里的死导出,以及 agentRunTrace / memoryCommands / projectCommandPolicy 中无调用方的校验与解析函数
- 更新 scripts/check-config.mjs 与 scripts/check-native-shells.mjs 的窗口与命令守卫
2026-09-19 21:59:44 +08:00
k88936 99be1d76fe 抽离 DirectProject 独立聊天容器并删除工作台壳 Direct 状态
- 新增 view/project-development/chat 模块:DirectProjectChatView 自己持有项目清单订阅、首屏锚点、历史分页、发送队列、附件、终止、草稿与本地消息
- DirectProjectChatView 入参收窄为项目路径、入口首轮需求(canonical content[])和两条权限门,Direct 状态不再穿过 App.tsx
- 线程订阅与聊天 reducer 提成 useDirectThreadChatSubscription:subscribe/consume/notify 单飞循环、订阅欠账、SUBSCRIPTION_EXPIRED 重订与 DirectThreadChatState
- chat/components 按「一个组件一个目录、逻辑与表现同目录」重组(ChatHeader、Composer、Conversation、SettingsDialog、ToolCallGroup)
- DirectProjectChatView 拆成聊天头、会话区、回合、输入盒四个组件
- App.tsx 删除 Direct 专属 state/ref/effect/handler 与 directCodex 分支,首轮需求按 canonical content 交给聊天
- 工作台级动作(运行本地预览等)经 DirectProjectChatHandle.announce 交给聊天自己的本地消息流
- Direct 首轮认领记录提成 app/initialSupervisorMessageClaims,供各入口共用
- Rust 侧 Direct 契约 ts-rs 导出目录改到 chat/generated
- 补齐 direct_codex_user_item/wire.rs 既有的 rustfmt 空格漂移
- 同步 ADR、实施计划、decision-log、pitfalls 与 Direct 技术文档
2026-09-19 15:08:31 +08:00
k88936 38ad2ce256 抽离项目开发 DirectProject 聊天模块
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m42s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m9s
Project CI / Backend tests (pull_request) Failing after 30s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m16s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m54s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m32s
Project CI / Frontend tests (pull_request) Failing after 16m6s
Project CI / Native shell tests (pull_request) Successful in 24m45s
Project CI / AI game creator shell web tests (pull_request) Failing after 24m28s
将 DirectProject 表现与 controller 移到 view/project-development/chat

删除运行时 Direct feature flag 与 ProjectSupervisorView 的 Direct 行为分支

迁移 ts-rs 生成契约并删除无意义 generated/index.ts

将钱包入口提升到工作台布局并保留现有 Direct 测试覆盖
2026-09-18 22:06:15 +08:00
k88936 65f8e44a88 拆分 DirectProject 聊天架构文档
记录 DirectProject 独立聊天容器与工作台钱包布局决策

更新实施里程碑、计划和项目上下文,明确 DirectProject 固定属于项目开发工作台

记录代码与生成契约迁移的验收边界
2026-09-18 21:04:23 +08:00
k88936 ade1b7803e 修复 DirectProject 校验清单合并
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m3s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m25s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m30s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m41s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m40s
Project CI / Frontend tests (pull_request) Failing after 7m45s
Project CI / AI game creator shell web tests (pull_request) Failing after 8m31s
Project CI / Native shell tests (pull_request) Successful in 13m1s
让校验函数返回已读取的项目清单

补齐附件在 Codex 输入转换中的分支;图片 part 已退役,只保留附件 part
2026-09-18 14:27:23 +08:00
k88936 71000b6df1 通过 Tauri command 提供 Skill 候选
从已校验的 AGC Skill manifest 派生 list_agc_skill_catalog 命令

移除前端内置 Skill 名称硬编码并增加读取命令测试
2026-09-18 14:27:23 +08:00
k88936 c047543825 复用 DirectProject 引用摘要构造
提取素材与运行区域摘要 helper

让历史投影和 turn 输入转换共用同一份摘要逻辑
2026-09-18 14:27:21 +08:00
k88936 34b3af1d2a 复用已校验的项目清单
DirectProject wire 转换只读取一次项目 manifest

避免每个素材引用重复 I/O 与解析并缩小校验到解析之间的竞态窗口
2026-09-18 14:26:50 +08:00
k88936 f7cba30b6b 拒绝保留路径段的 Skill 名称
在 DirectProject Skill 校验中拒绝 . 与 ..

阻止 Skill 路径解析通过父目录段逃逸配置根目录
2026-09-18 14:26:50 +08:00
k88936 273f12633c 按字符数限制 Skill 名称
使用 Unicode 字符数量而非 UTF-8 字节长度执行 120 字符上限

让多字节 Skill 名称与资源 ID 的长度校验口径一致
2026-09-18 14:26:50 +08:00
k88936 5f5aa17152 展开引用列表指纹分支
将资源、Skill 与运行区域 key 生成改为显式分支

移除 chatReferenceListKey 的嵌套三元表达式
2026-09-18 14:26:50 +08:00
k88936 3a57d9fbdf 移除未使用的 JSON 类型导入
删除 codex_app_server 中未被直接引用的 serde_json::Value 导入
2026-09-18 14:26:50 +08:00
k88936 382b925ab9 复用引用节点插入逻辑
统一资源与 Skill 候选的 Lexical 节点插入流程

避免两个选择处理器在删除查询和关闭菜单行为上漂移
2026-09-18 14:26:50 +08:00
k88936 c1482012c6 整理引用芯片标题分支
提取资源、Skill 与运行区域标题计算 helper

移除 ResourceReferenceChip 的嵌套三元表达式
2026-09-18 14:26:50 +08:00
k88936 c3a17a6efc 更新Skill提及里程碑状态
标记输入提示实现进入进行中阶段
2026-09-18 14:26:50 +08:00
k88936 1bfdc3a760 接通Skill提及Codex输入
将 canonical Skill part 按原顺序转换为 type skill 输入项

沿用客户端隔离 Skill root 解析受控路径
2026-09-18 14:26:50 +08:00
k88936 1e27cd229d 接入客户端已启用Skill候选
从扩展索引读取启用状态并合并内置 Skill 清单

输入提示只展示可用 Skill 并保持取消安全
2026-09-18 14:26:49 +08:00
k88936 774710452f 校验Skill提及用户输入
为 canonical Skill part 增加名称边界校验

将 Skill 提及保留为 Codex 可识别的 $name 文本
2026-09-18 14:26:49 +08:00
k88936 aaecb82622 增加输入框Skill提及候选
扩展聊天引用模型支持 Skill 节点与 canonical content

复用 Lexical typeahead 增加 $Skill 候选和芯片展示

 Conflicts:
	apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx

 Conflicts:
	apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx

 Conflicts:
	apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
	apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts
2026-09-18 14:26:49 +08:00
k88936 64ad24ac77 补充DirectProject Skill提及输入规范
更新聊天输入主规范并增加 Skill 提及边界

新增 Skill 提及里程碑与小切片实施计划
2026-09-18 14:26:49 +08:00
k88936 75ec3361dc 限制附件元数据格式与长度
校验 inline 附件名称和媒体类型的控制字符、格式与长度。

共享附件清洗上限常量并补充异常元数据拒绝测试。
2026-09-18 14:12:47 +08:00
k88936 bc1dc868a1 校验已导入附件的项目路径
imported 附件必须携带有效非空的项目相对路径。

failed 附件仍允许无路径,并补充状态路径组合测试。
2026-09-18 14:07:07 +08:00
k88936 50204ff7aa 限制 DirectProject inline 附件数量
为 canonical 附件增加独立的八个条目上限。

补充超限附件拒绝测试,避免绕过素材引用数量门禁。
2026-09-18 14:03:30 +08:00
k88936 70fa160819 清洗附件摘要元数据
wire 投影复用附件名称与媒体类型的安全清洗。

附件项目路径输出归一化结果,避免原始控制字符进入模型提示。
2026-09-18 14:00:00 +08:00
k88936 2e78cbf152 Merge remote-tracking branch 'refs/remotes/origin/master' into fix/ref-persistent
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m22s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m30s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m41s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m25s
Project CI / Repository checks (pull_request) Failing after 4m0s
Project CI / Frontend tests (pull_request) Failing after 4m50s
Project CI / Native shell tests (pull_request) Successful in 8m29s
Project CI / Backend tests (pull_request) Successful in 11m23s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m16s
2026-09-18 13:29:58 +08:00
k88936 840baf208f 移除草稿转换的文本引用回退
将 ChatComposerDraft.content 设为必填的 canonical 唯一来源。

删除 text 与 references 重建 DirectCodexUserItem 的兼容分支及其无效空值兜底。
2026-09-18 13:19:01 +08:00
k88936 d8af0fa882 保留聊天草稿中的附件引用节点
为 agc_attachment_reference 增加 Lexical 附件节点与可移除 chip。

恢复初始 canonical content 时不再静默丢弃附件,并让编辑器读回完整附件 part。
2026-09-18 13:17:31 +08:00
k88936 faed0ec251 移除 DirectProject 入口的重复回合审计
DirectProject 继续以 project.jsonl 作为完整回合记录。

入口不再创建 DirectCodexTurnAudit,保留底层可选审计接口供独立测试和其它路径使用。
2026-09-18 12:56:43 +08:00
k88936 6e63a118e0 延后资源草稿直到素材索引可用
避免初始编辑器挂载时 assetsById 为空导致资源引用永久丢失。

保留 Lexical 节点自行解析资产名称的后续 TODO。
2026-09-18 12:53:44 +08:00
k88936 4ae5ab14fa 移除队列标签不可达兜底分支
队列标签统一复用 canonical content 的文本派生。

删除不会被现有引用类型触发的附件和素材兜底文案。
2026-09-18 11:36:10 +08:00
k88936 b73a329b2c 修复附件场景下历史命令识别
使用不含附件的文本输入判断 /history 命令。

避免待发送附件把 canonical prompt 拼成长文本后绕过历史记录加载。
2026-09-18 11:35:18 +08:00
k88936 06004bfeea 清理资源引用标签的重复分支
移除相同结果的死三元分支。

保持资源与运行区域引用均使用统一的 @标签 文本。
2026-09-18 11:34:35 +08:00
k88936 ecea7b4ad3 解决 DirectProject 合并冲突
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m2s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m54s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m20s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m31s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m3s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m49s
Project CI / Frontend tests (pull_request) Failing after 6m56s
Project CI / Repository checks (pull_request) Failing after 6m23s
Project CI / Native shell tests (pull_request) Successful in 10m17s
Project CI / Backend tests (pull_request) Successful in 11m8s
Project CI / AI game creator shell web tests (pull_request) Failing after 6m0s
合并双方 DirectProject 历史分页、锚点闸门与订阅竞态处理。

保留 canonical content 输入、聊天 reducer、附件发送和历史分页行为。

合并 Codex 用户回显过滤、增量脱敏及 Direct 回合完成状态处理。

补齐历史截断、损坏行 fail-closed、首屏 anchor 与 appSurface 定向测试。

同步合并应用壳层、后端绑定和项目文档变更。
2026-09-18 10:47:55 +08:00
k88936 b1e82d64cb 合并 DirectProject 聊天真相源与 canonical content 行为
- 统一历史切片与 Thread Manager 事件投影,保留 DirectProject 单一聊天事实源

- 保留 canonical content 严格校验、资源显示名派生、附件引用与队列语义

- 修复首页首轮缺失 canonical user item 的真实回归并完成定向验证
2026-09-18 02:49:09 +08:00
k88936 b7622f4a24 Merge remote-tracking branch 'origin/master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-17 16:57:47 +08:00
k88936 cb840c6e8c Merge branch 'master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-17 16:53:18 +08:00
k88936 0a9cdf6395 补 DirectProject 历史分页连拉的实现证据
里程碑前端连拉那条转成已验收,证据指向新单测与跨页同回合集成用例
证据要求补上本轮实跑结果与变异验证现象,避免再出现打勾但只覆盖一半
决策记录把验证口径写成具体条数与变异验证结论
2026-09-17 16:51:30 +08:00
k88936 e382d23892 补跨页同回合的历史分页回归用例
用 51 条历史复现用户报的现象:20 条工具条目整屏落进同一个折叠的执行过程
断言一次点击连拉两页拿到新用户气泡,并按 process-31 / process-11 两次锚点推进
断言连拉插入旧内容后视口仍停在原处,不被拉回列表底部
2026-09-17 16:49:08 +08:00
k88936 37ae7508b8 App 聊天历史分页改走连拉循环
首屏与「显示更早」共用 readDirectHistoryPages,不再各自单发一页
首屏基线取空并在守卫后一次性并入聊天 reducer,失败仍按原有语义抛出
「显示更早」按当前视图条目判断新回合,取到多页一次性并入并推进锚点
连拉中途失败保留已取到的页,报错统一走同一个出口
2026-09-17 16:39:24 +08:00
k88936 1ae6c6cd06 新增 DirectProject 历史分页连拉模块与单测
directHistoryPaging 按「合并后聊天投影回合数增加」判断可见反馈,出现新回合或 hasMore=false 即停
连拉上限 DIRECT_HISTORY_MAX_PAGES_PER_ACTION 放进 app/constants,每个操作最多 5 次请求
锚点未推进(items 为空 / firstItemId 为 null 或原地打转)时立即停手并收口 hasMore
某页读取失败不丢已取到的页,返回 error 交给调用方报错
directThreadChat 抽出 projectDirectHistoryItems,让分页判据与视图共用同一份投影
2026-09-17 16:27:09 +08:00
k88936 459f8a60e5 文档先行:定死 DirectProject 历史分页连拉的可见性口径
把「可显示」判据明确为合并后聊天投影出现新回合,并写清每操作 5 页上限与锚点不前进即停
里程碑把原第 56 条按后端锚点回扫与前端连拉拆成两条独立验收项,前端那条先留待补证据
实施计划第 6 步同步口径,并把新分页单测加入验证命令
决策记录新增 2026-09-17 条目,记下落点、上限粒度、失败与竞态边界
2026-09-17 16:18:51 +08:00
k88936 63c495dab0 DirectProject 不再把 Codex 回显的用户消息下发给聊天
- 运行态条目投影新增回显过滤 direct_thread_visible_item,item/started 与 rawResponseItem/completed 两条路径共用
- is_direct_project_codex_user_item 提升为 pub(crate),落盘侧与运行态侧共用同一判据
- 新增 E2E direct_project_turn_does_not_forward_codex_user_echo_as_chat_items:修复前事件流会多出两条同文本用户条目(已做变异验证)
- 同步 ADR 与 shared-memory decision-log:回显过滤必须同时覆盖落盘与运行态两侧
2026-09-17 14:41:58 +08:00
k88936 b37d91882c Revert "修复DirectProject对话历史分页卡页"
This reverts commit 1b010fb1f8.
2026-09-17 14:38:39 +08:00
k88936 dd339638cc Revert "修复历史重读后旧消息追加到末尾的乱序"
This reverts commit 036395c6c9.
2026-09-17 14:38:22 +08:00
k88936 0caf998227 重新生成 ts-rs 前端绑定并让前端按 null 语义构造运行时区段
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m35s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m47s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m15s
Project CI / Repository checks (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m15s
Project CI / Frontend tests (pull_request) Failing after 5m44s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m49s
Project CI / Native shell tests (pull_request) Successful in 10m42s
- 跑 export_bindings 后 prettier 格式化:DirectCodexUser* 5 个文件内容更新,新增 DirectCodexUserMessageEnvelope.ts 与 ui-editor 的 BindingChange/BindingDTO
- DirectCodexUserRuntimeRegionPart 的可选字段由 `?:` 变成 `T | null`(Rust Option 会显式序列化成 null,不是省略键)
- chatReferenceToContentPart 改用 `?? null`,消息条目的 satisfies 目标改为 DirectCodexUserItem(type:'message' 归到信封类型)
- ResourceReferenceInput 不再复制一份区段字面量,改为复用 chatReferenceToContentPart
- 验证:tsc --noEmit 通过;resourceReferences 等 5 个用例集 81 passed;appSurface 452 passed / 17 skipped(连跑两次)
2026-09-17 14:03:05 +08:00
k88936 6eed6bd13f 收敛 content→可读文本为单一口径并强制传 manifest(review 第 2、3 条)
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m10s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m28s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / Frontend tests (pull_request) Failing after 4m24s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m2s
Project CI / Native shell tests (pull_request) Successful in 8m31s
- directCodexContentToPromptText 的 assets 改为必填,去掉 = [] 默认值:漏传素材清单从运行期文案退化变回编译期错误
- queuedChatTurnLabel 的 assets 与 ComposerTurnQueue 的 assets 属性同样必填,队列 chip 与消息正文共用同一个派生(chip 只多做压单行与限长)
- 断言改为显式传 [] 或 manifest:resourceReferences、chat-composer 用例按新签名收敛
- 更新 pitfalls 条目与 decision-log(canonical content 有效性只判整条 content、投影逐字透传、可读文本单一口径必填 assets)
2026-09-17 14:02:21 +08:00
k88936 b543a060b7 放宽 DirectProject content 校验为「整条判定」并按逐字投影(review 第 1 条,① 口径)
- Rust validate_direct_codex_user_item 改为只判整条 content:有一段非空白文本或任意非文本 part 即有效,单个纯空白 input_text(段落分隔 / 软换行 / chip 后的分隔空格)合法,不再逐个 part 拒绝空文本
- 新增 content_has_meaningful_input,并与前端 hasMeaningfulDirectCodexContent 同口径;wire.rs 的「不能转换为空 prompt」只作兜底
- ResourceReferenceInput 的投影层原样透传编辑器节点:不做空白过滤、也不与相邻 part 合并,删掉 appendInputText 与中间版本的「向前合并」收集器
- 按逐字投影更新资源输入、Godot 回合用例的 canonical content 断言
- 同步里程碑实现结论与证据、pitfalls 条目;Rust validation/wire 新增「单个纯空白 part 通过、整条全空白拒绝」用例
2026-09-17 14:01:41 +08:00
k88936 9993b2feb5 通知分类收敛成生产读取器与单测共用的唯一实现
- direct_codex_notification_event 收编 resolution / request / item / rawItem / Terminal 全部分支,并接收 turnId
- 读取器删掉自己那份内联分类,只保留「必须有 turnId 才处理」与活动/正文节流
- agentMessage 缺 itemId 的告警与 direct-missing-item:{turnId} 兜底身份原样保留,避免行为变化
- 新增 direct_notification_classification_covers_the_reader_branches 钉住各分支;E2E 假 app-server 用例继续覆盖读取器
2026-09-17 13:47:32 +08:00
k88936 d348cf9064 线程 id 只按路径归一,不再受 manifest 可读性影响
- direct_thread_id_for_project 改用 resolve_direct_codex_project_authority(存在/是目录/canonicalize)
- 不再读 .agent/manifest.json:那次读取是连接池权威身份的要求,线程 id 只是路径 key
- 否则 manifest 在订阅时读失败、跑回合时读成功,会让符号链接等非 canonical 调用方路径算出两个线程 id,订阅绑到空线程
- 新增 direct_project_thread_id_ignores_manifest_readability,旧实现下会退回调用方原始字符串(已做变异验证)
2026-09-17 13:42:35 +08:00
k88936 e640219243 思考正文改走生产路径的 ReasoningDelta 通道
- 新增 direct_codex_reasoning_delta_event,两条 reasoning 增量通知只在这一处分类
- 生产 stdout 读取器改用该函数,并跳过「preparing 活动」的降级与活动节流(活动节流会按类别吃掉逐段正文)
- 旧分类函数 direct_codex_notification_event 同步改用它,避免两份实现再次分叉
- 单测 direct_reasoning_deltas_stream_text_while_plan_and_command_output_stay_activity 取代原先断言 Activity 的旧用例
- 新增 E2E codex_app_server_streams_reasoning_deltas_without_activity_fallback:把读取器的路由关掉即转红(已做变异验证)
2026-09-17 13:22:36 +08:00
k88936 da2ad83c3b 分页锚点改用文件里的原始 item id
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 6m22s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m49s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m19s
Project CI / Frontend tests (pull_request) Failing after 6m15s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m47s
Project CI / Native shell tests (pull_request) Successful in 12m22s
- 新增 direct_project_history_anchor_id:优先取 project.jsonl 原始 id,缺 id 时退回归一身份兜底
- read_direct_project_history_items_slice_at 的锚点匹配与 first_item_id 都改用原始 id
- 工具条目的调用与输出共用 call_id,按归一身份当锚点会先跳过 output 再把上一屏的 function_call 带回来
- 原分页用例更名 history_window_paginates_upwards_by_item_id_anchor
- 新增 pagination_anchor_uses_raw_item_id_for_tool_call_pairs,旧实现下会返回 call-1 而非 fc-1(已做变异验证)
2026-09-17 00:24:01 +08:00
k88936 ad19c9475e 回扫的 terminated 改成按真实换行结尾判定
- DirectProjectHistoryReverseLines 增记 saw_delimiter,第一段按"文件是否以换行结尾"判定,之后的行按已切掉的分隔符判定
- 尾部残行(上一行完整、这一行没有换行)解析失败时改为跳过继续回扫,不再报错、也不再就地结束丢掉更早的完整历史
- 换行结尾的坏行(含文件第一段)与顺序读取保持一致地失败关闭
- 补两条回扫用例:尾部残行不丢更早条目、坏掉的第一段必须失败关闭(都按旧实现变异验证过)
2026-09-17 00:18:49 +08:00
k88936 975a2e577c 终止按钮改按综合繁忙信号判定
- handleCancelDirectCodexTurn 的守卫改用 supervisorChatBusy,与「终止」按钮的可见条件对齐
- 刚提交、turn.started 还没到达的窗口里点击终止不再回"当前没有正在运行的回合"
- chat-composer 用例补一条:生命周期事件缺席时也能发出 cancel_direct_codex_turn
2026-09-17 00:10:20 +08:00
k88936 1b40f030e7 删掉聊天 reducer 里没人读的订阅身份与首屏锚点
- DirectThreadChatState 去掉 subscriptionId 与 lastCompletedItemId 两个只写不读的字段
- resolveDirectThreadBootstrap 只把 bootstrap 事件 reduce 进状态,不再顺带写入这两个字段
- directThreadChat 单测改成断言聊天状态里不再出现这两个字段(bootstrap 载荷本身不变)
2026-09-17 00:05:57 +08:00
k88936 098693636a 分页切片开头的前导条目并进后面那个回合
- buildDirectChatTurns 先攒住第一个用户条目之前的过程条目,交给后面那个回合
- 不再渲染没有用户气泡的孤儿回合;整份历史都没有用户条目时仍保留一个回合
- directTurnPresentation 单测补一条分页切片从半截回合开头的情形
2026-09-17 00:02:11 +08:00
k88936 e17af3008e 首屏历史切片改到 staleness 守卫之后再并入聊天 reducer
- loadProjectConversation 不再在 read_direct_project_history_slice 的 then 回调里直接写 directThreadChat
- 切片先暂存,等 loadVersion / localProjectPathRef 守卫通过后再与 hasMore、锚点一起落到状态
- 现有 launcher 按项目路径 key 重挂载 App,跨项目污染暂不可复现;本次只是让这条写入与其它状态写入的时序一致
2026-09-16 23:59:29 +08:00
k88936 64bd006981 测试骨架的历史切片按 limit 与锚点分页
- harness.ts 的 read_direct_project_history_slice 按生产口径从队尾取一屏、锚点条目不进窗口
- 按本屏最老一条算 firstItemId,收满一屏后再有条目才算 hasMore
- project-development.suite.ts 补一条用例:26 条历史只显示尾部 20 条,点「显示更早的对话」按锚点取回前 6 条后按钮消失
2026-09-16 23:52:02 +08:00
k88936 6a4cb2bc18 测试骨架的 subscribe bootstrap 按生产口径过滤
- harness.ts 记录未完成条目集合、事件序号与最新生命周期锚点
- subscribe 只回放未完成条目与最新生命周期锚点,已完成条目与增量正文不再补发
- 去掉已下发事件的序号记录,避免序号表无限增长
2026-09-16 23:48:46 +08:00
k88936 1b3a3b003f 测试骨架不再重复下发 turn.started
- harness.ts 记录运行态"最新回合是否在跑",emitDirectThreadEvents 只在生命周期事件上更新
- completeDirectThreadTurn 仅在回合没在跑时补 turn.started,避免与用例先发的事件重叠
2026-09-16 23:45:19 +08:00
k88936 ff8e517c8e 清理 DirectProject 回合结束后丢弃回复正文的死语句
- App.tsx 不再绑定 chat_with_game_creator_direct_codex 的返回值,删掉 void reply
- 保留说明:回复正文按条目身份从线程事件 / 历史切片进聊天,本地不再补第二条
2026-09-16 23:42:58 +08:00
k88936 d874a13a92 补记 Node 26 下 vitest jsdom 用例的 localStorage 坑
- pitfalls 记录:Node 26 的实验性 globalThis.localStorage 会顶掉 jsdom 的 Storage,导致只声明 jsdom 的用例读到 undefined
- 给出跑法:NODE_OPTIONS=--localstorage-file=… 后 clientApi 与 chatPromptPolish 共 26 项转绿
- 里程碑证据更新为全量结果:125 files / 1676 passed / 17 skipped,tsc exit 0
2026-09-16 22:41:54 +08:00
k88936 5eba53a5cc 补齐 DirectProject 聊天真相源收敛的文档结论
- 主规范:运行态事件改为下发与历史切片同形的脱敏原始条目,不再只给类型与身份
- 主规范与 ADR 写清回执竞态:回执前到达的通知只能记欠账,回执到达后必须补一次 consume
- ADR 与实施计划说明 list_game_creator_direct_active_turns 有意保留,服务首页运行中项目列表
- 里程碑验收标准按自动化证据勾选,运行时验收与已知环境缺口单独标注
- 实施计划补 export_bindings 后需 prettier 生成目录、测试文件更名与竞态风险项
2026-09-16 22:16:24 +08:00
k88936 33ddb5de27 DirectProject 聊天框改用线程订阅作为唯一运行态来源
- App.tsx 删除 Direct 回合事件订阅、turn-stream/tool-calls 读取、活动回合快照接管与瞬时应答文本
- 新增 game-creator-direct-thread-notify 唤醒的 subscribe/consume 单飞循环,过期时重订一次
- 通知先于订阅回执到达时记一笔欠账,回执到达后补一次 consume,避免回合尾部事件卡在队列里
- 历史切片并入同一个 reducer,聊天条目只由 selectDirectChatEntries 投影
- ProjectSupervisorView 改为渲染用户气泡 / 执行过程折叠区 / 最终回复,运行态只保留一个 turnRunning
- directTurnPresentation 重写为条目分区:连续工具合块、本地运行期说明只作最终提示
- 测试改按订阅事件与历史条目断言,补回执竞态、持久化工具卡片、空对话首轮三条用例
2026-09-16 22:16:15 +08:00
k88936 bf8f96c115 DirectProject 回合事件补齐用户条目与终态兜底
- 新增 direct_thread_id_for_project:订阅、回合事件写入与兜底释放共用一个线程身份
- 订阅命令改用同一个身份函数,删掉命令层自己那套路径归一
- 用户消息落盘后立刻作为 item.completed 下发,前端用同一个 itemId 合并乐观气泡与历史条目
- 采集终态事件是否已经下发,缺失时按成功 / 失败补一条 turn.completed,避免前端永远停在运行中
- 取消回合的 Stale 分支补一条 turn.completed(aborted)
2026-09-16 22:15:57 +08:00
k88936 e469831543 修复快速编辑提示词把 @ 引用写成内部 id(review 第 3、4 条)
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m39s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m8s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m55s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m11s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m45s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m10s
Project CI / Native shell tests (pull_request) Successful in 8m37s
Project CI / Frontend tests (pull_request) Successful in 8m46s
- applyResourceQuickEditPrompt 与 applyResourceQuickEditDraft 改用 directCodexContentToPromptText(content, manifest.assets),并在 useCallback 依赖里加上 manifest.assets
- 两处必须同口径:比较用的草稿文本与落进 quickEditPanel.prompt 的文本都按显示名展开,出站 derive_local_project_resource 的 prompt 与聊天 @ 逐字一致,也不会每次输入都重跑 replaceText
- 补充两处注释说明素材清单口径,shared-memory/pitfalls 记录「派生文本漏传 manifest.assets」的现象、原因、处理与验证
2026-09-16 21:26:53 +08:00
k88936 869abad879 修复排队消息 chip 的 @ 引用露出内部 resourceId(review 第 2 条)
- queuedChatTurnLabel 增加 assets 入参并透传给 directCodexContentToPromptText,@ 引用按 manifest 显示名展开,与聊天输入区文案口径一致
- ComposerTurnQueue 增加 assets 属性,ProjectSupervisorView 把 chatProjectAssets 传下去
- chat-composer 用例新增「队列 chip 的 @ 引用按 manifest 显示名展开」,钉住显示名文案与不传清单时的稳定 id 兜底
2026-09-16 21:25:38 +08:00
k88936 280a2dc5d1 修复 DirectProject 输入草稿丢掉引用后的换行(review 第 1 条)
- ResourceReferenceInput 的草稿投影改为「向前合并」:上一 part 是引用 chip 时先把待写文本攒在 pendingText 上,等下一个文本 part 到来再合并,段落分隔符与软换行不再被 trim() 丢掉
- 攒到最后仍只有空白的文本不入 content,避免产出 Rust validate_direct_codex_user_item 会拒绝的空 input_text
- 新增 resourceReferenceInput 用例「引用后面的段落分隔不会被吞掉」,钉住 chip 后一段的 content part、派生文本与「不产生纯空白 part」
- 同步更新 canonical content 严格边界里程碑的实现结论与证据,并在 shared-memory/pitfalls 记录该缺陷的现象、原因、处理与验证
2026-09-16 21:24:41 +08:00
k88936 3c6e084a41 DirectProject 历史切片改为从文件尾回扫并删掉聊天框只读命令
- 新增 DirectProjectHistoryReverseLines:按块从文件尾逐行回读 project.jsonl
- 幂等回扫与"读一屏"共用这一套尾部回读,不再各自从文件头读到尾
- read_direct_project_history_items_slice_at 语义不变仍返回元组,新增第四项为本屏最老的 itemId 作分页锚点
- 分页锚点只按归一身份 itemId 匹配,不再接受第二个 id
- read_direct_project_last_item_id_at 改为尾部回扫读一行,不再读整份历史
- 删除只服务聊天框回读的 read_direct_tool_calls / read_direct_turn_stream 命令与注册
- 保留 list_game_creator_direct_active_turns:它服务首页跨页面"运行中的项目",不是聊天框读路径
- 补历史切片尾部回扫与分页锚点两条用例
2026-09-16 20:17:15 +08:00
k88936 ed3369a494 文档先行:把 DirectProject 聊天真相源收敛的文档改到当前实现状态
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m13s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m39s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m43s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m8s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m10s
Project CI / Frontend tests (pull_request) Failing after 3m24s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m7s
Project CI / Native shell tests (pull_request) Successful in 6m8s
- 主规范把事件模型写成 ts-rs 导出的 tagged enum,说明 seq 不下发、事件不带回合身份,并补上 ts 绑定生成与 f64 时间戳约定
- ADR 补上 tagged enum 绑定、turn id 删除、正文只增不减与回合回收、前端卡片去掉 turnId、未知类型前端丢弃
- 里程碑与实施计划同步:文件改名到 direct_thread_wire.rs,新增删回合身份与生成绑定两步,验证命令加上 export_bindings
- 工具卡片技术方案在文首加修订段:数据来源层作废,表现层契约仍然有效,DirectRuntime 的 tool-calls.jsonl 写入保留
- 对话回合唯一投影里程碑改为 superseded,列出仍然有效与已作废的分界
2026-09-16 19:57:21 +08:00
k88936 fdc48fe725 前端聊天投影按 ts-rs 生成类型收口,删掉 callId 与回合身份
- directThreadItemProjection.ts 直接消费生成的 DirectThreadItem,工具卡片 kind / 标题 / 摘要 / 状态与可见性判定都在这里
- directThreadChat.ts 聊天条目只留一个 itemId:去掉 callId 概念,去掉 deltaText 缓冲,增量直接追加到运行态条目正文
- 合并规则收成先到者赢、后到只补空:正文只增不减、工具状态允许从 running 升级到终态
- 回合结束把运行态条目并入历史并清空 live,条目既不消失也不重复
- directThreadEvents.ts 改为转发 ts-rs 绑定,只保留历史条目转消息的过渡函数
- ToolCallGroup 与 toolCallGroupPresentation 改用去掉回合身份的 DirectChatToolCard
- generated/index.ts 补上 DirectThread* 绑定导出
- 回退本仓库其它模块仍在改的 DirectCodexUser* 绑定重新生成(避免误改他人进行中的契约)
- 两个 vitest 用例文件按新 API 重写,11 条用例通过
2026-09-16 19:49:26 +08:00
k88936 2748468d12 DirectProject 聊天事件改用 ts-rs 导出的 tagged enum 并删掉 turn id
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m51s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m2s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m14s
Project CI / Native shell tests (pull_request) Failing after 2m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 46s
Project CI / Frontend tests (pull_request) Failing after 3m32s
- 新增 direct_thread_wire.rs:DirectThreadItem / DirectThreadEvent / 订阅与历史切片全部改成 ts-rs 导出的 tagged enum,取代原大而全的可空结构体
- 删除 direct_thread_raw_item.rs,模块注册与直通引用改到 direct_thread_wire
- 条目身份只看一个 itemId:工具条目的第二个 id 在 Rust 边界归一,不再对外暴露
- 删除 DirectProject 聊天事件里的 turn id:生命周期用无载荷的 turn.started / turn.completed{status} 表示
- append 直接接收 DirectThreadEvent 并返回同一事件,队列内部自算 seq
- 请求事件改为携带 DirectThreadRequestKind,去掉字符串中转
- 思考增量走 ReasoningDelta 通道,与正文增量共用 item.delta
- at 用 #[ts(as = "f64")] 对齐 Tauri JSON 通道的 number
- 用 cargo test export_bindings 重新生成 project-workspace/generated 绑定
2026-09-16 19:43:57 +08:00
k88936 09ad0073fe 修正 DirectProject 订阅语义:bootstrap 事件就是当前要处理的事件
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
subscribe 已在同一边界把游标钉在队尾,因此前端直接 reduce bootstrap 事件,不再补一次 consume
主规范、ADR 与实施计划同步删除"竞态窗口"说法,notify 仍是唯一唤醒来源
2026-09-16 19:06:05 +08:00
k88936 42b702d362 按 grill 结论修正 DirectProject 聊天真相源收敛计划
里程碑与实施计划改定边界:Thread Manager 只搬运脱敏原始条目,不生成卡片形状与可见性
条目身份归一为 call_id ?? id,Thread Manager 与前端不再暴露第二个 id 概念,分页锚点仍取文件里的原始 item id
合并规则收敛为"先到定形、后到补空白",删除逐字段优先级表
主规范补充条目形状三条职责边界,并说明思考正文流式下发不放宽可见范围
ADR 同步:DirectRuntime 自己的进度事件与投影文件保留,前端删除的是 DirectProject 的读路径
失败与中止说明只在运行期显示;历史切片改为从文件尾反向扫描;未知 item 类型透传并加 TODO
2026-09-16 19:05:04 +08:00
k88936 edfd1f9143 修正 Godot 直接回合用例对 canonical content 的断言
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
- appSurface Godot 用例不再断言被裁剪过的纯文本,改为断言编辑器实际提交的 canonical content(Shift+Enter 与组合态 Enter 插入的换行原样保留)
2026-09-16 18:51:08 +08:00
k88936 423f2d7184 修复独立总控聊天窗口未发送草稿的恢复
- App.tsx 增加 supervisorChatDraftHydratedRef 守卫的挂载水合:独立总控聊天窗口挂载走 loadProjectConversation,不经过 openWorkspace,此前 sessionStorage 里的草稿只写不读,重载后为空
- 仅在 supervisorChatOnly、存在持久化草稿且输入框已挂载时回填一次,避免覆盖用户后续输入
2026-09-16 18:50:55 +08:00
k88936 656c89e4b2 把 DirectProject 聊天条目的投影从前端收口,Rust 只搬运脱敏原始条目
新增 agent/direct_thread_raw_item.rs:把 Codex 原始 response item 与 app-server item 收敛成同一形状的脱敏原始条目,只做挑字段、脱敏、截断,不再生成卡片的 kind / 标题 / 折叠摘要
删除 agent/direct_chat_entry.rs:工具卡片形状不再由 Rust 生产
历史切片改为返回脱敏原始条目列表,不再在 Rust 侧合并 function_call 与 function_call_output
新增前端 directThreadItemProjection.ts:工具卡片 kind / 标题 / 摘要 / 状态与可见性判定全部在前端完成
directThreadChat reducer 改用同一投影函数处理运行态事件,新增 mergeDirectHistoryItems 走同一份投影与合并规则
DirectThreadHistorySlice 去掉 entries 与 itemTimestamps,改为 items + firstItemId
2026-09-16 18:45:13 +08:00
k88936 aa9076c95c 删除 DirectProject 图片专用 content part
- 移除 DirectCodexUserContentPart 的 agc_image_reference 分支与对应 ts-rs 绑定
- Rust 校验与 Codex wire 投影只保留 agc_attachment_reference
- 更新附件顺序投影测试的用例名、输入与断言
- 同步聊天素材功能说明、用户 Response item 里程碑与实施计划
- 在决策日志记录上传文件统一走附件 part、不保留图片兼容分支
2026-09-16 18:29:24 +08:00
k88936 0ec1179bf1 移除前端 consume 轮询兜底
- 唤醒只由 notify 驱动,bootstrap 后立刻 consume 一次补齐竞态窗口
- 定时 tick 无法自举:判断活动回合本身依赖事件,唤醒丢失时定时器不会启动
- 主规范、ADR 与实施计划同步删除该条款
2026-09-16 18:17:38 +08:00
k88936 2938a49cac 固化 DirectProject 聊天真相源契约
- 主规范新增首屏锚点、生命周期锚点必进 bootstrap、事件自足与不提供单点快照四条契约
- 新增 ADR 记录单一事实源取舍、删除清单与待验证的 id 空间风险
- 新增里程碑与实施计划,CONTEXT.md 补项目对话历史/运行态事件/聊天投影词条
- 目标文档合同与文档索引同步
2026-09-16 18:05:30 +08:00
k88936 ae0f9376c9 新增 DirectProject 聊天 reducer 深模块
- directThreadChat 把运行态事件与历史切片归并成同一份聊天条目,活动回合只由 turn.started/turn.completed 判定
- item.delta 载荷补 kind,reducer 不再靠 itemId 猜条目类型
- bootstrap 原子替换运行态并保留历史窗口,历史与运行态按 callId ?? itemId 去重合并
- 新增 5 条 reducer 单测
2026-09-16 18:05:10 +08:00
k88936 29d0cbb4df 运行态事件与历史切片共用聊天条目投影
- 新增 direct_chat_entry 深模块,把原始 response item 与 app-server item 投影成同一聊天条目
- item.started/item.completed 载荷改为完整条目,前端不再需要按 itemId 取快照
- 原始 function_call 与 function_call_output 按 callId 并成一张卡片,文本与明细统一脱敏截断
- 历史切片新增 entries 与 first_item_id,分页锚点不再依赖是否出现可显示条目
2026-09-16 18:01:50 +08:00
k88936 41366dd71d 修复DirectProject canonical内容边界(review 第 8 条)
- 移除 chatComposerDraftToDirectCodexUserItem 对纯空白 input_text 的预过滤,原样传递 draft.content
- ChatComposerDraft 只保留 canonical content,删除 text / references 旧字段
- 新增 directCodexContentToPromptText、hasMeaningfulDirectCodexContent、directCodexUserItemFromContent 供 caller 与展示派生
- 删除 executeChatAgentReply 的 userItem 兜底分支,首页首轮、队列出队、普通提交与策略重试显式构造 canonical user item
- QueuedChatTurn 只保存 clientTurnId 与 userItem,队列 chip 文案由 content 派生
- 旧 Planner / legacy Supervisor caller 显式构造纯文本 item 并留下迁移 TODO
- 资源输入区对外只暴露 canonical content,内部文本草稿改为文本 → content 重建
- 迁移草稿、润色、队列与 appSurface 测试 fixture 到 content-only
- 更新 canonical content 里程碑与实施计划文档结论
2026-09-16 17:46:41 +08:00
k88936 721e45f01b 统一 DirectProject 运行态条目的合并身份
- Thread Manager 事件新增 callId,条目身份统一为 callId 优先、其次 itemId
- 工具条目按 callId 归并 started/completed,不再留下永远收不到完成事件的幽灵活跃条目
- 历史锚点仍取 response item id,保证分页锚点能在 project.jsonl 中命中
- 补充跨 id 空间归并的单元测试
2026-09-16 17:45:28 +08:00
k88936 f4b496caca 补充DirectProject canonical内容严格边界计划
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
- 明确 content-only 输入与最终空内容校验

- 规划删除 userItem 兜底并修正 Direct caller
2026-09-16 15:28:25 +08:00
k88936 fc0fd846f9 避免快速编辑润色回填重复更新
- 回填前比较编辑器当前文本

- 防止输入区 onChange 与宿主回调形成循环
2026-09-16 13:22:57 +08:00
k88936 fa99d9ad62 避免总控草稿初始化误删
- 编辑器尚未恢复时跳过空草稿持久化

- 保留已有会话草稿等待输入区完成恢复
2026-09-16 13:20:47 +08:00
k88936 64a52005f8 修复清空草稿后润色状态残留
- 在编辑器发出空草稿时重置润色结果

- 清除本轮发送提醒确认标记
2026-09-16 13:20:01 +08:00
k88936 645d806fed 修复空初始草稿吞掉首次编辑
- 仅在初始内容实际非空时跳过初始化回调

- 保证快速编辑首次输入同步到宿主状态
2026-09-16 13:19:13 +08:00
k88936 0b9d16e8f1 修复润色按钮无法感知输入
- 用 React 状态同步编辑器当前文本

- 让按钮禁用态随用户输入即时更新
2026-09-16 13:18:31 +08:00
k88936 d5c4e0e780 修复快速编辑润色结果未回填
- 为快速编辑输入区绑定编辑器句柄

- 润色回调同时更新面板状态与编辑器文本
2026-09-16 13:17:50 +08:00
k88936 30e27abc60 修复建议命令草稿残留引用
- 准备命令草稿前先清空编辑器

- 避免提交用户上一轮选择的素材引用
2026-09-16 13:16:07 +08:00
k88936 b607501916 修复总控草稿恢复后被清空
- 仅在非纯聊天模式清空聊天编辑器

- 保留纯聊天模式恢复的会话草稿
2026-09-16 13:15:08 +08:00
k88936 46e3cd9744 内联 DirectProject 附件与图片内容
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m30s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m35s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m54s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m1s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m52s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m41s
Project CI / Frontend tests (pull_request) Failing after 3m54s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m41s
Project CI / Native shell tests (pull_request) Successful in 6m9s
新增附件与图片 canonical content part 及 ts-rs 绑定

移除 DirectProject Tauri 附件 sidecar 追加路径

修正前端引用字段与生成类型适配

补充附件图片顺序投影测试并同步里程碑文档
2026-09-16 13:03:11 +08:00
k88936 5fb3db662b 合并主线并保留 DirectProject 输入行为
合并主线回合队列、工具流展示与活动回合管理

保留 Lexical 单一输入状态及附件图片 sidecar 投影

收敛 DirectProject user item 校验、历史恢复与图片预算

同步输入链路里程碑和历史异常恢复文档
2026-09-16 11:27:09 +08:00
k88936 87b798322e 收敛输入编辑器单一状态并补齐附件投影
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m6s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 1m49s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 1m56s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m2s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m46s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Failing after 3m54s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m13s
Project CI / Native shell tests (pull_request) Successful in 7m14s
聊天与快速编辑输入统一由 Lexical 编辑器持有唯一当前状态

App 通过输入句柄读取草稿,移除跨视图 EditorState 和字符串镜像

DirectProject 将附件与图片 sidecar 投影纳入 prompt,支持附件-only 输入

迁移润色测试并更新 DirectProject 里程碑验收记录
2026-09-15 22:13:25 +08:00
k88936 ec286b3480 抽取通用富文本输入组件
新增可复用的 RichTextInput Lexical 输入外壳。

让首页输入与资源引用输入共享编辑器、回车提交和禁用状态。

保留首页附件与图片粘贴节点能力,并移除 DirectProject 草稿 legacy fallback。

统一聊天草稿 canonical content 状态并更新资源引用测试。
2026-09-15 19:42:37 +08:00
k88936 35733f0e33 过滤 canonical 草稿中的空白输入 part
在统一 user item 转换入口移除纯空白 input_text,覆盖恢复草稿和所有提交路径。
2026-09-15 17:54:00 +08:00
k88936 3c7b02b9f8 过滤 DirectProject 编辑器的空白内容 part
跳过纯空白文本节点并合并相邻文本,避免引用后的分隔空格和换行触发 Rust 空输入校验。
2026-09-15 17:53:33 +08:00
k88936 f2030a616f 复用 DirectProject 用户输入校验读取的 manifest
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 1m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m17s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m17s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / Frontend tests (pull_request) Failing after 3m5s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m51s
Project CI / Native shell tests (pull_request) Successful in 6m21s
让校验返回已读取的 manifest,wire 投影复用同一份数据,避免一次输入重复读取项目清单。
2026-09-15 17:33:49 +08:00
k88936 b3b5d77990 按运行模式创建 DirectProject 回合输入
仅在 DirectCodex 产品运行时生成 direct clientTurnId 和 user item,避免旧 Supervisor 与策划链路复用错误身份。
2026-09-15 17:32:46 +08:00
k88936 1da106af7d 避免重复校验 DirectProject 用户输入
复用 canonical 到 prompt 投影函数中的校验,删除不可达空 prompt 分支。
2026-09-15 17:29:08 +08:00
k88936 cd2edd6966 移除未使用的 DirectProject 输入信封类型
删除未被调用且未生成到前端绑定的 DirectCodexUserMessageEnvelope,收窄内部 API。
2026-09-15 17:27:42 +08:00
k88936 22e830ff93 避免 DirectProject 回合输入的无必要克隆
在互斥的观察器分支中移动 optional user item,避免深层 JSON 克隆。
2026-09-15 17:26:34 +08:00
k88936 c266ae7b50 清理策划模式提交后的聊天草稿
规划 V2 提交时同步清空资源引用和 canonical content,避免切回 DirectProject 后复用旧输入。
2026-09-15 17:25:15 +08:00
k88936 a543b75cf7 拆开聊天草稿内容分支判断
将资源草稿转换的三种回退路径改为显式分支,保持现有优先级并提升可读性。
2026-09-15 17:24:05 +08:00
k88936 f213987f9a 复用资源引用的统一内容映射
让 Lexical 草稿读取复用 chatReferenceToContentPart,避免引用字段映射分叉。
2026-09-15 17:22:55 +08:00
k88936 2c623bb577 移除 DirectProject 回合输入的重复赋值
删除已在对象初始化时设置的 userItem 重复赋值。
2026-09-15 17:19:17 +08:00
k88936 3353906e6f 修正 Response item content 类型投影
区分 turn/start 的 text 与历史 message 的 input_text

将 AGC 私有引用转换为标准 input_text

新增回归测试避免未知 variant text
2026-09-15 16:57:04 +08:00
k88936 17716347e2 收紧历史测试与注释口径
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m19s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m17s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m43s
Project CI / Frontend tests (pull_request) Failing after 4m6s
Project CI / Native shell tests (pull_request) Successful in 7m29s
移除 legacy 投影描述

将历史测试统一为 Response item 合同
2026-09-15 16:16:03 +08:00
k88936 9ad66a67a3 接入前端 canonical 用户 Response item
按 Lexical 顺序构造 message.content

DirectProject payload 改为发送 userItem

策略重试保留同一 canonical item
2026-09-15 16:12:56 +08:00
k88936 ba3aa3ccdb 更新用户 Response item 里程碑进度
标记 canonical、wire 与失败关闭验收项

同步实施计划为进行中状态
2026-09-15 16:09:28 +08:00
k88936 2b38eaafba 补充前端用户 Response item 契约测试
覆盖 Lexical 文本与引用交错顺序

验证资源引用只发送稳定 resourceId
2026-09-15 16:08:34 +08:00
k88936 7d5b9071e7 删除旧 DirectProject 引用协议
移除 DirectCodexTurnReference 平行 DTO 与渲染路径

将引用数量上限收回 canonical user item 校验模块
2026-09-15 16:01:45 +08:00
k88936 9e83f1d88c 补齐 Response item 严格合同测试
拒绝资源引用额外身份字段与未知 content part

补充历史 item wire 投影边界测试

同步 DirectProject 历史文档为无 fallback 合同
2026-09-15 15:55:15 +08:00
k88936 7e35d7c344 严格使用 DirectProject Response item 历史
移除 game-creator-conversation.v1 读取 fallback

历史注入前逐项投影 canonical user item 为 Codex message

无效 item 在持久化前失败关闭
2026-09-15 15:42:27 +08:00
k88936 5ec40c8b83 抽离 DirectProject 身份解析模块
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m16s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m20s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m4s
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 1m54s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m22s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m20s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m36s
Project CI / Frontend tests (pull_request) Successful in 3m45s
Project CI / Native shell tests (pull_request) Successful in 5m27s
将项目 canonical identity 与 digest 放入独立文件

保持 Codex app-server 线程池调用接口不变
2026-09-15 15:22:39 +08:00
k88936 948a80fc49 重构 DirectProject Agent 深模块
将 direct_runtime 与 codex_app_server 改为目录模块

拆分 user item 的模型、校验与 wire 投影文件

抽离 DirectProject 历史注入载荷构造 seam
2026-09-15 15:19:17 +08:00
k88936 dfd6fadedf 新增 DirectProject 用户 Response item 契约
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m35s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m34s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m36s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m31s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m46s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m32s
Project CI / Frontend tests (pull_request) Successful in 4m0s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m34s
Project CI / Native shell tests (pull_request) Successful in 6m35s
新增 Rust canonical user item 与 Codex wire 转换模块

增加 ts-rs 绑定目录及生成文件检查边界

更新聊天素材引用规范与里程碑实施计划
2026-09-15 14:45:58 +08:00
286 changed files with 9947 additions and 57471 deletions
+8
View File
@@ -174,6 +174,14 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject ## 项目开发对话(DirectProject
**DirectProject 专属聊天模块**:
AGC 普通项目聊天的独立容器,拥有 DirectProject 的聊天状态、运行态订阅、历史读取、发送队列、附件和中止交互,并把聊天投影交给专属表现层渲染;它不承接 Supervisor、Design Agent 或 Planning V2 的运行态。
_Avoid_: 把 DirectProject 作为项目总控聊天的一个布尔分支、把四种 Agent 会话抽象成同一事实源
**项目工作台布局**:
承载本地项目的资源工作区、项目级工具和独立聊天产品路径的外层界面;布局拥有跨面板的账户/钱包入口,聊天模块只负责项目对话,不嵌套账户展示。
_Avoid_: 把钱包入口塞进聊天设置、让聊天组件拥有工作台级账户状态
**项目对话历史**: **项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。 AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本 _Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
@@ -81,14 +81,12 @@ test('模板批量导入走 multipart,不预设 JSON Content-Type', async () =
}, },
], ],
}; };
const fetchMock = vi const fetchMock = vi.fn().mockImplementation(
.fn() async () =>
.mockImplementation( new Response(JSON.stringify({ ok: true, data: imported }), {
async () => status: 200,
new Response(JSON.stringify({ ok: true, data: imported }), { }),
status: 200, );
}),
);
vi.stubGlobal('fetch', fetchMock); vi.stubGlobal('fetch', fetchMock);
const form = new FormData(); const form = new FormData();
@@ -185,7 +185,12 @@ test('nextVersion 只在 patch 位递增', () => {
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => { test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
const base = { const base = {
args: ['cp', '--force', '/tmp/a.json', 'oss://agc-dev/agc/global-version.json'], args: [
'cp',
'--force',
'/tmp/a.json',
'oss://agc-dev/agc/global-version.json',
],
endpoint: 'oss-rg-china-mainland.aliyuncs.com', endpoint: 'oss-rg-china-mainland.aliyuncs.com',
accessKeyId: 'id', accessKeyId: 'id',
accessKeySecret: 'secret', accessKeySecret: 'secret',
@@ -107,10 +107,6 @@ const appInvokeSources = readSourceFiles(
new URL('../src/', import.meta.url), new URL('../src/', import.meta.url),
new Set(['.ts', '.tsx']), new Set(['.ts', '.tsx']),
); );
const appEntrypointSource = fs.readFileSync(
new URL('../src/main.tsx', import.meta.url),
'utf8',
);
const tauriHandlerSource = fs.readFileSync( const tauriHandlerSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url), new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8', 'utf8',
@@ -135,6 +131,45 @@ const rustSharedContractSource = fs.readFileSync(
); );
const allowedUncalledTauriCommands = [ const allowedUncalledTauriCommands = [
'append_direct_project_conversation_message', 'append_direct_project_conversation_message',
// Supervisor 调试窗口、开发者面板、专业 Agent 对话与旧命令聊天的前端调用方已随
// Supervisor 前端链路整体删除;命令本身仍注册在 Rust 侧并由 native Runtime、CLI
// swarm 与 Rust 测试使用,保留 present,仅不再出现在 App 前端源码里。
'answer_game_creator_agent_runtime_user_input',
'cancel_game_creator_agent_runtime_task',
'chat_with_game_creator_role_agent',
'chat_with_game_creator_role_agent_stream',
'check_game_creator_llm_config',
'confirm_game_creator_agent_runtime_task',
'diff_local_project_checkpoint',
'get_game_creation_agent_capabilities',
'get_limited_local_commands',
'list_local_project_export_packages',
'read_game_creator_agent_runtime',
'read_local_agent_memory',
'read_local_game_memory',
'reject_game_creator_agent_runtime_task',
'retry_game_creator_agent_runtime_task',
'schedule_game_creator_agent_ready_tasks',
'start_game_creator_agent_runtime_task',
'steer_game_creator_agent_runtime_task',
'write_local_agent_memory',
'write_local_game_memory',
'write_local_project_file',
// Agent 运行时会话 / 目标 / 协作命令由 native 侧与 CLI swarm 驱动,前端没有调用方。
'archive_game_creator_agent_session',
'clear_game_creator_agent_goal',
'compact_game_creator_agent_runtime_context',
'confirm_retry_game_creator_agent_runtime_task',
'create_game_creator_agent_session',
'edit_game_creator_agent_goal',
'fork_game_creator_agent_session',
'list_game_creator_agent_sessions',
'pause_game_creator_agent_goal',
'read_game_creator_agent_goal',
'resume_game_creator_agent_goal',
'set_active_game_creator_agent_session',
'start_game_creator_agent_goal',
'start_game_creator_supervisor_runtime_task',
// TODO: Remove the retired binding command after the legacy runtime path is removed. // TODO: Remove the retired binding command after the legacy runtime path is removed.
'bind_components', 'bind_components',
'chat_with_game_creator_agent', 'chat_with_game_creator_agent',
@@ -164,6 +199,26 @@ const allowedUncalledTauriCommands = [
'call_agc_plugin', 'call_agc_plugin',
'read_agc_plugin_panel', 'read_agc_plugin_panel',
'set_agc_plugin_enabled', 'set_agc_plugin_enabled',
// 下面这些命令的调用方只有随 Project Supervisor 前端链路一起删除的旧命令聊天入口;
// 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 / 导出包 /
// 画板同步 / 素材登记 / 权限策略 / 本地草案 / 平台美术),Rust 侧只剩注册与实现,
// `*_at` helper 仍由 Rust 用例覆盖。接回新入口还是删除属于 native 能力取舍,先按
// native-only 登记,避免孤儿检查一直报错。
// 预览不在本清单:`activate_local_game_preview` 已按 ADR 回接到 App 的「运行」入口。
'build_local_project_index',
'control_agent_run',
'create_local_project_checkpoint',
'export_local_project_package',
'generate_local_game_draft',
'generate_platform_art_asset',
'import_canvas_asset',
'import_canvas_export',
'open_canvas_project',
'register_local_asset',
'restore_local_project_checkpoint',
'run_limited_local_command',
'sync_canvas_project_assets',
'write_project_permission_policy',
]; ];
const sourceExtensions = new Set([ const sourceExtensions = new Set([
'.json', '.json',
@@ -1470,13 +1525,7 @@ const eventCapability = JSON.parse(
); );
const eventCapabilityWindows = new Set(eventCapability.windows ?? []); const eventCapabilityWindows = new Set(eventCapability.windows ?? []);
const eventCapabilityPermissions = new Set(eventCapability.permissions ?? []); const eventCapabilityPermissions = new Set(eventCapability.permissions ?? []);
for (const windowLabel of [ for (const windowLabel of ['client', 'main', 'launcher']) {
'client',
'developer',
'main',
'launcher',
'supervisor-chat',
]) {
if (!eventCapabilityWindows.has(windowLabel)) { if (!eventCapabilityWindows.has(windowLabel)) {
throw new Error( throw new Error(
`AI game creator shell event capability missing window: ${windowLabel}`, `AI game creator shell event capability missing window: ${windowLabel}`,
@@ -1859,20 +1908,6 @@ if (
); );
} }
for (const snippet of [
'import.meta.env.DEV',
'supervisorChatMode',
'supervisorChatOnly',
'open_project_supervisor_chat_window',
'index.html?supervisor-chat&projectPath=',
]) {
if (!`${appEntrypointSource}\n${tauriRustSource}`.includes(snippet)) {
throw new Error(
`AI game creator shell developer window guardrail drifted: ${snippet}`,
);
}
}
if (tauriHandlerSource.includes('open_developer_window(app.handle())?')) { if (tauriHandlerSource.includes('open_developer_window(app.handle())?')) {
throw new Error( throw new Error(
'AI game creator normal startup must not automatically open the developer window', 'AI game creator normal startup must not automatically open the developer window',
@@ -1905,31 +1940,17 @@ for (const snippet of [
'官方账号服务(固定)', '官方账号服务(固定)',
'runtime_config.save', 'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'", "'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
"'activate_local_game_preview'",
'已切换到客户端运行视图',
'async function executeRunLocal', 'async function executeRunLocal',
'function needsInitializedChatProject', 'function needsInitializedChatProject',
'function resolvePendingCommandProjectPath',
'resolveChatProjectPath(localProject) ?? draftProjectPath',
'`permission.cancel ${command.id} missing-project`',
"'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'", "'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'",
"'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'", "'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'",
'function parseRememberInput', 'function parseRememberInput',
"'/trace 或 /loop:查看最近一次 Agent loop trace'", "'/trace 或 /loop:查看最近一次 Agent loop trace'",
'async function executeAgentTraceChat',
"relativePath: '.agent/logs/command.log'",
"'permission.pending'", "'permission.pending'",
"'permission.confirm'", "'permission.confirm'",
"'permission.cancel'", "'permission.cancel'",
"'command.auto'", "'command.auto'",
"'agent.run_status'",
'function summarizeAgentRunTrace', 'function summarizeAgentRunTrace',
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
'agentRunTrace.error ?',
'className="trace-error"',
'agentRunTrace.taskGraph.repairRoutes.map',
"in: ${step.inputPaths.join(', ') || 'none'}",
"out: ${step.outputPaths.join(', ') || 'none'}",
]) { ]) {
if (!appSource.includes(snippet)) { if (!appSource.includes(snippet)) {
throw new Error( throw new Error(
@@ -1192,7 +1192,7 @@ async function main() {
function isDirectModuleExecution() { function isDirectModuleExecution() {
return Boolean( return Boolean(
process.argv[1] && process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url), resolve(process.argv[1]) === fileURLToPath(import.meta.url),
); );
} }
@@ -1,7 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "developer",
"description": "开发窗口允许打开本地素材选择对话框。",
"windows": ["developer"],
"permissions": ["dialog:allow-open"]
}
@@ -2,6 +2,6 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "events", "identifier": "events",
"description": "允许客户端窗口订阅并取消订阅 Rust Runtime 事件。", "description": "允许客户端窗口订阅并取消订阅 Rust Runtime 事件。",
"windows": ["client", "developer", "main", "launcher", "supervisor-chat"], "windows": ["client", "main", "launcher"],
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"] "permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
} }
@@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "window-chrome", "identifier": "window-chrome",
"description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。", "description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。",
"windows": ["client", "developer", "main", "launcher", "supervisor-chat"], "windows": ["client", "main", "launcher"],
"permissions": [ "permissions": [
"core:window:allow-close", "core:window:allow-close",
"core:window:allow-is-maximized", "core:window:allow-is-maximized",
@@ -13,7 +13,7 @@
"taonier_prepare_game_art.parameters.brief": "面向当前游戏的简洁视觉需求", "taonier_prepare_game_art.parameters.brief": "面向当前游戏的简洁视觉需求",
"taonier_prepare_game_art.parameters.mode": "缺省安全复用有效美术包;Codex 仅在当前对话需要换一套或重新生成时使用 regenerate", "taonier_prepare_game_art.parameters.mode": "缺省安全复用有效美术包;Codex 仅在当前对话需要换一套或重新生成时使用 regenerate",
"agc_generate_image.description": "生成一张新图片:普通插画、角色立绘、统一视觉规范图、游戏 UI 设计图或透明游戏素材图集。仅在用户明确要求生成新图时调用。", "agc_generate_image.description": "生成一张新图片:普通插画、角色立绘、统一视觉规范图、游戏 UI 设计图或透明游戏素材图集。仅在用户明确要求生成新图时调用。",
"agc_generate_image.parameters.prompt": "完整图片描述;普通图片、角色、规范图、UI 设计图或透明图集均可", "agc_generate_image.parameters.prompt": "完整图片描述;普通图片、角色、规范图、UI 设计图或透明图集均可。kind=icon-spritesheet 时,去除首尾空白后的描述须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交;超限拒绝,不截断、不拆条,客户端不追加生图指令",
"agc_generate_image.parameters.kind": "image=普通新图(保留生成原图),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),icon-spec=统一视觉规范图,ui-design=完整 UI 设计图,icon-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图", "agc_generate_image.parameters.kind": "image=普通新图(保留生成原图),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),icon-spec=统一视觉规范图,ui-design=完整 UI 设计图,icon-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图",
"agc_generate_image.parameters.assetName": "本地素材的人类可读显示名称", "agc_generate_image.parameters.assetName": "本地素材的人类可读显示名称",
"agc_generate_image.parameters.outputPath": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件", "agc_generate_image.parameters.outputPath": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件",
@@ -23,8 +23,6 @@
"icon_spec_generation": "为这个 Web 小游戏生成一张 1:1 的统一视觉规范图,作为后续 UI 设计图和透明游戏图集的共同权威参考。规范板必须分区展示:玩家主体及其成长形态、核心目标或收集物、场景地块与障碍、HUD/操作图标、得分/受击/胜负反馈、主辅强调色与材质规则。所有元素使用一致的正交视角、轮廓、光照和原创视觉语言,留出清楚间距;不要生成完整游戏截图、海报、黑底图集或纯文字说明。玩法机制只用于理解功能,不授权复刻现有作品。\n\n项目视觉需求:{}", "icon_spec_generation": "为这个 Web 小游戏生成一张 1:1 的统一视觉规范图,作为后续 UI 设计图和透明游戏图集的共同权威参考。规范板必须分区展示:玩家主体及其成长形态、核心目标或收集物、场景地块与障碍、HUD/操作图标、得分/受击/胜负反馈、主辅强调色与材质规则。所有元素使用一致的正交视角、轮廓、光照和原创视觉语言,留出清楚间距;不要生成完整游戏截图、海报、黑底图集或纯文字说明。玩法机制只用于理解功能,不授权复刻现有作品。\n\n项目视觉需求:{}",
"ui_design_generation": "根据下方当前项目 UI 需求生成一张完整的游戏 UI/UX 原型图,玩法与界面结构以这些需求为准。画面是完整 16:9 桌面端单屏界面,并同时明确移动端重排意图;清楚呈现当前玩法所需的分数/资源/生命/局内状态 HUD、主要可玩区域、玩家与目标/收集物/危险物、开始和主要操作、失败状态与重新开始、键盘和触控提示。使用正视角、清晰分区和可读占位文字,使前端开发可直接据此拆分 HTML/CSS。采用项目已经确定的原创命名、角色轮廓、配色、场景材质和界面视觉语言。\n\n当前项目 UI 需求:{}", "ui_design_generation": "根据下方当前项目 UI 需求生成一张完整的游戏 UI/UX 原型图,玩法与界面结构以这些需求为准。画面是完整 16:9 桌面端单屏界面,并同时明确移动端重排意图;清楚呈现当前玩法所需的分数/资源/生命/局内状态 HUD、主要可玩区域、玩家与目标/收集物/危险物、开始和主要操作、失败状态与重新开始、键盘和触控提示。使用正视角、清晰分区和可读占位文字,使前端开发可直接据此拆分 HTML/CSS。采用项目已经确定的原创命名、角色轮廓、配色、场景材质和界面视觉语言。\n\n当前项目 UI 需求:{}",
"scene_generation": "为 Web 小游戏生成一张可直接作为运行画面底图的原创 16:9 场景背景。严格从下方用户需求提炼自己的游戏主题、地点、季节、材质和氛围;画面要为真实可玩区域留出足够清楚的中部空间,并有前景、中景、远景层次。不得画玩家角色、道具、棋子、障碍、HUD、操作按钮、文字、Logo、完整游戏截图、海报或素材图集;这些元素会从独立透明核心图集中绘制。不得自行假设为塔防或加入玩法合同中不存在的实体;必须原创,不得复刻现有游戏场景、贴图、标志性布局或受保护视觉语言。\n\n用户需求:{}", "scene_generation": "为 Web 小游戏生成一张可直接作为运行画面底图的原创 16:9 场景背景。严格从下方用户需求提炼自己的游戏主题、地点、季节、材质和氛围;画面要为真实可玩区域留出足够清楚的中部空间,并有前景、中景、远景层次。不得画玩家角色、道具、棋子、障碍、HUD、操作按钮、文字、Logo、完整游戏截图、海报或素材图集;这些元素会从独立透明核心图集中绘制。不得自行假设为塔防或加入玩法合同中不存在的实体;必须原创,不得复刻现有游戏场景、贴图、标志性布局或受保护视觉语言。\n\n用户需求:{}",
"default_art_brief": "需要一张可直接用于 Web 小游戏首版原型的核心美术素材。",
"spritesheet_generation": "为 Web 小游戏首版原型生成一张可切分的原创透明核心美术素材图集,适合放入本地 assets 并被游戏直接引用。严格从用户需求和美术 brief 提取当前项目自己的标题、玩法实体、目标物、收集物、障碍、状态与反馈,素材类别与数量以当前项目需求为准。所有元素沿用当前规范图的轮廓、配色、材质和光照,分区排布并留出清楚切分间距。角色轮廓、图标排布与配色采用项目原创设计。\n用户需求:{}\n美术资产 brief{}",
"ui_inspection_focus": "请只依据真实可见像素判断这是否是可供前端直接实现的完整游戏 UI 原型,不能依据文件名、生成提示词或图片内自述放行。纯场景图、概念图、地图、海报或只展示角色而没有可玩界面的插画必须判定失败;按当前项目的玩法识别界面结构与关键要素。逐项检查:informationHud=清楚显示当前玩法需要的分数、资源、生命、关卡或局内状态;gameplaySurface=主要可玩区域及空间规则清楚;objectiveEntities=玩家主体、目标/收集/危险物、谜题或文本选项、轨道等当前玩法等价关键要素可辨;primaryControls=当前玩法需要的开始、移动、暂停或操作控件清楚;failureRestartFlow=存在可识别的结束态表现意图或明确重开入口;responsiveLayout=能从可见布局、触控目标和可重排分组判断移动适配意图,实际双视口另由浏览器验证;implementationClarity=分区、层级和文字清楚到可指导 HTML/CSSoriginalTheme=原创主题且未复刻现有游戏角色、Logo、贴图或受保护视觉语言。请只返回一个 JSON object,字段必须严格为:{\"checks\":{\"informationHud\":true,\"gameplaySurface\":true,\"objectiveEntities\":true,\"primaryControls\":true,\"failureRestartFlow\":true,\"responsiveLayout\":true,\"implementationClarity\":true,\"originalTheme\":true},\"issues\":[\"未通过项及原因;全部通过时必须为空数组\"],\"summary\":\"500 字以内中文结论\"}。只有八项 checks 全为 true 且 issues 为空才通过。", "ui_inspection_focus": "请只依据真实可见像素判断这是否是可供前端直接实现的完整游戏 UI 原型,不能依据文件名、生成提示词或图片内自述放行。纯场景图、概念图、地图、海报或只展示角色而没有可玩界面的插画必须判定失败;按当前项目的玩法识别界面结构与关键要素。逐项检查:informationHud=清楚显示当前玩法需要的分数、资源、生命、关卡或局内状态;gameplaySurface=主要可玩区域及空间规则清楚;objectiveEntities=玩家主体、目标/收集/危险物、谜题或文本选项、轨道等当前玩法等价关键要素可辨;primaryControls=当前玩法需要的开始、移动、暂停或操作控件清楚;failureRestartFlow=存在可识别的结束态表现意图或明确重开入口;responsiveLayout=能从可见布局、触控目标和可重排分组判断移动适配意图,实际双视口另由浏览器验证;implementationClarity=分区、层级和文字清楚到可指导 HTML/CSSoriginalTheme=原创主题且未复刻现有游戏角色、Logo、贴图或受保护视觉语言。请只返回一个 JSON object,字段必须严格为:{\"checks\":{\"informationHud\":true,\"gameplaySurface\":true,\"objectiveEntities\":true,\"primaryControls\":true,\"failureRestartFlow\":true,\"responsiveLayout\":true,\"implementationClarity\":true,\"originalTheme\":true},\"issues\":[\"未通过项及原因;全部通过时必须为空数组\"],\"summary\":\"500 字以内中文结论\"}。只有八项 checks 全为 true 且 issues 为空才通过。",
"default_inspection_focus": "请检查布局、遮挡、裁切、视觉层级、素材一致性,以及桌面与移动视口是否可用。", "default_inspection_focus": "请检查布局、遮挡、裁切、视觉层级、素材一致性,以及桌面与移动视口是否可用。",
"custom_inspection_focus": "检查重点:{question}", "custom_inspection_focus": "检查重点:{question}",
@@ -36,7 +36,7 @@
"preview.start.description": "启动当前项目的 loopback HTTP 预览。", "preview.start.description": "启动当前项目的 loopback HTTP 预览。",
"preview.validate.description": "用真实浏览器验证桌面和移动预览并保存证据。", "preview.validate.description": "用真实浏览器验证桌面和移动预览并保存证据。",
"image.inspect.description": "让视觉模型检查一至两张项目内图片。", "image.inspect.description": "让视觉模型检查一至两张项目内图片。",
"canvas.asset_generate.description": "通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考。assetKind=icon-spritesheet 时 sliceMode 必填且没有默认值:需求要求等分网格、固定槽位或指定行列数时用 grid 并提供来自需求本身的 gridX/gridY;自由排布、数量不定或只要求一张图集时用 connected-components,可用 sliceCount 约束素材张数;其它 assetKind 不得携带 sliceMode/gridX/gridY。", "canvas.asset_generate.description": "通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考。assetKind=icon-spritesheet 的 prompt 去除首尾空白后须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交,超限拒绝,不截断、不拆条,客户端不追加生图指令。assetKind=icon-spritesheet 时 sliceMode 必填且没有默认值:需求要求等分网格、固定槽位或指定行列数时用 grid 并提供来自需求本身的 gridX/gridY;自由排布、数量不定或只要求一张图集时用 connected-components,可用 sliceCount 约束素材张数;其它 assetKind 不得携带 sliceMode/gridX/gridY。",
"ui.workflow.run.description": "先用 discover 从受控 game/ui-pages.json 或页面声明标记自动发现全部功能页面,再把已登记 ui-design 与每个页面的设计图桥接成独立 UI JSON State;可同时载入已登记图片、图标和项目字体,执行 Provider 结构识别、多树合并与分批组件绑定、回读阶段,并且只有所有页面已绑定且已应用到 game/ 后才允许 finalize。项目根目录由 Runtime 注入,模型不得传入宿主路径。", "ui.workflow.run.description": "先用 discover 从受控 game/ui-pages.json 或页面声明标记自动发现全部功能页面,再把已登记 ui-design 与每个页面的设计图桥接成独立 UI JSON State;可同时载入已登记图片、图标和项目字体,执行 Provider 结构识别、多树合并与分批组件绑定、回读阶段,并且只有所有页面已绑定且已应用到 game/ 后才允许 finalize。项目根目录由 Runtime 注入,模型不得传入宿主路径。",
"cocos.editor.execute.description": "在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。", "cocos.editor.execute.description": "在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;Runtime 自动绑定唯一匹配的 Creator 主进程,代码与结果都通过注入 payload 的本机 bridge 返回。",
"unity.editor.execute.description": "在当前 Unity 项目已打开的 Windows x64 Mono Editor 中执行 C#。仅提交 code,宿主绑定项目身份;结果待核对时禁止自动重发。", "unity.editor.execute.description": "在当前 Unity 项目已打开的 Windows x64 Mono Editor 中执行 C#。仅提交 code,宿主绑定项目身份;结果待核对时禁止自动重发。",
@@ -13,7 +13,7 @@ mod codex_app_server;
mod codex_cli; mod codex_cli;
mod codex_provider_proxy; mod codex_provider_proxy;
mod design_runtime; mod design_runtime;
mod design_tools; pub(crate) mod design_tools;
mod direct_codex_attachments; mod direct_codex_attachments;
mod direct_codex_audit; mod direct_codex_audit;
mod direct_codex_user_item; mod direct_codex_user_item;
@@ -1180,7 +1180,6 @@ fn direct_codex_thread_delta_event(
) -> DirectThreadEvent { ) -> DirectThreadEvent {
DirectThreadEvent::item_delta(item_id, kind, direct_thread_delta_text(root, delta)) DirectThreadEvent::item_delta(item_id, kind, direct_thread_delta_text(root, delta))
} }
/// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。 /// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。
/// ///
/// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外 /// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外
@@ -3358,8 +3357,22 @@ impl CodexAppServerConnection {
codex_app_server_text_prompt(&request) codex_app_server_text_prompt(&request)
.map_err(platform_llm::LlmError::InvalidRequest)? .map_err(platform_llm::LlmError::InvalidRequest)?
}; };
let mut input = let mut input = if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?; if let Some(item) = direct_user_item {
let canonical: DirectCodexUserItem = serde_json::from_value(item.clone())
.map_err(|error| platform_llm::LlmError::InvalidRequest(error.to_string()))?;
direct_codex_user_item_to_codex_turn_input(
&self.inner.workspace_path,
&canonical,
self.inner._skill_roots.as_deref().unwrap_or_default(),
)
.map_err(platform_llm::LlmError::InvalidRequest)?
} else {
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
}
} else {
codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?
};
if thread_created && self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject if thread_created && self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject
{ {
if let Some(client_turn_id) = direct_client_turn_id { if let Some(client_turn_id) = direct_client_turn_id {
@@ -7684,7 +7697,7 @@ done
&temp.path().join("host"), &temp.path().join("host"),
&project, &project,
"turn-0001", "turn-0001",
"fixture-request", &format!("{:x}", Sha256::digest("请创建菜单".as_bytes())),
false, false,
&super::super::direct_validation::DirectValidationConfig::default(), &super::super::direct_validation::DirectValidationConfig::default(),
) )
@@ -4,10 +4,12 @@ use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs; use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tauri::Manager; use tauri::Manager;
const DESIGN_WORKSPACE_ROOT: &str = "design_artifacts"; const DESIGN_WORKSPACE_ROOT: &str = "design_artifacts";
const DESIGN_REFERENCES_ROOT: &str = "references";
const SEARCH_HIT_LIMIT: usize = 200; const SEARCH_HIT_LIMIT: usize = 200;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@@ -547,6 +549,123 @@ pub(crate) fn read_design_workspace_file_at(root: &Path, path: &str) -> Result<S
fs::read_to_string(&target).map_err(|error| format!("读取失败:{error}")) fs::read_to_string(&target).map_err(|error| format!("读取失败:{error}"))
} }
#[tauri::command]
pub(crate) fn import_design_workspace_file(
app: tauri::AppHandle,
project_path: String,
file_name: String,
bytes: Vec<u8>,
) -> Result<String, String> {
let root = PathBuf::from(project_path.trim());
let relative_path = import_design_workspace_file_at(&root, &file_name, &bytes)?;
let _ = app.emit(
"design-agent-update",
serde_json::json!({
"projectPath": root.to_string_lossy(),
"clientTurnId": "",
"kind": "workspace",
"messageId": null,
"text": null,
"view": null,
}),
);
Ok(relative_path)
}
fn import_design_workspace_file_at(
root: &Path,
file_name: &str,
bytes: &[u8],
) -> Result<String, String> {
enforce_project_permission_policy(root, "conversation.write")?;
read_existing_manifest_for_project(root)?;
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"conversation.write",
)?;
if file_name.contains(['/', '\\']) {
return Err("附件文件名必须是单个文件名,不能包含目录".to_string());
}
let normalized_name =
normalize_relative_path(file_name).map_err(|error| format!("附件文件名无效:{error}"))?;
if normalized_name != file_name {
return Err("附件文件名无效".to_string());
}
let (_, references) = resolve_design_workspace_path(root, DESIGN_REFERENCES_ROOT)?;
crate::ensure_game_creator_private_directory_tree(&references, "策划参考附件目录")?;
crate::prepare_game_creator_private_path_for_read(&references, true, "策划参考附件目录")?;
let mut sequence = 1_u64;
loop {
let candidate_name = design_reference_file_name(&normalized_name, sequence);
let relative_path = format!("{DESIGN_REFERENCES_ROOT}/{candidate_name}");
let (_, target) = resolve_design_workspace_path(root, &relative_path)?;
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW).mode(0o600);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
options.custom_flags(crate::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut file = match options.open(&target) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
sequence = sequence
.checked_add(1)
.ok_or_else(|| "无法为同名附件分配新序号".to_string())?;
continue;
}
Err(error) => {
return Err(format!(
"创建策划参考附件失败:{}: {error}",
target.display()
));
}
};
if let Err(error) =
crate::harden_new_game_creator_private_path(&target, false, "策划参考附件")
{
drop(file);
let _ = fs::remove_file(&target);
return Err(error);
}
let write_result = file.write_all(bytes).and_then(|_| file.sync_all());
drop(file);
if let Err(error) = write_result {
let _ = fs::remove_file(&target);
return Err(format!(
"写入策划参考附件失败:{}: {error}",
target.display()
));
}
return Ok(relative_path);
}
}
fn design_reference_file_name(file_name: &str, sequence: u64) -> String {
if sequence == 1 {
return file_name.to_string();
}
let path = Path::new(file_name);
let stem = path
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or(file_name);
match path.extension().and_then(|value| value.to_str()) {
Some(extension) if !extension.is_empty() => {
format!("{stem} ({sequence}).{extension}")
}
_ => format!("{stem} ({sequence})"),
}
}
fn load_design_catalog(root: &Path) -> Result<Vec<DesignCatalogItem>, String> { fn load_design_catalog(root: &Path) -> Result<Vec<DesignCatalogItem>, String> {
let catalog_path = root.join("resources/catalog.json"); let catalog_path = root.join("resources/catalog.json");
let data: DesignCatalogFile = serde_json::from_str( let data: DesignCatalogFile = serde_json::from_str(
@@ -756,6 +875,13 @@ mod tests {
tempfile::tempdir().expect("tempdir") tempfile::tempdir().expect("tempdir")
} }
fn initialized_test_root() -> tempfile::TempDir {
let temp = test_root();
init_local_game_project_at(temp.path(), "design-import-test", "策划附件导入测试")
.expect("init project");
temp
}
fn pack_root() -> PathBuf { fn pack_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("design-agent") PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("design-agent")
} }
@@ -868,6 +994,138 @@ mod tests {
); );
} }
#[test]
fn imported_reference_is_visible_to_workspace_list_and_read() {
let temp = initialized_test_root();
let root = temp.path();
let manifest_before = read_existing_manifest_for_project(root).expect("read manifest");
let revision_before =
read_game_creator_agent_runtime_project_revision(root).expect("read revision");
let relative =
import_design_workspace_file_at(root, "玩法构想.txt", "横版解谜\n".as_bytes())
.expect("import text reference");
assert_eq!(relative, "references/玩法构想.txt");
assert!(list_design_workspace_files(root)
.expect("list workspace")
.iter()
.any(|entry| entry.path == relative && entry.kind == "file"));
assert_eq!(
read_design_workspace_file_at(root, &relative).expect("read imported reference"),
"横版解谜\n"
);
assert_eq!(
read_existing_manifest_for_project(root).expect("read manifest after import"),
manifest_before
);
assert_eq!(
read_game_creator_agent_runtime_project_revision(root)
.expect("read revision after import"),
revision_before
);
}
#[test]
fn import_keeps_existing_names_and_accepts_empty_and_binary_bytes() {
let temp = initialized_test_root();
let root = temp.path();
let first = import_design_workspace_file_at(root, "brief.md", b"first")
.expect("import first reference");
let second = import_design_workspace_file_at(root, "brief.md", b"second")
.expect("import repeated reference");
let empty = import_design_workspace_file_at(root, "empty.bin", b"")
.expect("import empty reference");
let binary_bytes = [0_u8, 0xff, 0x10, 0x80];
let binary = import_design_workspace_file_at(root, "bytes.bin", &binary_bytes)
.expect("import binary reference");
assert_eq!(first, "references/brief.md");
assert_eq!(second, "references/brief (2).md");
assert_eq!(empty, "references/empty.bin");
assert_eq!(binary, "references/bytes.bin");
assert_eq!(
fs::read(root.join("design_artifacts").join(&first)).expect("read first"),
b"first"
);
assert_eq!(
fs::read(root.join("design_artifacts").join(&second)).expect("read second"),
b"second"
);
assert!(fs::read(root.join("design_artifacts").join(&empty))
.expect("read empty")
.is_empty());
assert_eq!(
fs::read(root.join("design_artifacts").join(&binary)).expect("read binary"),
binary_bytes
);
}
#[test]
fn import_rejects_unsafe_names_and_denied_project_permission() {
let temp = initialized_test_root();
let root = temp.path();
for file_name in [
"../outside.txt",
"nested/file.txt",
r"nested\file.txt",
"C:stream",
] {
let error = import_design_workspace_file_at(root, file_name, b"blocked")
.expect_err("reject unsafe file name");
assert!(error.contains("文件名"), "unexpected error: {error}");
}
assert!(!root.join("outside.txt").exists());
let mut policy = ProjectPermissionPolicy::default();
policy
.denied_commands
.push("conversation.write".to_string());
write_project_permission_policy_at(root, policy).expect("deny conversation write");
let error = import_design_workspace_file_at(root, "denied.txt", b"blocked")
.expect_err("respect project permission policy");
assert!(error.contains("conversation.write"));
assert!(!root.join("design_artifacts/references/denied.txt").exists());
}
#[cfg(unix)]
#[test]
fn import_rejects_linked_references_directory() {
use std::os::unix::fs::symlink;
let temp = initialized_test_root();
let root = temp.path();
let outside = tempfile::tempdir().expect("outside tempdir");
fs::create_dir_all(root.join("design_artifacts")).expect("create workspace");
symlink(outside.path(), root.join("design_artifacts/references"))
.expect("link references directory");
let error = import_design_workspace_file_at(root, "escape.txt", b"blocked")
.expect_err("reject linked references directory");
assert!(error.contains("符号链接") || error.contains("reparse point"));
assert!(!outside.path().join("escape.txt").exists());
}
#[cfg(windows)]
#[test]
fn import_rejects_windows_linked_references_directory_when_supported() {
use std::os::windows::fs::symlink_dir;
let temp = initialized_test_root();
let root = temp.path();
let outside = tempfile::tempdir().expect("outside tempdir");
fs::create_dir_all(root.join("design_artifacts")).expect("create workspace");
if symlink_dir(outside.path(), root.join("design_artifacts/references")).is_err() {
return;
}
let error = import_design_workspace_file_at(root, "escape.txt", b"blocked")
.expect_err("reject linked references directory");
assert!(error.contains("符号链接") || error.contains("reparse point"));
assert!(!outside.path().join("escape.txt").exists());
}
#[test] #[test]
fn phase_context_injects_current_skill_only() { fn phase_context_injects_current_skill_only() {
let resources = DesignResources::new(pack_root()).expect("pack"); let resources = DesignResources::new(pack_root()).expect("pack");
@@ -2,9 +2,9 @@
//! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。 //! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。
pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8; pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8;
const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160;
const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96;
const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512;
const HOME_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.homeHeader"); const HOME_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.homeHeader");
const PROJECT_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.projectHeader"); const PROJECT_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.projectHeader");
@@ -5,11 +5,11 @@ mod validation;
mod wire; mod wire;
pub(crate) use model::{ pub(crate) use model::{
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem, DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem,
DirectCodexUserRole, DirectCodexUserRuntimeRegionPart, DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
}; };
pub(crate) use validation::validate_direct_codex_user_item; pub(crate) use validation::validate_direct_codex_user_item;
pub(crate) use wire::{ pub(crate) use wire::{
direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item, direct_codex_user_item_to_codex_turn_input, direct_codex_user_item_to_prompt,
direct_codex_user_item_to_wire_input, direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input,
}; };
@@ -4,7 +4,7 @@ use ts_rs::TS;
/// DirectProject 本轮 user input 的唯一结构化入口。 /// DirectProject 本轮 user input 的唯一结构化入口。
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(tag = "type", deny_unknown_fields)] #[serde(tag = "type", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectCodexUserItem { pub(crate) enum DirectCodexUserItem {
#[serde(rename = "message")] #[serde(rename = "message")]
Message(DirectCodexUserMessageItem), Message(DirectCodexUserMessageItem),
@@ -12,7 +12,7 @@ pub(crate) enum DirectCodexUserItem {
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectCodexUserMessageItem { pub(crate) struct DirectCodexUserMessageItem {
pub(crate) role: DirectCodexUserRole, pub(crate) role: DirectCodexUserRole,
pub(crate) content: Vec<DirectCodexUserContentPart>, pub(crate) content: Vec<DirectCodexUserContentPart>,
@@ -21,26 +21,43 @@ pub(crate) struct DirectCodexUserMessageItem {
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectCodexUserRole { pub(crate) enum DirectCodexUserRole {
User, User,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectCodexUserContentPart { pub(crate) enum DirectCodexUserContentPart {
#[serde(rename = "input_text")] #[serde(rename = "input_text")]
InputText { text: String }, InputText { text: String },
#[serde(rename = "agc_resource_reference")] #[serde(rename = "agc_resource_reference")]
AgcResourceReference { resource_id: String }, AgcResourceReference { resource_id: String },
#[serde(rename = "agc_skill_reference")]
AgcSkillReference { name: String },
#[serde(rename = "agc_runtime_region_reference")] #[serde(rename = "agc_runtime_region_reference")]
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart), AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
/// Uploaded project attachment kept inline in canonical content.
#[serde(rename = "agc_attachment_reference")]
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectCodexUserAttachmentReferencePart {
pub(crate) name: String,
pub(crate) media_type: String,
#[ts(type = "number")]
pub(crate) size: u64,
pub(crate) local_path: String,
pub(crate) status: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectCodexUserRuntimeRegionPart { pub(crate) struct DirectCodexUserRuntimeRegionPart {
pub(crate) label: String, pub(crate) label: String,
#[serde(default)] #[serde(default)]
@@ -4,15 +4,20 @@ use super::model::{
}; };
use crate::agent::{ use crate::agent::{
read_manifest_for_project, sanitize_attachment_local_path, GameCreationAppManifest, read_manifest_for_project, sanitize_attachment_local_path, GameCreationAppManifest,
MAX_DIRECT_CODEX_ATTACHMENTS, MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS,
MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS,
}; };
use std::path::Path; use std::path::Path;
pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32; pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32;
// Skill 引用也是非文本 part,但没有走 reference_count:它每一条都会触发一次
// `root/<name>/SKILL.md` 文件探测并往 turn input 里加一项,所以单独设上限。
pub(crate) const MAX_DIRECT_CODEX_SKILL_REFERENCES: usize = 32;
pub(crate) fn validate_direct_codex_user_item( pub(crate) fn validate_direct_codex_user_item(
root: &Path, root: &Path,
item: &DirectCodexUserItem, item: &DirectCodexUserItem,
) -> Result<(), String> { ) -> Result<GameCreationAppManifest, String> {
let DirectCodexUserItem::Message(message) = item; let DirectCodexUserItem::Message(message) = item;
if !matches!(message.role, DirectCodexUserRole::User) { if !matches!(message.role, DirectCodexUserRole::User) {
return Err("DirectProject 只接受 user message item".to_string()); return Err("DirectProject 只接受 user message item".to_string());
@@ -20,38 +25,98 @@ pub(crate) fn validate_direct_codex_user_item(
if message.id.trim().is_empty() { if message.id.trim().is_empty() {
return Err("DirectProject user item 缺少稳定 id".to_string()); return Err("DirectProject user item 缺少稳定 id".to_string());
} }
if message.content.is_empty() { // 有效输入只判一整条 content:单个纯空白 `input_text` 是合法 part —— 编辑器里的段落
// 分隔、软换行与 chip 后的分隔空格就是这样落进 canonical content 的,前端不为它过滤。
if !content_has_meaningful_input(&message.content) {
return Err("聊天内容不能为空".to_string()); return Err("聊天内容不能为空".to_string());
} }
let manifest = read_manifest_for_project(root)?; let manifest = read_manifest_for_project(root)?;
let mut reference_count = 0usize; let mut reference_count = 0usize;
let mut has_effective_content = false; let mut attachment_count = 0usize;
let mut skill_count = 0usize;
for part in &message.content { for part in &message.content {
match part { match part {
DirectCodexUserContentPart::InputText { text } => { DirectCodexUserContentPart::InputText { .. } => {}
if !text.trim().is_empty() {
has_effective_content = true;
}
}
DirectCodexUserContentPart::AgcResourceReference { resource_id } => { DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
reference_count = reference_count.saturating_add(1); reference_count = reference_count.saturating_add(1);
validate_resource_id_and_manifest(&manifest, resource_id)?; validate_resource_id_and_manifest(&manifest, resource_id)?;
has_effective_content = true; }
DirectCodexUserContentPart::AgcSkillReference { name } => {
skill_count = skill_count.saturating_add(1);
if skill_count > MAX_DIRECT_CODEX_SKILL_REFERENCES {
return Err(format!(
"一次最多引用 {MAX_DIRECT_CODEX_SKILL_REFERENCES} 个 Skill"
));
}
let name = name.trim();
if name.is_empty()
|| name.chars().count() > 120
|| matches!(name, "." | "..")
|| name.chars().any(|character| {
character.is_control()
|| character.is_whitespace()
|| matches!(character, '/' | '\\' | ':' | '$')
})
{
return Err("引用的 Skill 名称无效,请移除后重新选择".to_string());
}
} }
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => { DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
reference_count = reference_count.saturating_add(1); reference_count = reference_count.saturating_add(1);
validate_runtime_region_reference(&manifest, reference)?; validate_runtime_region_reference(&manifest, reference)?;
has_effective_content = true; }
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
attachment_count = attachment_count.saturating_add(1);
if attachment_count > MAX_DIRECT_CODEX_ATTACHMENTS {
return Err(format!(
"一次最多携带 {MAX_DIRECT_CODEX_ATTACHMENTS} 个附件"
));
}
if reference.name.trim().is_empty() {
return Err("附件缺少文件名".to_string());
}
let name = reference.name.trim();
if name.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS
|| name.chars().any(char::is_control)
{
return Err("附件文件名无效或过长".to_string());
}
let media_type = reference.media_type.trim();
if media_type.is_empty()
|| media_type.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS
|| media_type.chars().any(|character| {
!(character.is_ascii_alphanumeric()
|| matches!(character, '/' | '+' | '-' | '.' | '_'))
})
{
return Err("附件媒体类型无效或过长".to_string());
}
let status = reference.status.trim();
if status == "imported" && reference.local_path.trim().is_empty() {
return Err("已导入附件缺少项目路径".to_string());
}
if !reference.local_path.trim().is_empty() {
sanitize_attachment_local_path(&reference.local_path)
.ok_or_else(|| "附件项目路径无效".to_string())?;
}
if !matches!(status, "imported" | "failed") {
return Err("附件状态无效".to_string());
}
} }
} }
} }
if reference_count > MAX_DIRECT_CODEX_REFERENCES { if reference_count > MAX_DIRECT_CODEX_REFERENCES {
return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材")); return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材"));
} }
if !has_effective_content { Ok(manifest)
return Err("聊天内容不能为空".to_string()); }
}
Ok(()) /// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。
pub(crate) fn content_has_meaningful_input(content: &[DirectCodexUserContentPart]) -> bool {
content.iter().any(|part| match part {
DirectCodexUserContentPart::InputText { text } => !text.trim().is_empty(),
_ => true,
})
} }
pub(crate) fn validate_resource_id_and_manifest( pub(crate) fn validate_resource_id_and_manifest(
@@ -92,3 +157,173 @@ fn validate_runtime_region_reference(
} }
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::{
content_has_meaningful_input, validate_direct_codex_user_item,
MAX_DIRECT_CODEX_SKILL_REFERENCES,
};
use crate::agent::direct_codex_user_item::model::DirectCodexUserContentPart;
use serde_json::json;
fn input_text(text: &str) -> DirectCodexUserContentPart {
DirectCodexUserContentPart::InputText {
text: text.to_string(),
}
}
#[test]
fn only_all_blank_content_counts_as_empty_input() {
// 空数组与「整条只有空白」是同一种空输入。
assert!(!content_has_meaningful_input(&[]));
assert!(!content_has_meaningful_input(&[input_text(" \n ")]));
assert!(!content_has_meaningful_input(&[
input_text("\n"),
input_text(" "),
]));
}
#[test]
fn whitespace_parts_are_valid_next_to_meaningful_input() {
// 段落分隔 / 软换行 / chip 后的分隔空格都是合法的单个 part。
assert!(content_has_meaningful_input(&[
input_text("\n"),
input_text(""),
]));
assert!(content_has_meaningful_input(&[
input_text(""),
input_text("\n\n"),
]));
}
#[test]
fn non_text_parts_always_count_as_input() {
assert!(content_has_meaningful_input(&[
DirectCodexUserContentPart::AgcResourceReference {
resource_id: "asset-hero".to_string(),
},
]));
}
#[test]
fn inline_attachment_count_is_bounded_independently() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "validation-test", "校验测试")
.expect("init project");
let content = (0..=crate::agent::MAX_DIRECT_CODEX_ATTACHMENTS)
.map(|index| {
json!({
"type": "agc_attachment_reference",
"name": format!("attachment-{index}.txt"),
"mediaType": "text/plain",
"size": 1,
"localPath": "",
"status": "failed"
})
})
.collect::<Vec<_>>();
let item = serde_json::from_value(json!({
"type": "message",
"role": "user",
"content": content,
"id": "turn-1:user"
}))
.expect("deserialize user item");
let error = validate_direct_codex_user_item(root.path(), &item)
.expect_err("too many inline attachments must be rejected");
assert!(error.contains("最多携带"), "{error}");
}
#[test]
fn imported_attachment_requires_a_project_path() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "validation-test", "校验测试")
.expect("init project");
let item = serde_json::from_value(json!({
"type": "message",
"role": "user",
"content": [{
"type": "agc_attachment_reference",
"name": "attachment.txt",
"mediaType": "text/plain",
"size": 1,
"localPath": "",
"status": "imported"
}],
"id": "turn-1:user"
}))
.expect("deserialize user item");
let error = validate_direct_codex_user_item(root.path(), &item)
.expect_err("imported attachment without a project path must fail");
assert!(error.contains("缺少项目路径"), "{error}");
}
#[test]
fn inline_skill_reference_count_is_bounded_independently() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "validation-test", "校验测试")
.expect("init project");
let content = (0..=MAX_DIRECT_CODEX_SKILL_REFERENCES)
.map(|index| json!({ "type": "agc_skill_reference", "name": format!("skill-{index}") }))
.collect::<Vec<_>>();
let item = serde_json::from_value(json!({
"type": "message",
"role": "user",
"content": content,
"id": "turn-1:user"
}))
.expect("deserialize user item");
let error = validate_direct_codex_user_item(root.path(), &item)
.expect_err("too many skill references must be rejected");
assert!(error.contains("最多引用"), "{error}");
}
#[test]
fn attachment_name_and_media_type_are_bounded_and_well_formed() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "validation-test", "校验测试")
.expect("init project");
let long_name = "a".repeat(crate::agent::MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS + 1);
let cases = [
(
json!({
"name": "bad\nname.txt",
"mediaType": "text/plain"
}),
"文件名",
),
(
json!({
"name": "ok.txt",
"mediaType": "text/plain\nsecret"
}),
"媒体类型",
),
(
json!({
"name": long_name,
"mediaType": "text/plain"
}),
"文件名",
),
];
for (metadata, expected) in cases {
let mut value = metadata;
value["type"] = json!("agc_attachment_reference");
value["size"] = json!(1);
value["localPath"] = json!("");
value["status"] = json!("failed");
let item = serde_json::from_value(json!({
"type": "message",
"role": "user",
"content": [value],
"id": "turn-1:user"
}))
.expect("deserialize user item");
let error = validate_direct_codex_user_item(root.path(), &item)
.expect_err("invalid attachment metadata must fail");
assert!(error.contains(expected), "{error}");
}
}
}
@@ -1,6 +1,12 @@
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem}; use super::model::{
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem,
DirectCodexUserMessageItem, DirectCodexUserRuntimeRegionPart,
};
use super::validation::validate_direct_codex_user_item; use super::validation::validate_direct_codex_user_item;
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path}; use crate::agent::{
read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type,
sanitize_attachment_name, GameCreationAppManifest,
};
use crate::ui_editor::persistence::{ use crate::ui_editor::persistence::{
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND, generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE, UI_DESIGN_DOC_MEDIA_TYPE,
@@ -55,54 +61,90 @@ fn direct_codex_user_item_to_response_content(
.collect() .collect()
} }
fn resource_reference_summary(
manifest: &GameCreationAppManifest,
resource_id: &str,
) -> Result<String, String> {
let resource_id = resource_id.trim();
let asset = manifest
.assets
.iter()
.find(|asset| asset.id == resource_id)
.ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?;
let path = sanitize_attachment_local_path(&asset.local_path)
.ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?;
Ok(format!(
"[素材引用 resourceId={resource_id};项目路径={path}]"
))
}
fn runtime_region_summary(reference: &DirectCodexUserRuntimeRegionPart) -> String {
let resources = reference
.resource_ids
.iter()
.map(|id| id.trim())
.collect::<Vec<_>>()
.join(",");
let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim());
if let Some(run_id) = reference.run_id.as_deref() {
summary.push_str(&format!("运行标识={} ", run_id.trim()));
}
if let Some(role) = reference.element_role.as_deref() {
summary.push_str(&format!("角色={} ", role.trim()));
}
if let Some(text) = reference.text.as_deref() {
summary.push_str(&format!("文本={} ", text.trim()));
}
if !resources.is_empty() {
summary.push_str(&format!("关联素材={resources}"));
}
summary.push(']');
summary
}
/// 附件引用的安全摘要。
///
/// turn 输入与 history/prompt 投影共用这一份清洗:文件名取 basename 并去控制字符、
/// media type 与项目路径同样过白名单,避免两条路径对同一个引用给出不同摘要。
fn attachment_reference_summary(reference: &DirectCodexUserAttachmentReferencePart) -> String {
let name = sanitize_attachment_name(&reference.name);
let media_type = sanitize_attachment_media_type(&reference.media_type);
let mut summary = format!(
"[附件:名称={name};类型={media_type};大小={} 字节",
reference.size
);
if let Some(local_path) = sanitize_attachment_local_path(&reference.local_path) {
summary.push_str(&format!(";项目路径={local_path}"));
}
summary.push_str(&format!(";状态={}", reference.status.trim()));
summary.push(']');
summary
}
/// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。 /// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。
/// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。 /// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。
pub(crate) fn direct_codex_user_item_to_wire_input( pub(crate) fn direct_codex_user_item_to_wire_input(
root: &Path, root: &Path,
item: &DirectCodexUserItem, item: &DirectCodexUserItem,
) -> Result<Value, String> { ) -> Result<Value, String> {
validate_direct_codex_user_item(root, item)?; // validate 已经读过清单并返回它,不要再读一次(seed task 变更也会被重复触发)。
let manifest = read_manifest_for_project(root)?; let manifest = validate_direct_codex_user_item(root, item)?;
let DirectCodexUserItem::Message(message) = item; let DirectCodexUserItem::Message(message) = item;
let mut input = Vec::with_capacity(message.content.len()); let mut input = Vec::with_capacity(message.content.len());
for part in &message.content { for part in &message.content {
let text = match part { let text = match part {
DirectCodexUserContentPart::InputText { text } => text.clone(), DirectCodexUserContentPart::InputText { text } => text.clone(),
DirectCodexUserContentPart::AgcResourceReference { resource_id } => { DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
let asset = manifest resource_reference_summary(&manifest, resource_id)?
.assets }
.iter() DirectCodexUserContentPart::AgcSkillReference { name } => {
.find(|asset| asset.id == resource_id.trim()) format!("${}", name.trim())
.ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?;
let path = sanitize_attachment_local_path(&asset.local_path)
.ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?;
format!(
"[素材引用 resourceId={};项目路径={path}]",
resource_id.trim()
)
} }
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => { DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
let resources = reference runtime_region_summary(reference)
.resource_ids }
.iter() DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
.map(|id| id.trim()) attachment_reference_summary(reference)
.collect::<Vec<_>>()
.join(",");
let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim());
if let Some(run_id) = reference.run_id.as_deref() {
summary.push_str(&format!("运行标识={} ", run_id.trim()));
}
if let Some(role) = reference.element_role.as_deref() {
summary.push_str(&format!("角色={} ", role.trim()));
}
if let Some(text) = reference.text.as_deref() {
summary.push_str(&format!("文本={} ", text.trim()));
}
if !resources.is_empty() {
summary.push_str(&format!("关联素材={resources}"));
}
summary.push(']');
summary
} }
}; };
input.push(serde_json::json!({ "type": "text", "text": text })); input.push(serde_json::json!({ "type": "text", "text": text }));
@@ -110,6 +152,55 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
Ok(Value::Array(input)) Ok(Value::Array(input))
} }
pub(crate) fn direct_codex_user_item_to_codex_turn_input(
root: &Path,
item: &DirectCodexUserItem,
skill_roots: &[std::path::PathBuf],
) -> Result<Value, String> {
let manifest = validate_direct_codex_user_item(root, item)?;
let DirectCodexUserItem::Message(message) = item;
let mut input = Vec::with_capacity(message.content.len());
for part in &message.content {
match part {
DirectCodexUserContentPart::InputText { text } => {
input.push(serde_json::json!({ "type": "text", "text": text }));
}
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
input.push(serde_json::json!({
"type": "text",
"text": resource_reference_summary(&manifest, resource_id)?,
}));
}
DirectCodexUserContentPart::AgcSkillReference { name } => {
let name = name.trim();
let path = skill_roots
.iter()
.map(|root| root.join(name).join("SKILL.md"))
.find(|path| path.is_file())
.ok_or_else(|| "引用的 Skill 当前不可用,请重新选择".to_string())?;
input.push(serde_json::json!({
"type": "skill",
"name": name,
"path": path,
}));
}
DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => {
input.push(serde_json::json!({
"type": "text",
"text": runtime_region_summary(reference),
}));
}
DirectCodexUserContentPart::AgcAttachmentReference(reference) => {
input.push(serde_json::json!({
"type": "text",
"text": attachment_reference_summary(reference),
}));
}
}
}
Ok(Value::Array(input))
}
pub(crate) fn direct_codex_user_item_to_prompt( pub(crate) fn direct_codex_user_item_to_prompt(
root: &Path, root: &Path,
item: &DirectCodexUserItem, item: &DirectCodexUserItem,
@@ -118,17 +209,10 @@ pub(crate) fn direct_codex_user_item_to_prompt(
let DirectCodexUserItem::Message(message) = item; let DirectCodexUserItem::Message(message) = item;
let mut prompt = wire let mut prompt = wire
.as_array() .as_array()
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string()) .ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())?
.and_then(|parts| { .iter()
parts .filter_map(|part| part.get("text").and_then(Value::as_str))
.iter() .collect::<String>();
.map(|part| {
part.get("text")
.and_then(Value::as_str)
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
})
.collect::<Result<String, String>>()
})?;
if let Some(code_context) = render_ui_design_code_context(root, message)? { if let Some(code_context) = render_ui_design_code_context(root, message)? {
prompt.push('\n'); prompt.push('\n');
prompt.push_str(&code_context); prompt.push_str(&code_context);
@@ -199,8 +283,9 @@ fn render_ui_design_code_context(
mod tests { mod tests {
use super::{ use super::{
direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item, direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item,
validate_direct_codex_user_item, direct_codex_user_item_to_wire_input, validate_direct_codex_user_item,
}; };
use crate::agent::direct_codex_user_item::model::DirectCodexUserItem;
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE; use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
use serde_json::json; use serde_json::json;
use shared_contracts::game_creation_app::{ use shared_contracts::game_creation_app::{
@@ -283,6 +368,83 @@ mod tests {
}) })
} }
#[test]
fn ui_design_doc_reference_appends_generated_code_context() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(
prompt.contains(&format!(
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
)),
"{prompt}"
);
assert!(
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
"{prompt}"
);
}
#[test]
fn history_projection_never_writes_generated_ui_design_code() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item = user_item_with_resource_reference(&asset_id);
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
let generated_root =
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
let generated_files = std::fs::read_dir(generated_root)
.expect("read ui directory")
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("generated-")
})
.count();
assert_eq!(
generated_files, 0,
"历史回读只做纯投影,不得生成 UI 设计代码"
);
}
#[test]
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
let (project, asset_id) = ui_design_doc_fixture(false);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
}
#[test]
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
let project = prompt_context_project();
let asset_id = register_fixture_asset(
project.path(),
"assets/hero.png",
GameCreationAppAssetKind::Character,
"image/png",
);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(
!prompt.contains("请先阅读生成的带有文档的代码片段"),
"{prompt}"
);
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
}
#[test] #[test]
fn standard_response_item_passes_through_without_agc_private_parts() { fn standard_response_item_passes_through_without_agc_private_parts() {
let item = json!({ let item = json!({
@@ -297,6 +459,23 @@ mod tests {
); );
} }
#[test]
fn response_item_projection_uses_input_text_not_turn_input_text() {
let root = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
.expect("init project");
let item = json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [{"type": "input_text", "text": "你好"}]
});
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
.expect("user response item should project");
assert_eq!(projected["content"][0]["type"], "input_text");
assert_ne!(projected["content"][0]["type"], "text");
}
#[test] #[test]
fn text_projection_preserves_empty_parts_line_breaks_and_trailing_whitespace() { fn text_projection_preserves_empty_parts_line_breaks_and_trailing_whitespace() {
let root = tempfile::tempdir().expect("temp project"); let root = tempfile::tempdir().expect("temp project");
@@ -445,79 +624,97 @@ mod tests {
} }
#[test] #[test]
fn ui_design_doc_reference_appends_generated_code_context() { fn attachment_parts_remain_in_canonical_order_when_projected() {
let (project, asset_id) = ui_design_doc_fixture(true); let root = tempfile::tempdir().expect("temp project");
let item: super::DirectCodexUserItem = crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
serde_json::from_value(user_item_with_resource_reference(&asset_id)) .expect("init project");
.expect("canonical user item"); let item = json!({
let prompt = "type": "message",
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); "role": "user",
assert!( "id": "turn-1:user",
prompt.contains(&format!( "content": [
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]" {"type": "input_text", "text": "先看"},
)), {"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"}
"{prompt}" ]
); });
assert!( let projected = direct_codex_user_item_to_response_item(root.path(), &item)
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"), .expect("user response item should project");
"{prompt}" let content = projected["content"].as_array().expect("content array");
); assert_eq!(content.len(), 2);
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
assert!(content[1]["text"].as_str().unwrap().contains("notes.txt"));
} }
#[test] #[test]
fn history_projection_never_writes_generated_ui_design_code() { fn attachment_metadata_is_sanitized_before_prompt_projection() {
let (project, asset_id) = ui_design_doc_fixture(true); let root = tempfile::tempdir().expect("temp project");
let item = user_item_with_resource_reference(&asset_id); crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection"); .expect("init project");
let generated_root = let item = json!({
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory"); "type": "message",
let generated_files = std::fs::read_dir(generated_root) "role": "user",
.expect("read ui directory") "id": "turn-1:user",
.filter_map(|entry| entry.ok()) "content": [{
.filter(|entry| { "type": "agc_attachment_reference",
entry "name": "C:\\tmp\\notes.md",
.file_name() "mediaType": "text/plain",
.to_string_lossy() "size": 4,
.starts_with("generated-") "localPath": "assets\\.\\notes.txt",
}) "status": "imported"
.count(); }]
assert_eq!( });
generated_files, 0, let wire = super::direct_codex_user_item_to_wire_input(
"历史回读只做纯投影,不得生成 UI 设计代码" root.path(),
); &serde_json::from_value(item).expect("deserialize user item"),
)
.expect("attachment metadata should project");
let text = wire[0]["text"].as_str().expect("wire text");
assert!(text.contains("名称=notes.md"), "{text}");
assert!(text.contains("类型=text/plain"), "{text}");
assert!(text.contains("项目路径=assets/notes.txt"), "{text}");
} }
#[test] #[test]
fn ui_design_generation_failure_keeps_reference_and_reports_error() { fn whitespace_only_text_parts_survive_validation() {
let (project, asset_id) = ui_design_doc_fixture(false); let root = tempfile::tempdir().expect("temp project");
let item: super::DirectCodexUserItem = crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
serde_json::from_value(user_item_with_resource_reference(&asset_id)) .expect("init project");
.expect("canonical user item"); let item: DirectCodexUserItem = serde_json::from_value(json!({
let prompt = "type": "message",
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); "role": "user",
assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); "id": "turn-1:user",
assert!(prompt.contains("生成代码遇到错误"), "{prompt}"); "content": [
{"type": "input_text", "text": "先看"},
{"type": "input_text", "text": "\n"},
{"type": "input_text", "text": " "}
]
}))
.expect("deserialize user item");
let wire = direct_codex_user_item_to_wire_input(root.path(), &item)
.expect("whitespace-only part next to real text must pass");
let parts = wire.as_array().expect("wire input array");
assert_eq!(parts.len(), 3);
assert_eq!(parts[1]["text"].as_str(), Some("\n"));
assert_eq!(parts[2]["text"].as_str(), Some(" "));
} }
#[test] #[test]
fn other_asset_kind_reference_does_not_generate_ui_design_code() { fn all_blank_content_is_rejected() {
let project = prompt_context_project(); let root = tempfile::tempdir().expect("temp project");
let asset_id = register_fixture_asset( crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
project.path(), .expect("init project");
"assets/hero.png", let item: DirectCodexUserItem = serde_json::from_value(json!({
GameCreationAppAssetKind::Character, "type": "message",
"image/png", "role": "user",
); "id": "turn-1:user",
let item: super::DirectCodexUserItem = "content": [
serde_json::from_value(user_item_with_resource_reference(&asset_id)) {"type": "input_text", "text": "\n"},
.expect("canonical user item"); {"type": "input_text", "text": " "}
let prompt = ]
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); }))
assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); .expect("deserialize user item");
assert!( let error = direct_codex_user_item_to_wire_input(root.path(), &item)
!prompt.contains("请先阅读生成的带有文档的代码片段"), .expect_err("all-blank content must fail closed");
"{prompt}" assert!(error.contains("不能为空"), "{error}");
);
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
} }
} }
@@ -30,11 +30,9 @@ pub(crate) fn normalize_direct_client_turn_id(
#[tauri::command] #[tauri::command]
pub(crate) async fn chat_with_game_creator_direct_codex( pub(crate) async fn chat_with_game_creator_direct_codex(
project_path: String, project_path: String,
prompt: String, user_item: DirectCodexUserItem,
mut user_item: DirectCodexUserItem,
creation_type: Option<String>, creation_type: Option<String>,
client_turn_id: Option<String>, client_turn_id: Option<String>,
attachments: Option<Vec<DirectCodexTurnAttachment>>,
) -> Result<String, String> { ) -> Result<String, String> {
let root = Path::new(project_path.trim()); let root = Path::new(project_path.trim());
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?; let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
@@ -43,74 +41,33 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500) redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
})?; })?;
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone()); let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
let mut audit = DirectCodexTurnAudit::start( validate_direct_codex_user_item(root, &user_item)?;
root, let user_prompt = direct_codex_user_item_to_prompt(root, &user_item)?;
&turn_id,
&prompt,
attachments.as_deref().unwrap_or_default(),
);
let attachments = attachments.unwrap_or_default();
if !attachments.is_empty() {
let attachment_context = match render_direct_codex_user_prompt("", &attachments) {
Ok(context) => context,
Err(error) => {
audit.finish(false);
audit.flush().await;
return Err(error);
}
};
let DirectCodexUserItem::Message(message) = &mut user_item;
message.content.push(DirectCodexUserContentPart::InputText {
text: attachment_context,
});
}
if let Err(error) = validate_direct_codex_user_item(root, &user_item) {
audit.finish(false);
audit.flush().await;
return Err(error);
}
let user_prompt = match direct_codex_user_item_to_prompt(root, &user_item) {
Ok(prompt) => prompt,
Err(error) => {
audit.finish(false);
audit.flush().await;
return Err(error);
}
};
if user_prompt.trim().is_empty() { if user_prompt.trim().is_empty() {
audit.finish(false);
audit.flush().await;
return Err("聊天内容不能为空".to_string()); return Err("聊天内容不能为空".to_string());
} }
let canonical_user_item = let canonical_user_item =
// 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。 // 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。
match crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref()).await { match crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref())
.await
{
Ok(_) => Some(serde_json::to_value(user_item).map_err(|error| error.to_string())?), Ok(_) => Some(serde_json::to_value(user_item).map_err(|error| error.to_string())?),
Err(error) => { Err(error) => return Err(redact_agent_runtime_error(root, &error, 1800)),
audit.finish(false);
audit.flush().await;
return Err(redact_agent_runtime_error(root, &error, 1800));
}
}; };
let reply = match run_direct_game_creator_turn_at_with_creation_type_and_emitter( let reply = match run_direct_game_creator_turn_at_with_creation_type_and_emitter(
root, root,
&user_prompt, &user_prompt,
creation_type.as_deref(), creation_type.as_deref(),
Some(&turn_emitter), Some(&turn_emitter),
Some(&mut audit), // DirectProject 的完整回合权威已经落在 project.jsonl;不再创建平行审计日志。
None,
canonical_user_item, canonical_user_item,
) )
.await .await
{ {
Ok(reply) => reply, Ok(reply) => reply,
Err(error) => { Err(error) => return Err(error),
audit.finish(false);
audit.flush().await;
return Err(error);
}
}; };
audit.finish(true);
audit.flush().await;
turn_emitter.emit("completed", Some("none"), Some(reply.clone()), None); turn_emitter.emit("completed", Some("none"), Some(reply.clone()), None);
Ok(reply) Ok(reply)
} }
@@ -1,6 +1,6 @@
//! DirectProject 聊天事件的线上模型与投影。 //! DirectProject 聊天事件的线上模型与投影。
//! //!
//! 前端消费的类型由 ts-rs 导出到 `src/features/project-workspace/generated/` //! 前端消费的类型由 ts-rs 导出到 `src/view/project-development/chat/generated/`
//! 与 Rust 定义同源:加一个字段不会只改一边。 //! 与 Rust 定义同源:加一个字段不会只改一边。
//! //!
//! 本模块只做三件事:挑字段、脱敏、截断。工具卡片的 `kind`、标题、折叠摘要、可见性与 //! 本模块只做三件事:挑字段、脱敏、截断。工具卡片的 `kind`、标题、折叠摘要、可见性与
@@ -29,7 +29,7 @@ const DIRECT_THREAD_PATH_MAX_CHARS: usize = 300;
/// 一条文件变更。 /// 一条文件变更。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectThreadFileChange { pub(crate) struct DirectThreadFileChange {
pub(crate) path: String, pub(crate) path: String,
/// `add` | `update` | `delete` /// `add` | `update` | `delete`
@@ -45,7 +45,7 @@ pub(crate) struct DirectThreadFileChange {
/// 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。 /// 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(tag = "itemType", rename_all_fields = "camelCase", deny_unknown_fields)] #[serde(tag = "itemType", rename_all_fields = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectThreadItem { pub(crate) enum DirectThreadItem {
#[serde(rename = "message")] #[serde(rename = "message")]
Message { Message {
@@ -179,7 +179,7 @@ impl DirectThreadItem {
/// 增量正文属于哪类条目。 /// 增量正文属于哪类条目。
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectThreadDeltaKind { pub(crate) enum DirectThreadDeltaKind {
/// assistant 正文。 /// assistant 正文。
Message, Message,
@@ -190,7 +190,7 @@ pub(crate) enum DirectThreadDeltaKind {
/// 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。 /// 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectThreadRequestKind { pub(crate) enum DirectThreadRequestKind {
#[serde(rename = "approval.requested")] #[serde(rename = "approval.requested")]
ApprovalRequested, ApprovalRequested,
@@ -235,7 +235,7 @@ impl DirectThreadRequestKind {
/// (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。 /// (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) enum DirectThreadEvent { pub(crate) enum DirectThreadEvent {
#[serde(rename = "turn.started")] #[serde(rename = "turn.started")]
TurnStarted { TurnStarted {
@@ -398,7 +398,7 @@ impl DirectThreadEvent {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectThreadSubscriptionBootstrap { pub(crate) struct DirectThreadSubscriptionBootstrap {
pub(crate) subscription_id: String, pub(crate) subscription_id: String,
/// 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。 /// 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。
@@ -410,14 +410,14 @@ pub(crate) struct DirectThreadSubscriptionBootstrap {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectThreadConsumeResult { pub(crate) struct DirectThreadConsumeResult {
pub(crate) events: Vec<DirectThreadEvent>, pub(crate) events: Vec<DirectThreadEvent>,
} }
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectThreadHistorySlice { pub(crate) struct DirectThreadHistorySlice {
/// 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。 /// 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。
pub(crate) items: Vec<DirectThreadItem>, pub(crate) items: Vec<DirectThreadItem>,
@@ -3407,7 +3407,7 @@ async fn start_tool_bridge_for_source(
} }
#[cfg(test)] #[cfg(test)]
pub(in crate::agent) struct DirectExecutionTestFixture { pub(crate) struct DirectExecutionTestFixture {
execution: Option<super::direct_execution::ExecutionSessionGuard>, execution: Option<super::direct_execution::ExecutionSessionGuard>,
_host: tempfile::TempDir, _host: tempfile::TempDir,
} }
@@ -3426,7 +3426,7 @@ impl Drop for DirectExecutionTestFixture {
} }
#[cfg(test)] #[cfg(test)]
pub(in crate::agent) async fn direct_execution_fixture( pub(crate) async fn direct_execution_fixture(
root: &Path, root: &Path,
turn: &str, turn: &str,
) -> DirectExecutionTestFixture { ) -> DirectExecutionTestFixture {
@@ -72,7 +72,8 @@ pub(crate) use canvas_generation::{
platform_art_asset_art_spec, platform_art_asset_output_extension_matches, platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path, platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path,
project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call, project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS, validate_platform_art_icon_prompt, PlatformArtAssetGenerationOptions,
PLATFORM_ART_ASSET_GENERATION_KINDS,
}; };
#[allow(unused_imports)] #[allow(unused_imports)]
pub(crate) use draft_validation::{ pub(crate) use draft_validation::{
@@ -687,7 +687,7 @@ pub(in crate::agent) fn recover_persisted_visual_generation_options(
let snapshot = platform_art_generation_runtime_request_snapshot(&state)?; let snapshot = platform_art_generation_runtime_request_snapshot(&state)?;
// 只恢复经过身份校验的已有请求;显式改参和新请求仍走当前合同。 // 只恢复经过身份校验的已有请求;显式改参和新请求仍走当前合同。
if snapshot.generation_kind == generation_kind if snapshot.generation_kind == generation_kind
&& snapshot.generation_prompt == build_platform_art_asset_prompt(prompt, &[], &options) && snapshot.generation_prompt == build_platform_art_asset_prompt(prompt, &options)
&& options.asset_kind == kind && options.asset_kind == kind
&& options.aspect_ratio == ratio && options.aspect_ratio == ratio
&& options.image_size == size && options.image_size == size
@@ -2261,39 +2261,16 @@ async fn resolve_platform_art_generation_references_at(
Ok(PlatformArtGenerationReferences { canonical, ordered }) Ok(PlatformArtGenerationReferences { canonical, ordered })
} }
fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> { pub(crate) fn validate_platform_art_icon_prompt(prompt: &str) -> Result<(), String> {
// External Editor validates each description independently (currently at // External Editor 的单条 iconDescriptions 合同一致;超限拒绝,不截断或拆条。
// 200 Unicode characters). Keep the gameplay context short enough that a let prompt = prompt.trim();
// long creation request cannot reject the atlas before it is queued. if prompt.is_empty() {
const MAX_DESCRIPTION_CHARS: usize = 200; return Err("图标素材描述不能为空".to_string());
const CONTEXT_PREFIX: &str = ";遵循同一项目视觉规范:";
let category =
"按当前项目需求生成一组可独立使用的透明素材;数量、类别、排列和切片方式由本次需求决定";
let context_budget = MAX_DESCRIPTION_CHARS.saturating_sub(
category
.chars()
.count()
.saturating_add(CONTEXT_PREFIX.chars().count()),
);
let project_context = truncate_inline_bounded(prompt.trim(), context_budget);
vec![format!("{category}{CONTEXT_PREFIX}{project_context}")]
}
fn truncate_inline_bounded(value: &str, max_chars: usize) -> String {
let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
let actual_chars = normalized.chars().count();
if actual_chars <= max_chars {
return normalized;
} }
if max_chars <= 3 { if prompt.chars().count() > 200 {
return ".".repeat(max_chars); return Err("图标素材描述不能超过 200 个字符".to_string());
} }
let mut output = normalized Ok(())
.chars()
.take(max_chars.saturating_sub(3))
.collect::<String>();
output.push_str("...");
output
} }
fn decode_platform_art_image_with_limits( fn decode_platform_art_image_with_limits(
@@ -2565,10 +2542,10 @@ pub(in crate::agent) struct AdmittedPlatformArtGeneration {
pub(in crate::agent) fn admit_platform_art_generation_at( pub(in crate::agent) fn admit_platform_art_generation_at(
root: &Path, root: &Path,
prompt: &str, prompt: &str,
briefs: &[AgentGroupBrief], _briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions, options: &PlatformArtAssetGenerationOptions,
) -> Result<AdmittedPlatformArtGeneration, String> { ) -> Result<AdmittedPlatformArtGeneration, String> {
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options); let generation_prompt = build_platform_art_asset_prompt(prompt, options);
let context = let context =
standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?; standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?;
let guard = super::external_generation_state::acquire_durable_platform_art_generation_guard( let guard = super::external_generation_state::acquire_durable_platform_art_generation_guard(
@@ -2590,7 +2567,7 @@ pub(in crate::agent) async fn generate_admitted_platform_art_asset_at(
options: &PlatformArtAssetGenerationOptions, options: &PlatformArtAssetGenerationOptions,
admission: AdmittedPlatformArtGeneration, admission: AdmittedPlatformArtGeneration,
) -> Result<GeneratedPlatformArtAsset, String> { ) -> Result<GeneratedPlatformArtAsset, String> {
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options); let generation_prompt = build_platform_art_asset_prompt(prompt, options);
let context = let context =
standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?; standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?;
if admission.context != context if admission.context != context
@@ -2618,7 +2595,7 @@ pub(crate) async fn generate_platform_art_asset_with_options_at(
briefs: &[AgentGroupBrief], briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions, options: &PlatformArtAssetGenerationOptions,
) -> Result<GeneratedPlatformArtAsset, String> { ) -> Result<GeneratedPlatformArtAsset, String> {
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options); let generation_prompt = build_platform_art_asset_prompt(prompt, options);
let runtime_context = let runtime_context =
standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?; standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?;
generate_platform_art_asset_with_runtime_options_at( generate_platform_art_asset_with_runtime_options_at(
@@ -2645,7 +2622,7 @@ pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
if options.asset_kind != GameCreationAppAssetKind::IconSpritesheet { if options.asset_kind != GameCreationAppAssetKind::IconSpritesheet {
return Err("严格游戏切片生成只允许 icon-spritesheet 资产类型".to_string()); return Err("严格游戏切片生成只允许 icon-spritesheet 资产类型".to_string());
} }
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options); let generation_prompt = build_platform_art_asset_prompt(prompt, options);
let runtime_context = let runtime_context =
standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?; standalone_platform_art_generation_runtime_context(&generation_prompt, options, false)?;
generate_platform_art_asset_with_runtime_options_at( generate_platform_art_asset_with_runtime_options_at(
@@ -3106,6 +3083,9 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
options: &PlatformArtAssetGenerationOptions, options: &PlatformArtAssetGenerationOptions,
runtime_context: Option<&PlatformArtGenerationRuntimeContext>, runtime_context: Option<&PlatformArtGenerationRuntimeContext>,
) -> Result<PreparedPlatformArtAssetGeneration, String> { ) -> Result<PreparedPlatformArtAssetGeneration, String> {
if options.asset_kind == GameCreationAppAssetKind::IconSpritesheet {
validate_platform_art_icon_prompt(prompt)?;
}
enforce_project_permission_policy(root, "canvas.asset_generate")?; enforce_project_permission_policy(root, "canvas.asset_generate")?;
let persisted_runtime_state = runtime_context let persisted_runtime_state = runtime_context
.map(|context| { .map(|context| {
@@ -3150,7 +3130,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
.timeout(EXTERNAL_GENERATION_SUBMIT_TIMEOUT) .timeout(EXTERNAL_GENERATION_SUBMIT_TIMEOUT)
.build() .build()
.map_err(|error| format!("创建 External Editor 生成提交客户端失败:{error}"))?; .map_err(|error| format!("创建 External Editor 生成提交客户端失败:{error}"))?;
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options); let generation_prompt = build_platform_art_asset_prompt(prompt, options);
let ( let (
generated, generated,
canvas_context, canvas_context,
@@ -3348,7 +3328,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
"/api/external/v1/editor/icon-spritesheets/generations", "/api/external/v1/editor/icon-spritesheets/generations",
serde_json::json!({ serde_json::json!({
"referenceId": reference_id, "referenceId": reference_id,
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt), "iconDescriptions": [generation_prompt],
"sliceCount": options.slice_count, "sliceCount": options.slice_count,
"sliceMode": options.slice_mode, "sliceMode": options.slice_mode,
"gridX": options.grid_x, "gridX": options.grid_x,
@@ -8413,7 +8393,6 @@ pub(crate) fn platform_art_asset_art_spec(
pub(crate) fn build_platform_art_asset_prompt( pub(crate) fn build_platform_art_asset_prompt(
prompt: &str, prompt: &str,
briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions, options: &PlatformArtAssetGenerationOptions,
) -> String { ) -> String {
if options.asset_kind == GameCreationAppAssetKind::Image { if options.asset_kind == GameCreationAppAssetKind::Image {
@@ -8452,20 +8431,8 @@ pub(crate) fn build_platform_art_asset_prompt(
truncate_prompt_context(prompt.trim()) truncate_prompt_context(prompt.trim())
); );
} }
let art_asset_brief = briefs // 图集的工程化生图指令由 API Server 统一添加,客户端仅保留用户原文。
.iter() prompt.trim().to_string()
.flat_map(|brief| brief.role_briefs.iter())
.find(|role_brief| {
role_brief.group_definition.id == "art" && role_brief.role_definition.id == "asset"
})
.map(|role_brief| role_brief.markdown.trim())
.filter(|markdown| !markdown.is_empty())
.unwrap_or(prompt_text!("media.default_art_brief"));
format!(
prompt_text!("media.spritesheet_generation"),
truncate_inline(prompt, 240),
truncate_prompt_context(art_asset_brief)
)
} }
#[cfg(test)] #[cfg(test)]
@@ -8533,15 +8500,15 @@ mod canvas_generation_tests {
} }
#[test] #[test]
fn generation_kind_catalog_binds_art_spritesheet_to_a_spec_board_prompt() { fn generation_kind_catalog_keeps_art_spritesheet_description_and_art_spec() {
// 放行 icon-spritesheet 必须真的走到图集提示词与图集请求合同, // 图集描述由用户提供,工程化生图提示词由 API Server 添加。
// 否则新 IPC 只是加了一个能通过校验但不生成图集的 kind。
let options = PlatformArtAssetGenerationOptions { let options = PlatformArtAssetGenerationOptions {
asset_kind: GameCreationAppAssetKind::IconSpritesheet, asset_kind: GameCreationAppAssetKind::IconSpritesheet,
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
assert!( assert_eq!(
build_platform_art_asset_prompt("原创收集玩法", &[], &options).contains("素材图集") build_platform_art_asset_prompt("原创收集玩法", &options),
"原创收集玩法"
); );
assert_eq!( assert_eq!(
platform_art_asset_art_spec(&options)["assetType"], platform_art_asset_art_spec(&options)["assetType"],
@@ -8945,7 +8912,7 @@ mod canvas_generation_tests {
asset_label: "失败后可重试素材".to_string(), asset_label: "失败后可重试素材".to_string(),
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let generated_prompt = build_platform_art_asset_prompt("相同手工请求", &[], &options); let generated_prompt = build_platform_art_asset_prompt("相同手工请求", &options);
let runtime_context = let runtime_context =
standalone_platform_art_generation_runtime_context(&generated_prompt, &options, false) standalone_platform_art_generation_runtime_context(&generated_prompt, &options, false)
.expect("standalone retry context"); .expect("standalone retry context");
@@ -9121,8 +9088,8 @@ mod canvas_generation_tests {
asset_label: "图标规范".to_string(), asset_label: "图标规范".to_string(),
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let first_prompt = build_platform_art_asset_prompt("第一个手工请求", &[], &options); let first_prompt = build_platform_art_asset_prompt("第一个手工请求", &options);
let second_prompt = build_platform_art_asset_prompt("第二个手工请求", &[], &options); let second_prompt = build_platform_art_asset_prompt("第二个手工请求", &options);
let first = let first =
standalone_platform_art_generation_runtime_context(&first_prompt, &options, false) standalone_platform_art_generation_runtime_context(&first_prompt, &options, false)
.expect("first standalone slot context"); .expect("first standalone slot context");
@@ -9556,7 +9523,7 @@ mod canvas_generation_tests {
asset_label: "图标规范".to_string(), asset_label: "图标规范".to_string(),
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let prompt = build_platform_art_asset_prompt("旧槽兼容手工请求", &[], &options); let prompt = build_platform_art_asset_prompt("旧槽兼容手工请求", &options);
let context = standalone_platform_art_generation_runtime_context(&prompt, &options, false) let context = standalone_platform_art_generation_runtime_context(&prompt, &options, false)
.expect("current standalone context"); .expect("current standalone context");
let legacy_run_id = legacy_standalone_platform_art_generation_run_id(&options, false) let legacy_run_id = legacy_standalone_platform_art_generation_run_id(&options, false)
@@ -9676,8 +9643,7 @@ mod canvas_generation_tests {
asset_label: "另一个图标规范".to_string(), asset_label: "另一个图标规范".to_string(),
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let other_prompt = let other_prompt = build_platform_art_asset_prompt("另一个旧槽手工请求", &other_options);
build_platform_art_asset_prompt("另一个旧槽手工请求", &[], &other_options);
let other_context = standalone_platform_art_generation_runtime_context( let other_context = standalone_platform_art_generation_runtime_context(
&other_prompt, &other_prompt,
&other_options, &other_options,
@@ -10877,7 +10843,7 @@ mod canvas_generation_tests {
let current_prompt = "恢复时使用同一个生成意图"; let current_prompt = "恢复时使用同一个生成意图";
let current_options = PlatformArtAssetGenerationOptions::default(); let current_options = PlatformArtAssetGenerationOptions::default();
let durable_generation_prompt = let durable_generation_prompt =
build_platform_art_asset_prompt(current_prompt, &[], &current_options); build_platform_art_asset_prompt(current_prompt, &current_options);
let request_body = serde_json::json!({ let request_body = serde_json::json!({
"prompt": "持久化且必须原样重发的生成正文", "prompt": "持久化且必须原样重发的生成正文",
"kind": "spec", "kind": "spec",
@@ -11143,7 +11109,7 @@ mod canvas_generation_tests {
screen_color: None, screen_color: None,
}; };
let prompt = "生成同一套整包美术"; let prompt = "生成同一套整包美术";
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options); let generation_prompt = build_platform_art_asset_prompt(prompt, &options);
let request_body = serde_json::json!({ let request_body = serde_json::json!({
"prompt": generation_prompt, "prompt": generation_prompt,
"kind": "spec", "kind": "spec",
@@ -11530,8 +11496,7 @@ mod canvas_generation_tests {
asset_label: "账号隔离测试".to_string(), asset_label: "账号隔离测试".to_string(),
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let generation_prompt = let generation_prompt = build_platform_art_asset_prompt(current_prompt, &current_options);
build_platform_art_asset_prompt(current_prompt, &[], &current_options);
let request_body = serde_json::json!({ let request_body = serde_json::json!({
"prompt": generation_prompt, "prompt": generation_prompt,
"kind": "spec", "kind": "spec",
@@ -11739,7 +11704,7 @@ mod canvas_generation_tests {
..PlatformArtAssetGenerationOptions::default() ..PlatformArtAssetGenerationOptions::default()
}; };
let durable_generation_prompt = let durable_generation_prompt =
build_platform_art_asset_prompt(current_prompt, &[], &current_options); build_platform_art_asset_prompt(current_prompt, &current_options);
let request_body = serde_json::json!({ let request_body = serde_json::json!({
"prompt": "持久化的原始生成正文", "prompt": "持久化的原始生成正文",
"kind": "spec", "kind": "spec",
@@ -11883,7 +11848,7 @@ mod canvas_generation_tests {
}; };
let options = PlatformArtAssetGenerationOptions::default(); let options = PlatformArtAssetGenerationOptions::default();
let old_generation_prompt = let old_generation_prompt =
build_platform_art_asset_prompt("原来的贪吃蛇美术方向", &[], &options); build_platform_art_asset_prompt("原来的贪吃蛇美术方向", &options);
let binding_access = let binding_access =
ExternalEditorBindingAccess::new(&base_url, "changed-intent-key", None) ExternalEditorBindingAccess::new(&base_url, "changed-intent-key", None)
.expect("prepare changed-intent binding access"); .expect("prepare changed-intent binding access");
@@ -12050,7 +12015,7 @@ mod canvas_generation_tests {
screen_color: None, screen_color: None,
}; };
let prompt = "保持同一个生成提示词"; let prompt = "保持同一个生成提示词";
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options); let generation_prompt = build_platform_art_asset_prompt(prompt, &options);
let binding_access = let binding_access =
ExternalEditorBindingAccess::new(&base_url, "changed-reference-key", None) ExternalEditorBindingAccess::new(&base_url, "changed-reference-key", None)
.expect("prepare changed-reference binding access"); .expect("prepare changed-reference binding access");
@@ -12313,8 +12278,7 @@ mod canvas_generation_tests {
.expect("create accepted failure ledger access"); .expect("create accepted failure ledger access");
let current_prompt = "失败恢复提示词"; let current_prompt = "失败恢复提示词";
let current_options = PlatformArtAssetGenerationOptions::default(); let current_options = PlatformArtAssetGenerationOptions::default();
let generation_prompt = let generation_prompt = build_platform_art_asset_prompt(current_prompt, &current_options);
build_platform_art_asset_prompt(current_prompt, &[], &current_options);
let (state, _) = prepare_platform_art_generation_runtime_state( let (state, _) = prepare_platform_art_generation_runtime_state(
root, root,
&runtime_context, &runtime_context,
@@ -12517,7 +12481,7 @@ mod canvas_generation_tests {
screen_color: None, screen_color: None,
}; };
let prompt = "恢复已受理视觉规范图"; let prompt = "恢复已受理视觉规范图";
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options); let generation_prompt = build_platform_art_asset_prompt(prompt, &options);
let (_, _, frozen_platform_session) = let (_, _, frozen_platform_session) =
resolve_canvas_sync_api_credentials(None, None).expect("freeze recovery account"); resolve_canvas_sync_api_credentials(None, None).expect("freeze recovery account");
let frozen_platform_session = let frozen_platform_session =
@@ -12831,21 +12795,32 @@ mod canvas_generation_tests {
} }
#[test] #[test]
fn canonical_art_spritesheet_request_preserves_project_requirements() { fn canonical_art_spritesheet_request_preserves_description_and_internal_newlines() {
let descriptions = canonical_art_spritesheet_icon_descriptions("原创收集玩法"); let options = PlatformArtAssetGenerationOptions {
assert_eq!(descriptions.len(), 1); asset_kind: GameCreationAppAssetKind::IconSpritesheet,
assert!(descriptions[0].contains("原创收集玩法")); ..PlatformArtAssetGenerationOptions::default()
assert!(descriptions[0].contains("数量、类别、排列和切片方式由本次需求决定")); };
let prompt = "\u{0085} 金币\n木制宝箱\n银色钥匙 \u{0085}";
validate_platform_art_icon_prompt(prompt).expect("valid multiline description");
assert_eq!(
build_platform_art_asset_prompt(prompt, &options),
"金币\n木制宝箱\n银色钥匙"
);
} }
#[test] #[test]
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() { fn canonical_art_spritesheet_description_obeys_external_editor_unicode_item_limit() {
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128)); let boundary = "🪙".repeat(200);
validate_platform_art_icon_prompt(&format!("\u{0085} {boundary}\n"))
assert!(!descriptions.is_empty()); .expect("200 Unicode characters after Rust trim must pass");
assert!(descriptions let error = validate_platform_art_icon_prompt(&"🪙".repeat(201))
.iter() .expect_err("201 Unicode characters must be rejected instead of truncated");
.all(|description| description.chars().count() <= 200)); assert!(error.contains("200"), "{error}");
for blank in ["", " \r\n\t", "\u{0085}\u{2003}"] {
assert!(validate_platform_art_icon_prompt(blank).is_err());
}
validate_platform_art_icon_prompt("\u{feff}")
.expect("BOM is not whitespace under the API Server Rust trim contract");
} }
#[tokio::test] #[tokio::test]
@@ -1,4 +1,4 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@@ -143,6 +143,13 @@ struct AgcSkillManifestEntry {
sha256: String, sha256: String,
} }
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AgcSkillCatalogEntry {
pub(crate) name: String,
pub(crate) description: String,
}
fn is_safe_skill_relative_path(value: &str) -> bool { fn is_safe_skill_relative_path(value: &str) -> bool {
let path = Path::new(value); let path = Path::new(value);
!value.is_empty() !value.is_empty()
@@ -256,6 +263,21 @@ pub(crate) fn agc_skill_pack_fingerprint() -> Result<String, String> {
Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref()))) Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref())))
} }
/// 返回当前客户端随 AGC 一起启用的内置 Skill 候选。
///
/// 前端不得复制审核清单;Skill 名称和描述统一从经过校验的资源 manifest 派生。
#[tauri::command]
pub(crate) fn list_agc_skill_catalog() -> Result<Vec<AgcSkillCatalogEntry>, String> {
Ok(validated_skill_pack_manifest()?
.skills
.into_iter()
.map(|entry| AgcSkillCatalogEntry {
name: entry.name,
description: entry.purpose,
})
.collect())
}
pub(crate) fn render_agc_skill_pack_index() -> Result<String, String> { pub(crate) fn render_agc_skill_pack_index() -> Result<String, String> {
let manifest = validated_skill_pack_manifest()?; let manifest = validated_skill_pack_manifest()?;
let mut lines = vec![format!( let mut lines = vec![format!(
@@ -350,6 +372,19 @@ mod tests {
} }
} }
#[test]
fn skill_catalog_is_derived_from_the_validated_manifest() {
let catalog = list_agc_skill_catalog().expect("skill catalog");
assert_eq!(catalog.len(), AGC_SKILL_PACK_EXPECTED_NAMES.len());
for expected_name in AGC_SKILL_PACK_EXPECTED_NAMES {
let entry = catalog
.iter()
.find(|entry| entry.name == expected_name)
.expect("expected bundled skill");
assert!(!entry.description.trim().is_empty());
}
}
#[test] #[test]
fn skill_content_digest_is_stable_across_lf_and_crlf() { fn skill_content_digest_is_stable_across_lf_and_crlf() {
fn digest(bytes: &[u8]) -> String { fn digest(bytes: &[u8]) -> String {

Some files were not shown because too many files have changed in this diff Show More