Compare commits

..

36 Commits

Author SHA1 Message Date
lhk229 3de9341a9c 合并最新主分支并保留策划退役清理
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m37s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m6s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 19m40s
Project CI / Frontend tests (pull_request) Successful in 21m8s
Project CI / Repository checks (pull_request) Successful in 21m29s
Project CI / Backend tests (pull_request) Successful in 27m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 19m50s
合并 origin/master 的最新功能与测试变更

保留已删除的策划 Agent V1/V2 运行时和旧 GDD 入口

仅解决资源预览类型冲突,不恢复退役策划调用
2026-09-17 14:10:46 +00:00
lhk229 3e3b35713d 合并最新主分支并清理新增策划耦合
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
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
合入 master 的聊天界面、回合控制及运行时更新
解决七处合并冲突,保持退役策划 V1/V2 链路删除
将新增界面判断连接到 Design Agent 状态并移除旧 GDD 测试依赖
保留现役文档建造指令、回合终止及推理档设置
2026-09-17 02:37:48 +00:00
lhk229 ea34cbdea6 合并远程分支并解决提交历史分叉
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 5m21s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m21s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 5m47s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
合并 origin/rm/design-v2 的 master 合并记录
保留本地已合入的 master 更新及策划 Agent 清理成果
合并前后文件树一致,仅统一双方提交历史
2026-09-17 02:29:46 +00:00
lhk229 f957a545bf Merge remote-tracking branch 'origin/master' into rm/design-v2 2026-09-15 15:52:12 +00:00
lhk229 c2064836ce Merge branch 'master' into rm/design-v2
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
2026-09-15 20:59:09 +08:00
lhk229 8e6fb7d078 更新退役GDD样式测试
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m31s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m35s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m2s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m5s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m15s
Project CI / Frontend tests (pull_request) Successful in 4m35s
Project CI / Repository checks (pull_request) Successful in 4m3s
Project CI / Native shell tests (pull_request) Successful in 7m5s
Project CI / Backend tests (pull_request) Successful in 8m2s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m12s
删除对已移除 GDD 审批弹层选择器的旧断言

保留现役窗口覆盖层样式校验
2026-09-15 08:56:16 +00:00
lhk229 94bd7310a9 更新 Design Agent 运行时说明
移除随包提示词中的旧 project-planning 部署描述

改为当前 Design Agent Runtime 的单 Agent 运行口径
2026-09-15 08:15:15 +00:00
lhk229 1d50f4fef5 清理策划样例路径残留
将通用 file.read 证据测试改用中性 design.md 样例

确保现役代码和测试不再引用旧 Fast GDD 路径
2026-09-15 08:12:32 +00:00
lhk229 23bb0bf131 Merge remote-tracking branch 'origin/master' into rm/design-v2
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m39s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m27s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m35s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m56s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m18s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m57s
Project CI / Frontend tests (pull_request) Failing after 3m31s
Project CI / Repository checks (pull_request) Failing after 3m25s
Project CI / Native shell tests (pull_request) Successful in 6m48s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m16s
Project CI / Backend tests (pull_request) Successful in 7m26s
2026-09-15 08:09:21 +00:00
lhk229 758ef5ceec 清理旧Fast GDD命名残留
将通用验收证据和委派测试中的旧 GDD 路径改为中性产物

保留做游戏 Agent 的通用证据回执逻辑
2026-09-15 08:07:12 +00:00
lhk229 ea843641e8 清理退役策划提示词入口
删除 Supervisor playbook 中旧 project-planning 委派说明

保留通用协作与回执规则
2026-09-15 08:04:27 +00:00
lhk229 26d31d73e2 清理退役策划注释残留
移除静态委派验收中的旧 plan.submit_gdd 说明

保持通用委派返工判定语义不变
2026-09-15 08:01:36 +00:00
lhk229 2bce007a0a 收口退役策划运行时分流
移除旧策划 source、plan.submit_gdd 专用卡和相关前端提交分支

保留通用 Supervisor 提交与做游戏 DAG 路由

同步临时计划文档的实际删除边界
2026-09-15 07:58:03 +00:00
lhk229 337cb39340 删除退役策划事件测试
移除 planning-session-v2-stream 订阅注销测试

保留通用 Tauri 事件订阅竞态回归覆盖
2026-09-15 07:43:39 +00:00
lhk229 6d4f7a976f 清理旧策划委派语义
删除 project-planning 的澄清与用户修订跳转说明

保留做游戏 Agent 的静态委派返工语义

更新运行时诊断测试中的旧策划身份夹具
2026-09-15 07:41:25 +00:00
lhk229 12092ceb50 删除策划专用文件与GDD命令
移除旧 Fast GDD 打开命令及 Tauri 注册

删除规划目录和 GDD 投影的专用文件保护分支

保留通用 Agent 控制面与 DirectCodex 文件安全边界
2026-09-15 07:27:30 +00:00
lhk229 bcbf398b65 移除退役策划界面样式
删除旧 GDD 审批卡与策划窄条的孤儿 CSS

更新聊天布局测试以覆盖现役 DirectCodex 选择器

保留设计 Agent 和通用项目工作台表现
2026-09-15 07:05:46 +00:00
lhk229 151c61ad08 清理退役策划测试夹具
删除策划 V2 与批准 GDD 的旧测试数据和调用模拟

保留现役设计 Agent 与 DirectCodex 的界面测试

移除测试中对退役规划命令的断言
2026-09-15 07:00:00 +00:00
lhk229 79b49519b6 删除退役GDD交付入口
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
移除已批准 fast GDD 自动转建游戏流程

清理工作台与首页创建控制器中的旧策划回调

保留 DirectCodex 正常做游戏入口
2026-09-15 06:41:41 +00:00
lhk229 ec2180f281 删除策划V2会话状态链路
移除 App 内策划 V2 会话、GDD 同步与审批状态

删除策划 V2 类型转换模块和退役 GDD 状态类型

保留现役设计 Agent 会话恢复与项目运行时入口
2026-09-15 06:38:29 +00:00
lhk229 341080f42d 收紧退役策划与Supervisor清理边界
明确Supervisor产品运行链路已退役

将做游戏和做素材统一收口到DirectCodex

保留16 Agent DAG仅到编译所需范围
2026-09-15 05:49:54 +00:00
lhk229 45401de57e 解耦设计Agent与策划V2状态
为 Design Agent 增加独立 active、瞬态回复和 reasoning 状态

保留策划 V2 会话自身的状态与消息动画链路

统一项目总控轮询使用两条策划 lane 的合并状态

移除 Design Agent 对旧 planning lane ref 的共享依赖
2026-09-15 05:03:38 +00:00
lhk229 908d248f27 Merge remote-tracking branch 'origin/master' into rm/design-v2
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
2026-09-15 04:49:59 +00:00
lhk229 65598b9886 补充退役策划Agent解耦清理计划
新增退役策划 Agent V1/V2 解耦清理里程碑规范

新增分提交实施计划、修改边界与验收命令

明确不主动清理做游戏 Supervisor 与 16 Agent DAG
2026-09-15 04:49:49 +00:00
lhk229 64ab959d4f 移除退役策划 V2 流式事件订阅
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m3s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m47s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m48s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 2m2s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m26s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m7s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m33s
Project CI / Native shell tests (pull_request) Failing after 3m17s
Project CI / Frontend tests (pull_request) Successful in 3m42s
删除 planning-session-v2-stream 事件监听与旧实时回复错误处理

保留新版 Design Agent 事件订阅链路
2026-09-15 02:42:03 +00:00
lhk229 9833a53e06 Merge remote-tracking branch 'origin/master' into rm/design-v2
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m2s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 3m6s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 3m7s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 3m10s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m57s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m39s
Project CI / Native shell tests (pull_request) Failing after 3m35s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m6s
Project CI / Repository checks (pull_request) Successful in 2m49s
Project CI / Frontend tests (pull_request) Successful in 4m4s
Project CI / Backend tests (pull_request) Successful in 7m22s
2026-09-15 02:27:25 +00:00
lhk229 0847b1bb83 Merge remote-tracking branch 'origin/rm/design-v2' into rm/design-v2
Project CI / Repository checks (pull_request) Successful in 2m42s
Project CI / Frontend tests (pull_request) Successful in 3m0s
Project CI / Native shell tests (pull_request) Failing after 3m51s
Project CI / Backend tests (pull_request) Successful in 5m46s
2026-09-14 07:23:48 +00:00
lhk229 8f691d4c8c Merge branch 'master' into rm/design-v2
Project CI / Repository checks (pull_request) Failing after 3m20s
Project CI / Frontend tests (pull_request) Failing after 2m42s
Project CI / Native shell tests (pull_request) Failing after 3m57s
Project CI / Backend tests (pull_request) Successful in 6m23s
2026-09-14 15:22:30 +08:00
lhk229 d3b9f326f0 删除孤立的策划 GDD 模型模块
移除已无调用方的 planning_gdd_model.rs 及模块导出

删除仅依赖退役提交错误类型的无效判定函数
2026-09-14 07:20:13 +00:00
lhk229 7eb2326a76 移除退役策划 V2 运行态刷新入口
Project CI / Repository checks (pull_request) Failing after 2m22s
Project CI / Frontend tests (pull_request) Failing after 3m7s
Project CI / Native shell tests (pull_request) Failing after 3m47s
Project CI / Backend tests (pull_request) Successful in 6m38s
删除旧版策划状态随项目总控运行态刷新的专用 effect

移除仅服务该 effect 的 planningLane 判定模块
2026-09-14 06:30:07 +00:00
lhk229 9fc28790a4 移除退役策划 V2 运行面板参数
删除项目总控运行面板中已无调用方的 GDD 审批状态参数
2026-09-14 06:26:54 +00:00
lhk229 cea45a608d 删除退役策划 V2 前端审批面板
移除旧版 GDD 审批卡和策划运行条组件

项目总控视图仅保留新版 Design Agent 与通用运行面板
2026-09-14 06:23:16 +00:00
lhk229 ee03ade156 Merge remote-tracking branch 'origin/master' into rm/design-v2
Project CI / Repository checks (pull_request) Failing after 3m24s
Project CI / Frontend tests (pull_request) Failing after 3m2s
Project CI / Backend tests (pull_request) Successful in 6m55s
Project CI / Native shell tests (pull_request) Failing after 4m20s
2026-09-14 06:19:46 +00:00
lhk229 4933c6c380 删除退役策划 V2 前端测试契约
Project CI / Repository checks (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 / Native shell tests (pull_request) Has been cancelled
移除 Plan GDD 审批测试套件及注册入口
2026-09-14 06:15:13 +00:00
lhk229 931f3eae0a 删除退役策划 V2 Rust Runtime
移除 Planning V2 协议模块与 Tauri 命令注册

保留新版 Design Agent、做游戏 Agent 与共享运行时
2026-09-14 06:07:20 +00:00
lhk229 c5f8fa1ef6 清理旧策划 V1 残留注释
删除已失效的 --plan 策划自动应答说明
2026-09-14 05:54:24 +00:00
129 changed files with 1139 additions and 21542 deletions
@@ -957,9 +957,6 @@ async function runInteractiveCargo(cliArguments, setActiveChild) {
return result;
}
// 立项策划跑 standard 档,`agent.delegate` 这类动作按项目权限策略必须逐个确认,
// 而确认和问询都只从 CLI 的 stdin 读。自主构建档没有这一步,所以只有 --plan 需要
// 一个把「人坐在终端前敲 approve」自动化掉的应答器;判据本身仍然走后端确认命令。
const swarmConfirmationPromptPattern = /输入 approve 或 reject$/u;
const swarmUserInputPromptPattern = /请选择 1-\d+,或直接输入其他答案:$/u;
@@ -1553,13 +1553,11 @@ if (!viteConfigSource.includes('allow: [repoRoot]')) {
);
}
if (
!(
tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
)
) {
if (!(
tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
)) {
throw new Error(
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
);
@@ -1192,7 +1192,7 @@ async function main() {
function isDirectModuleExecution() {
return Boolean(
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
resolve(process.argv[1]) === fileURLToPath(import.meta.url),
);
}
File diff suppressed because one or more lines are too long
@@ -2,8 +2,4 @@
需要等待专业 Agent 时不得调用 respond_to_userRuntime 会通过 delegate/all-join 完成屏障保持同一父 run,取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。contractStatus=needs-user-input 时,Runtime 会按原 delivery 逐一发起 user.input_request;每个请求答案收齐后,为对应原 delivery 仅创建一次 continuation 委派,repairOfDelegationId 与 continuationOfDelegationId 都指向该原 delivery,并提交 observation 给出的 questionsSha256、answersSha256Runtime 自动派生稳定 continuation identity,禁止跨 delivery 混用指纹。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。
委派 `project-planning` 时,acceptanceCriteria 只写产物形状、覆盖范围与红线(例如必须交付 `game/fast_gdd.md`、必须原创、必须只定义一个 MVP 闭环),**不得替用户预先裁定产品取舍**。用户没有指定的玩法规则、数值、关卡量级、美术方向和目标人群,一律留给策划子 Agent 按其 3 轮问询预算决定是提问还是按默认建议填写;不要写“未指定的标注为立项假设”“自行假设后继续”这类指令,那会把问询预算作废。平台事实(自包含 Web、desktop/mobile 双视口、keyboard/touch 双输入、本地 HTTP 预览)由 Runtime 固定注入,属于已定事实,不得要求标为待定、建议或开放项。
`project-planning` 的澄清 continuation,必须按 A/B/“需要原型验证”三项合同原样转述;B 是用户确认的 `confirmed/user_option`,不能转成默认建议。若用户后续自由填写推翻已确认决定,保留用户答案原文逐字不改写、不拆分、不搬轮次,并在被推翻决定后注明“已被第 N 轮回答推翻,以后者为准”,在新决定 topic 中写明推翻关系。
只在所有必要回执已认领、manifest 正式任务图已经完成、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。
@@ -1069,15 +1069,10 @@ pub(crate) async fn continue_design_agent_at(
.ok_or("策划 Agent 当前正在工作")?;
let mut session = match read_design_session(root)? {
Some(session) => session,
None => {
if read_planning_session_v2(root)?.is_some() {
return Err("此项目包含旧策划会话,请查看原有记录或在新项目开始五阶段策划".into());
}
new_design_session(
&project_id,
&load_game_creator_app_config()?.selected_model_id,
)
}
None => new_design_session(
&project_id,
&load_game_creator_app_config()?.selected_model_id,
),
};
if session.project_id != project_id {
return Err("策划会话与当前项目不匹配".into());
@@ -2566,18 +2566,11 @@ fn direct_taonier_art_asset_identity(
.to_string(),
reference_resource_ids: asset.source.reference_resource_ids.clone(),
};
// 参考集合先按该 kind 的请求合同收口:根素材(规范图)允许用户参考(icon-spec 没有
// 规范前置,参考只是风格输入),派生素材仍必须按合同携带规范前置。
let references_match_contract =
crate::agent::platform_art_runtime_references_match_request_contract(
&identity.reference_resource_ids,
expected_kind,
);
let lineage_matches = match expected_reference_source {
Some(source) => direct_taonier_reference_matches_local_source(root, source, &identity),
None => true,
None => identity.reference_resource_ids.is_empty(),
};
(references_match_contract && lineage_matches).then_some(identity)
lineage_matches.then_some(identity)
})
}
@@ -2586,9 +2579,7 @@ fn direct_taonier_reference_matches_local_source(
source: &DirectTaonierArtAssetIdentity,
derived: &DirectTaonierArtAssetIdentity,
) -> bool {
// 派生素材的规范身份只由参考序列首项承担:用户参考按顺序追加在规范图之后,
// 不能让它们顶替或淹没规范引用,也不能因为多出用户参考就判定派生关系不成立。
let Some(remote_reference_id) = derived.reference_resource_ids.first() else {
let [remote_reference_id] = derived.reference_resource_ids.as_slice() else {
return false;
};
if derived.canvas_project_id == source.canvas_project_id
@@ -3399,8 +3390,6 @@ async fn generate_direct_taonier_art_asset_at(
slice_mode: (asset_kind == "art-spritesheet").then(|| "connected-components".to_string()),
grid_x: None,
grid_y: None,
reference_asset_ids: Vec::new(),
target_category: None,
};
let runtime_context =
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
@@ -9813,78 +9802,6 @@ mod tests {
);
}
#[test]
fn direct_taonier_art_package_accepts_manifest_user_references() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-art-references", "直连美术参考")
.expect("init project");
register_direct_taonier_art_package_fixture(root.path());
assert!(direct_taonier_art_package_is_valid(root.path()));
// 规范图与背景图带用户参考:参考只是风格输入,规范身份仍由参考序列首项承担。
mutate_manifest_at(root.path(), |manifest| {
let art_spec = manifest
.assets
.iter_mut()
.find(|asset| asset.local_path == DIRECT_CODEX_ART_SPEC_ASSET_PATH)
.expect("art spec asset");
art_spec.source.reference_resource_ids = vec![
"user-reference-1".to_string(),
"user-reference-2".to_string(),
];
let background = manifest
.assets
.iter_mut()
.find(|asset| asset.local_path == DIRECT_CODEX_BACKGROUND_ASSET_PATH)
.expect("background asset");
background
.source
.reference_resource_ids
.push("user-reference-1".to_string());
Ok(())
})
.expect("apply user references to the art base");
assert!(
direct_taonier_art_package_is_valid(root.path()),
"user references must not invalidate the art package"
);
// 图集仍只接受唯一规范引用:多一项用户参考必须失败关闭。
mutate_manifest_at(root.path(), |manifest| {
let spritesheet = manifest
.assets
.iter_mut()
.find(|asset| asset.local_path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH)
.expect("spritesheet asset");
spritesheet
.source
.reference_resource_ids
.push("user-reference-1".to_string());
Ok(())
})
.expect("add an extra spritesheet reference");
assert!(
!direct_taonier_art_package_is_valid(root.path()),
"art spritesheet must reject extra user references"
);
// 用户参考不能顶替图集的规范前置。
mutate_manifest_at(root.path(), |manifest| {
let spritesheet = manifest
.assets
.iter_mut()
.find(|asset| asset.local_path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH)
.expect("spritesheet asset");
spritesheet.source.reference_resource_ids = vec!["user-reference-1".to_string()];
Ok(())
})
.expect("replace the spritesheet canonical reference");
assert!(
!direct_taonier_art_package_is_valid(root.path()),
"a user reference must not replace the art spritesheet canonical spec"
);
}
#[test]
fn direct_output_sync_accepts_a_complete_spritesheet_without_slices() {
let root = tempfile::tempdir().expect("temp dir");
@@ -2341,8 +2341,6 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
slice_mode,
grid_x,
grid_y,
reference_asset_ids: Vec::new(),
target_category: None,
};
let _generation_guard = state.image_generation_gate.lock().await;
let generated = with_direct_editor_api_credentials(
@@ -67,11 +67,10 @@ pub(crate) use canvas_generation::{
generate_platform_art_asset_with_options_at,
generate_platform_art_asset_with_required_slices_at, maybe_generate_platform_art_asset_step,
needs_platform_art_asset_generation, normalize_platform_art_asset_generation_kind,
normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category,
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
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,
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
prepare_platform_art_asset_output_path, project_canvas_asset_media_types,
role_has_canvas_assets, suggested_canvas_tool_call, PlatformArtAssetGenerationOptions,
PLATFORM_ART_ASSET_GENERATION_KINDS,
};
#[allow(unused_imports)]
pub(crate) use draft_validation::{
File diff suppressed because it is too large Load Diff
@@ -549,7 +549,7 @@ fn append_agent_runtime_file_read_evidence_ref(
// 给的是**整个三元组**,不是一个碎片。`agent.acceptance_update` 的 evidence 引用
// 要求 {agentId, runId, actionId} 三个字段,回执查找也按三元组整体做 key。早期
// 只给 actionId,另外两个靠模型回忆——实测它第一次就把其中一个记错,白吃一次
// 拒绝。反正 `validate_fast_gdd_evidence_identity` 只接受当前根 run 的回执,
// 拒绝。验收证据只接受当前根 run 的回执,
// 合法取值唯一,本来就不该让它猜。
observation.summary = format!(
"{} · sourceAgentId={agent_id} · sourceRunId={run_id} · sourceActionId={action_id}",
@@ -919,9 +919,9 @@ mod file_read_source_action_id_tests {
AgentRuntimeToolObservation {
tool: "file.read".to_string(),
status: "ok".to_string(),
summary: "已读取 game/fast_gdd.md 第 1-134 行(共 134 行)".to_string(),
summary: "已读取 design.md 第 1-134 行(共 134 行)".to_string(),
detail: Some(format!(
"game/fast_gdd.md · sha256={} · lines 1-134 of 134
"design.md · sha256={} · lines 1-134 of 134
第一行内容",
"a".repeat(64)
)),
@@ -954,7 +954,7 @@ mod file_read_source_action_id_tests {
agent_runtime_action_receipt_public_safe_detail_for_test(&root, &observation)
.expect("safe detail still parses");
let value = serde_json::from_str::<serde_json::Value>(&safe_detail).expect("json");
assert_eq!(value["path"], "game/fast_gdd.md");
assert_eq!(value["path"], "design.md");
assert_eq!(value["lines"], "1-134 of 134");
assert_eq!(value["contentSha256"], "a".repeat(64));
}
@@ -78,12 +78,6 @@ pub(super) fn game_creator_agent_final_reply_error_allows_fallback(error: &str)
matches!(kind.as_str(), "empty-response" | "deserialize")
}
/// 这些错误只描述本次 Provider input 或候选 GDD;真正的 session CAS 冲突不在
/// 此列——那说明 durable session 已被推进或损坏,必须 reconcile。
fn plan_submit_error_is_business_rejection(error: &PlanningStorageError) -> bool {
matches!(error.code(), "PLAN_INVALID_REQUEST" | "PLAN_SIZE_LIMIT")
}
const AGENT_RUNTIME_PLAN_UPDATE_IDLE_LIMIT: u32 = 4;
/// 最终回复被收束门禁拦下后 run 会原地续跑重试。多数 blocker 是模型自己能解的
@@ -7,9 +7,6 @@ mod design_session;
mod finalization;
mod json_sidecar;
mod models;
mod planning_gdd_model;
mod planning_policy_v2;
mod planning_session_v2;
mod provider_control;
mod provider_retry;
mod real_e2e_checkpoint;
@@ -24,9 +21,6 @@ pub(crate) use design_session::*;
pub(in crate::agent) use finalization::*;
pub(in crate::agent) use json_sidecar::*;
pub(in crate::agent) use models::*;
pub(crate) use planning_gdd_model::*;
pub(crate) use planning_policy_v2::*;
pub(crate) use planning_session_v2::*;
pub(in crate::agent) use provider_control::*;
pub(in crate::agent) use provider_retry::*;
pub(in crate::agent) use real_e2e_checkpoint::*;
@@ -781,7 +781,7 @@ mod provider_reconciliation_diagnostic_tests {
let directory = tempfile::tempdir().expect("diagnostic directory");
let snapshot = AgentRuntimeProviderRequestSnapshot {
project_id: "project-1".to_string(),
agent_id: "project-planning".to_string(),
agent_id: "project-supervisor".to_string(),
task_id: "task-1".to_string(),
session_id: "session-1".to_string(),
run_id: "run-1".to_string(),
@@ -805,7 +805,7 @@ mod provider_reconciliation_diagnostic_tests {
usage: None,
tool_calls: vec![platform_llm::LlmToolCall {
id: "call-1".to_string(),
name: "runtime_tool_plan_submit_gdd".to_string(),
name: "runtime_tool_agent_message".to_string(),
arguments: "{\"path\":\"C:\\\\private\\\\argument\"}".to_string(),
}],
responses_output: Vec::new(),
@@ -201,39 +201,17 @@ pub(crate) fn observe_agent_runtime_agent_message(
}
}
/// 委派 task 末尾那句「你在这条链路上的位置」。三种跳的语义互不相同,共用一句话
/// 就会说谎,所以这里把它显式化。
/// 委派 task 末尾那句「你在这条链路上的位置」。返工和普通委派的语义不同,单独保留。
///
/// - `Repair`:质量返工,`repair_depth` 上限 1,「唯一返工轮」是真的。而且这句话是
/// 做游戏链路的**授权信号**——`design-foundation` / `art-director` /
/// `art-asset-plan` 的角色提示词都把「任务正文明确标识这是带 repairOfDelegationId
/// 的唯一返工轮」当作 `replaceExisting=true` 的唯一许可(见 agent/prompt.rs 的三处
/// 角色 prompt)。这一支逐字不能动。
/// - `PlanClarification`:澄清续跑不消耗 `repair_depth`,预算是
/// `static_delegate_clarification_round_limit_at`plan 链路 3 轮)。master 只有一道
/// 平坦的 depth <= 1 门,那时「唯一返工轮」对澄清跳也成立;本仓库改成按谱系分类后
/// 把预算抬到 3,这句话就变成了假天花板——生产实测 4 次澄清续跑全部命中它,命中后
/// 全部直接出稿,没有任何一个 run 走到第 2 轮。
/// - `UserRevision`:用户在审批卡上点「修改 / 退回」后的修订轮。它同样带
/// `repairOfDelegationId`,但 `repair_depth` 防的是 runaway agent,而这一跳每一轮
/// 都由人触发——人本身就是循环边界,所以 `static_delegate_lineage_counters` 早就
/// 把 depth/round 原样继承了。缺的是这句话:走 `Repair` 分支时用户第一次点修改就
/// 会被告知「这是唯一返工轮」,和澄清跳当初那个假天花板是同一个错误。原型对应的是
/// `USER_REVISION_SOFT_LIMIT = 16`,且超过只提示、不拒绝。
/// - `None`:普通委派,不加这一段。
pub(in crate::agent) enum StaticDelegateHopNote<'a> {
None,
Repair {
original_delegation_id: &'a str,
},
UserRevision {
original_delegation_id: &'a str,
},
PlanClarification {
original_delegation_id: &'a str,
rounds_used: u32,
rounds_limit: u32,
},
Repair { original_delegation_id: &'a str },
}
impl StaticDelegateHopNote<'_> {
@@ -247,32 +225,6 @@ impl StaticDelegateHopNote<'_> {
StaticDelegateHopNote::Repair {
original_delegation_id,
} => format!("\n\n这是对已认领委派 {original_delegation_id} 的唯一返工轮。"),
StaticDelegateHopNote::UserRevision {
original_delegation_id,
} => format!(
"\n\n这是对已认领委派 {original_delegation_id} 的用户修订轮,由用户在审批卡上提出,不是质量返工,不消耗返工深度,也不重置澄清轮次。按任务正文里的用户意见原文修订同一份 GDD 谱系后重新提交;用户看过新稿还可以再次提出修改,这不是最后一轮,不要因此压缩改动或提前收尾。"
),
// 预算用尽:planning_coordinator 出卡时会用
// `current_round >= 3` 直接拒掉第四张卡,所以这里不能再邀请提问,
// 只能要求收稿——语义上等价于原型的 INJ_MUST_DRAFT_ROUNDS。
StaticDelegateHopNote::PlanClarification {
original_delegation_id,
rounds_used,
rounds_limit,
} if rounds_used >= rounds_limit => format!(
"\n\n这是对已认领委派 {original_delegation_id} 的澄清续跑,不是返工轮。已用澄清轮次 {rounds_used}/{rounds_limit},澄清预算已用尽:本轮不得再输出 AGC_NEEDS_USER_INPUT_V1 信封,剩余空白按默认建议补齐并标 default_pending,立即提交 GDD。"
),
// 轮号必须和 planning_coordinator 出卡时的期望一致:那边用
// `validate_exact_plan_clarification_question(.., current_round + 1)`
// current_round 就是本 delivery 的谱系轮次,也就是这里的 rounds_used。
StaticDelegateHopNote::PlanClarification {
original_delegation_id,
rounds_used,
rounds_limit,
} => format!(
"\n\n这是对已认领委派 {original_delegation_id} 的澄清续跑,不是返工轮,不消耗返工深度。已用澄清轮次 {rounds_used}/{rounds_limit}。仍有会实质改变结果的空白且预算未用尽时,可以继续以 AGC_NEEDS_USER_INPUT_V1 信封退出:questions 恰好一题,header 写成「第{next_round}轮·当前要决定:<主题>」,轮号必须是 {next_round},主题写这一轮真正要定的那件事。预算已用尽,或剩余空白能由默认建议覆盖且不影响首个可玩闭环时,立即提交 GDD。",
next_round = rounds_used.saturating_add(1),
),
}
}
}
@@ -1523,122 +1475,3 @@ pub(crate) fn observe_agent_runtime_agent_spawn_isolated(
.map(|value| redact_agent_runtime_project_paths(root, &value, 3_600)),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 委派 task 末尾那句话是两条链路的合同,不能共用一份文案。
///
/// 上半条钉做游戏链路:`design-foundation` / `art-director` / `art-asset-plan`
/// 的角色提示词把「任务正文明确标识这是带 repairOfDelegationId 的唯一返工轮」
/// 当作 `replaceExisting=true` 的唯一授权信号,改一个字就会让返工轮拿不到许可。
///
/// 下半条钉立项策划链路:澄清续跑不是返工轮,套用返工文案等于告诉策划子 Agent
/// 「你只剩这一轮」——这正是生产上 4 次澄清续跑之后无一走到第 2 轮的原因。
/// 同时钉住轮号:`planning_coordinator` 出卡时按 `rounds_used + 1` 校验 header
/// 这里写进 task 的必须是同一个数,否则第 2 轮信封会当场被拒。
/// 用户修订轮同样不能套返工文案。
///
/// 「唯一返工轮」防的是 runaway agent,而这一跳由用户在审批卡上亲手点出来——人本身
/// 就是循环边界,`static_delegate_lineage_counters` 早就把 depth/round 原样继承了。
/// 套用返工文案就是告诉策划子 Agent「用户只能改这一次」,和澄清跳当初那个假天花板
/// 是同一个错误。原型对应的是软阈值 16 次、超过只提示不拒绝。
#[test]
fn user_revision_hop_note_is_not_the_repair_round_note() {
let revision = render_static_delegate_task_contract(
"任务",
"project-supervisor",
"run-1",
"delegation-new",
&["交付 game/fast_gdd.md".to_string()],
&["game/fast_gdd.md".to_string()],
StaticDelegateHopNote::UserRevision {
original_delegation_id: "delegation-old",
},
)
.expect("render user revision hop note");
assert!(
!revision.contains("唯一返工轮"),
"用户修订轮不得复用返工文案,否则子 Agent 以为用户只能改这一次:{revision}"
);
assert!(
revision.contains("不消耗返工深度"),
"必须写明它不吃返工额度:{revision}"
);
assert!(
revision.contains("不是最后一轮"),
"必须写明用户还能再改,否则子 Agent 会把多条意见攒到一轮改完:{revision}"
);
}
#[test]
fn plan_clarification_hop_note_is_not_the_repair_round_note() {
let repair = render_static_delegate_task_contract(
"任务",
"project-supervisor",
"run-1",
"delegation-new",
&["交付产物".to_string()],
&["assets/art-spec.png".to_string()],
StaticDelegateHopNote::Repair {
original_delegation_id: "delegation-old",
},
)
.expect("render repair");
assert!(
repair.contains("这是对已认领委派 delegation-old 的唯一返工轮。"),
"返工轮文案是做游戏链路 replaceExisting 的授权信号,必须逐字保留:{repair}"
);
let clarification = render_static_delegate_task_contract(
"任务",
"project-supervisor",
"run-1",
"delegation-new",
&["交付 game/fast_gdd.md".to_string()],
&["game/fast_gdd.md".to_string()],
StaticDelegateHopNote::PlanClarification {
original_delegation_id: "delegation-old",
rounds_used: 1,
rounds_limit: 3,
},
)
.expect("render clarification");
assert!(
!clarification.contains("唯一返工轮"),
"澄清续跑不得复用返工文案,否则策划子 Agent 以为只剩这一轮:{clarification}"
);
assert!(
clarification.contains("已用澄清轮次 1/3"),
"澄清续跑必须写明已用轮次与上限:{clarification}"
);
assert!(
clarification.contains("第2轮·当前要决定:"),
"task 里的轮号必须等于 planning_coordinator 校验 header 时用的 rounds_used + 1{clarification}"
);
let exhausted = render_static_delegate_task_contract(
"任务",
"project-supervisor",
"run-1",
"delegation-new",
&["交付 game/fast_gdd.md".to_string()],
&["game/fast_gdd.md".to_string()],
StaticDelegateHopNote::PlanClarification {
original_delegation_id: "delegation-old",
rounds_used: 3,
rounds_limit: 3,
},
)
.expect("render exhausted clarification");
assert!(
exhausted.contains("澄清预算已用尽"),
"预算用尽时必须要求收稿,出卡侧会直接拒掉第四张卡:{exhausted}"
);
assert!(
!exhausted.contains("第4轮·当前要决定:"),
"预算用尽时不得再给出下一轮 header,那是一张永远递不上去的卡:{exhausted}"
);
}
}
@@ -10,18 +10,6 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
if autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false) {
return None;
}
if is_agent_planning_storage_path(path) || is_plan_fast_gdd_projection_path(path) {
return Some(AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "blocked".to_string(),
summary: if is_agent_planning_storage_path(path) {
"`.agent/planning/**` 只能由立项策划 Runtime 专用存储层写入".to_string()
} else {
"`game/fast_gdd.md` 只能由立项策划 Runtime renderer 写入".to_string()
},
detail: Some(format!("agentId={agent_id} · runId={run_id} · path={path}")),
});
}
match autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id) {
Ok(true) => {
let allowed = autonomous_manifest_owner_artifact_paths(agent_id);
@@ -577,8 +577,6 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
slice_mode: (!slice_mode.trim().is_empty()).then_some(slice_mode.clone()),
grid_x,
grid_y,
reference_asset_ids: Vec::new(),
target_category: None,
};
if let Some(pending) = pending_action {
match recover_persisted_visual_generation_options(
@@ -630,9 +628,6 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
.or_else(|| (!slice_mode.trim().is_empty()).then_some(slice_mode)),
grid_x,
grid_y,
reference_asset_ids: requested_options.reference_asset_ids,
// Agent 运行时不会指定完成登记的目标栏目,保持调用方给的值(默认 `None`)。
target_category: requested_options.target_category,
}
};
options.replace_existing = replace_existing;
@@ -275,8 +275,7 @@ fn agent_runtime_native_capability_registry() -> Result<&'static CapabilityRegis
///
/// `"__all_agents__"` 是个不对应任何真实 Agent 的哨兵:走这条路径拿到的是
/// 未按身份收窄的完整函数目录。生产代码必须调用 `_for_agent` 版本并传入真实
/// `agentId`,否则按身份收窄的工具面(如 `project-planning` 的 exact
/// allowlist)会被静默绕开。这里用 `#[cfg(test)]` 把「忘记改用 `_for_agent`」
/// `agentId`,否则按身份收窄的工具面会被静默绕开。这里用 `#[cfg(test)]` 把「忘记改用 `_for_agent`」
/// 从运行时静默扩权变成编译期错误。
#[cfg(test)]
pub(crate) fn build_agent_runtime_native_function_tools() -> Result<Vec<LlmFunctionTool>, String> {
@@ -390,12 +390,6 @@ pub(crate) async fn start_local_project_asset_generation(
image_size: Option<String>,
asset_name: Option<String>,
output_path: Option<String>,
// 前端 IPC 字段 `referenceAssetIds`:当前项目 manifest 里的图片素材 id,只做参考输入,
// 不进任务账本(重试由调用方继续用同一份引用提交,账本本身不新增字段)。
reference_asset_ids: Option<Vec<String>>,
// 前端 IPC 字段 `targetCategory`:完成登记时要落盘的正式栏目分类。同样不进任务账本:
// 它与引用一样属于「同一次提交的本地落点」,重试由调用方继续用同一个栏目提交。
target_category: Option<String>,
) -> Result<AssetGenerationTaskRecord, String> {
let task_id = asset_generation_task_id(&task_id)?;
let request = prepare_local_project_asset_generation(
@@ -406,8 +400,6 @@ pub(crate) async fn start_local_project_asset_generation(
image_size.as_deref(),
asset_name.as_deref(),
output_path.as_deref(),
reference_asset_ids.as_deref().unwrap_or_default(),
target_category.as_deref(),
)?;
enforce_project_permission_policy(&request.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&request.root, "asset.register")?;
@@ -1,6 +1,5 @@
use super::*;
use sha2::{Digest as _, Sha256};
use shared_contracts::game_creation_app::GameCreationAppAssetCategory;
use std::future::Future;
const PRIVATE_EXTERNAL_EDITOR_API_KEY_FILE_PREFIX: &str = "external-editor-api-";
@@ -663,7 +662,6 @@ pub(crate) fn register_design_artifacts_at(root: &Path) -> Result<bool, String>
generation_kind: None,
reference_resource_ids: Vec::new(),
},
None,
)?;
changed |= asset_changed;
}
@@ -1878,56 +1876,8 @@ pub(crate) fn register_local_asset_entry(
id_prefix: &str,
source: GameCreationAppAssetSource,
) -> Result<UploadLocalAssetResult, String> {
register_local_asset_entry_with_change(
root, local_path, kind, media_type, id_prefix, source, None,
)
.map(|(result, _)| result)
}
/// 带**显式目标分类**的登记入口:只给 GUI 生成完成路径用(前端 `targetCategory`)。
///
/// 入口栏目与生成 kind 不是同一套词汇(栏目 `character` / `scene` / `ui-interaction`
/// 生成 kind 的派生分类会把图片落到 `unclassified`、规范图落到 `document`),所以要落回
/// 入口栏目只能由调用方把目标分类显式交进来。取值必须先过
/// [`shared_contracts::game_creation_app::game_creation_app_asset_category_from_str`]
/// 非法值失败关闭,绝不回退到 kind 派生;其它调用方继续走
/// [`register_local_asset_entry`],行为不变。
pub(crate) fn register_local_asset_entry_with_category(
root: &Path,
local_path: &str,
kind: &str,
media_type: &str,
id_prefix: &str,
source: GameCreationAppAssetSource,
target_category: Option<&str>,
) -> Result<UploadLocalAssetResult, String> {
let target_category = normalize_asset_category_override(target_category)?;
register_local_asset_entry_with_change(
root,
local_path,
kind,
media_type,
id_prefix,
source,
target_category,
)
.map(|(result, _)| result)
}
/// 归一显式目标分类:只接受合法枚举值,返回落盘字符串。
fn normalize_asset_category_override(
target_category: Option<&str>,
) -> Result<Option<GameCreationAppAssetCategory>, String> {
let Some(target_category) = target_category else {
return Ok(None);
};
let target_category = target_category.trim();
if target_category.is_empty() {
return Ok(None);
}
game_creation_app_asset_category_from_str(target_category)
.map(Some)
.ok_or_else(|| format!("非法资源分类:{target_category}"))
register_local_asset_entry_with_change(root, local_path, kind, media_type, id_prefix, source)
.map(|(result, _)| result)
}
fn register_local_asset_entry_with_change(
@@ -1937,7 +1887,6 @@ fn register_local_asset_entry_with_change(
media_type: &str,
id_prefix: &str,
source: GameCreationAppAssetSource,
target_category: Option<GameCreationAppAssetCategory>,
) -> Result<(UploadLocalAssetResult, bool), String> {
let normalized_path = normalize_relative_path(local_path)?;
let absolute_path = resolve_local_project_path(root, &normalized_path)?;
@@ -1963,17 +1912,11 @@ fn register_local_asset_entry_with_change(
// kind 没变时刻意不动 category——落盘分类是权威值,同 kind 重登记不得抹掉它。
let changed = existing.kind != kind
|| existing.media_type != media_type
|| existing.source != source
|| target_category.is_some_and(|category| existing.category != category);
|| existing.source != source;
if existing.kind != kind {
existing.kind = kind.to_string();
existing.category = game_creation_app_asset_category_for_kind(kind);
}
// 调用方显式给出目标分类时它就是权威值:GUI 完成登记必须能落回入口栏目,
// 这也是同路径重新生成时把资产从旧栏目(或 unclassified)原位接管过来的唯一入口。
if let Some(category) = target_category {
existing.category = category;
}
existing.media_type = media_type.to_string();
existing.source = source;
Ok((existing.id.clone(), "asset.update", changed))
@@ -1990,8 +1933,7 @@ fn register_local_asset_entry_with_change(
local_path: normalized_path.clone(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: target_category
.unwrap_or_else(|| game_creation_app_asset_category_for_kind(kind)),
category: game_creation_app_asset_category_for_kind(kind),
tags: Vec::new(),
source,
});
@@ -2211,7 +2153,6 @@ pub(crate) fn delete_manifest_asset_at(
#[cfg(test)]
mod tests {
use super::*;
use shared_contracts::game_creation_app::GameCreationAppAssetCategory;
use std::io::{Read, Write};
#[test]
@@ -2235,121 +2176,6 @@ mod tests {
assert!(!register_design_artifacts_at(root).expect("register idempotently"));
}
/// GUI 完成登记可以显式指定目标栏目:新建条目与已登记条目都按显式值落盘。
///
/// 入口栏目(character / scene / ui-interaction)与生成 kind 不是同一套词汇,按 kind 派生
/// 会把图片落到 unclassified,占位拿不回原位;非法值必须失败关闭,不传时保持 kind 派生。
#[test]
fn explicit_target_category_overrides_the_kind_derived_category() {
fn canvas_source() -> GameCreationAppAssetSource {
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
canvas_project_id: None,
resource_id: None,
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
}
}
fn category_of(root: &Path, asset_id: &str) -> GameCreationAppAssetCategory {
read_existing_manifest_for_project(root)
.expect("read manifest")
.assets
.into_iter()
.find(|asset| asset.id == asset_id)
.expect("registered asset is present")
.category
}
let temporary = tempfile::tempdir().expect("tempdir");
let root = temporary.path();
crate::project::init_local_game_project_at(root, "target-category-test", "目标栏目登记")
.expect("init project");
fs::create_dir_all(root.join("assets")).expect("create assets dir");
fs::write(root.join("assets/hero.png"), b"png-bytes").expect("write asset");
let registered = register_local_asset_entry_with_category(
root,
"assets/hero.png",
"image",
"image/png",
"platform-art",
canvas_source(),
Some("character"),
)
.expect("register with a target category");
assert_eq!(
category_of(root, &registered.id),
GameCreationAppAssetCategory::Character
);
// 同 kind 重新生成时显式目标分类仍是权威值:资产要能换栏目原位接管。
register_local_asset_entry_with_category(
root,
"assets/hero.png",
"image",
"image/png",
"platform-art",
canvas_source(),
Some("ui-interaction"),
)
.expect("re-register with another target category");
assert_eq!(
category_of(root, &registered.id),
GameCreationAppAssetCategory::UiInteraction
);
// 非法值失败关闭,且不动已落盘的分类。
assert!(register_local_asset_entry_with_category(
root,
"assets/hero.png",
"image",
"image/png",
"platform-art",
canvas_source(),
Some("version"),
)
.is_err());
assert_eq!(
category_of(root, &registered.id),
GameCreationAppAssetCategory::UiInteraction
);
// 不传目标分类时保持原有行为:新条目按 kind 派生(image → unclassified)。
fs::write(root.join("assets/plain.png"), b"png-bytes").expect("write plain asset");
let plain = register_local_asset_entry(
root,
"assets/plain.png",
"image",
"image/png",
"platform-art",
canvas_source(),
)
.expect("register without a target category");
assert_eq!(
category_of(root, &plain.id),
GameCreationAppAssetCategory::Unclassified
);
// 已落盘的显式分类在 kind 未变时仍然是权威值:同 kind 重登记不得把它抹掉。
register_local_asset_entry(
root,
"assets/hero.png",
"image",
"image/png",
"platform-art",
canvas_source(),
)
.expect("re-register without a target category");
assert_eq!(
category_of(root, &registered.id),
GameCreationAppAssetCategory::UiInteraction
);
}
/// 画板导出推断出的 kind 必须已经是 canonical 值。
///
/// 这个值会被原样写进 manifest 并据以派生落盘 `category`;一旦写出非 canonical 值
@@ -879,104 +879,6 @@ pub(crate) fn validated_local_project_directory_path(
Ok(path.to_path_buf())
}
/// Open the approved Fast GDD Markdown in whatever application the OS has
/// registered for it.
///
/// The GDD is the one product artifact the 立项策划 lane hands back, and it is
/// already on disk — `plan.submit_gdd` renders `game/fast_gdd.md` and the
/// approval receipt re-renders it with the approved header. This command only
/// hands that existing path to the shell; it never creates or rewrites it.
#[tauri::command]
pub(crate) fn open_local_project_plan_gdd_markdown(
app: tauri::AppHandle,
project_path: String,
) -> Result<(), String> {
let path = validated_local_project_plan_gdd_markdown_path(project_path.trim())?;
app.opener()
.open_path(path.to_string_lossy().into_owned(), None::<&str>)
.map_err(|error| format!("打开 Fast GDD 文件失败:{error}"))
}
pub(crate) fn validated_local_project_plan_gdd_markdown_path(
project_path: &str,
) -> Result<PathBuf, String> {
let root = validated_local_project_directory_path(project_path)?;
// `resolve_local_project_path` 是项目内路径的唯一安全入口:它做根校验、相对路径
// 归一化,并逐段拒绝符号链接。这里的相对路径是常量,但仍然走它——GDD 的渲染侧
// `planning_storage`)用的也是同一个解析器,两边对「项目内的这个文件」必须是
// 同一个判定,不能一边解析一边拼字符串。
let path = resolve_local_project_path(&root, PLAN_FAST_GDD_PATH)?;
match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.file_type().is_file() => Ok(path),
Ok(_) => Err("Fast GDD 产物不是普通文件".to_string()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Err("Fast GDD 产物尚未生成,请先完成立项策划审批".to_string())
}
Err(error) => Err(format!("读取 Fast GDD 产物失败:{error}")),
}
}
#[cfg(test)]
mod plan_gdd_markdown_path_tests {
use super::*;
fn fixture() -> tempfile::TempDir {
let temporary = tempfile::tempdir().expect("create GDD path fixture");
crate::project::init_local_game_project_at(
&temporary.path().join("project"),
"gdd-open",
"打开 GDD 产物",
)
.expect("initialize GDD path fixture");
temporary
}
#[test]
fn resolves_the_rendered_markdown_under_the_project_root() {
let temporary = fixture();
let root = temporary.path().join("project");
fs::create_dir_all(root.join("game")).expect("create game directory");
fs::write(root.join(PLAN_FAST_GDD_PATH), "# Fast GDD").expect("render markdown");
let resolved =
validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned())
.expect("resolve rendered markdown");
assert_eq!(resolved, root.join(PLAN_FAST_GDD_PATH));
}
#[test]
fn refuses_to_open_a_markdown_that_has_not_been_rendered_yet() {
// 恢复态下 `plan.submit_gdd` 的 Markdown 渲染可能还没落盘。这时按钮必须给出
// 明确原因,而不是把一个不存在的路径丢给 shell 由系统弹一个无从解释的错误。
let temporary = fixture();
let root = temporary.path().join("project");
let error =
validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned())
.expect_err("missing markdown must fail closed");
assert!(error.contains("尚未生成"), "unexpected error: {error}");
}
#[test]
fn refuses_a_project_path_that_is_not_an_initialized_project() {
let temporary = tempfile::tempdir().expect("create bare fixture");
let error = validated_local_project_plan_gdd_markdown_path(
&temporary.path().to_string_lossy().into_owned(),
)
.expect_err("a directory without .agent is not a project root");
assert!(!error.is_empty());
}
#[test]
fn refuses_a_relative_project_path() {
let error = validated_local_project_plan_gdd_markdown_path("relative/project")
.expect_err("relative project path must fail");
assert!(error.contains("绝对路径"), "unexpected error: {error}");
}
}
#[tauri::command]
pub(crate) async fn get_local_game_manifest(
project_path: String,
@@ -2320,27 +2222,6 @@ pub(crate) fn update_local_project_resource_classification(
)
}
/// 为一批已登记素材追加标签:整批一次校验、一次 manifest 写入、一次 revision 推进。
///
/// 权限位与单素材分类更新同口径取 `asset.register`(命令包装层只做权限门面,
/// 身份 / 写锁 / CAS / 原子写与审计都在 `project/manifest.rs` 内完成)。
/// 这里刻意**不**循环调用单素材命令:逐项调用会写出多份 manifest、推进多次 revision
/// 中途失败还会留下"前几个素材改了、后面的没改"的部分写入。
#[tauri::command]
pub(crate) fn add_local_project_resource_tags(
input: AddLocalProjectResourceTagsInput,
) -> Result<AddLocalProjectResourceTagsResult, String> {
let root = Path::new(input.project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
add_manifest_asset_tags_at(
root,
&input.expected_project_id,
input.expected_project_revision,
input.asset_ids,
input.tags,
)
}
#[tauri::command]
pub(crate) async fn derive_local_project_resource(
input: DeriveLocalProjectResourceInput,
@@ -4892,8 +4773,6 @@ pub(crate) fn prepare_local_project_asset_generation(
image_size: Option<&str>,
asset_name: Option<&str>,
output_path: Option<&str>,
reference_asset_ids: &[String],
target_category: Option<&str>,
) -> Result<LocalProjectAssetGenerationRequest, String> {
let project_path = project_path.trim();
if project_path.is_empty() {
@@ -4901,13 +4780,6 @@ pub(crate) fn prepare_local_project_asset_generation(
}
let asset_kind = normalize_platform_art_asset_generation_kind(kind)
.ok_or_else(|| format!("素材类型不受支持:{}", kind.trim()))?;
// 参考入参只接受当前项目 manifest 素材 id:路径、远端 resourceId 与超限在这里就被拒绝,
// 不把校验推迟到远端(远端只该收到当前账号绑定下的 resource ID)。
let reference_asset_ids =
normalize_platform_art_reference_asset_ids(asset_kind, reference_asset_ids)?;
// GUI 完成登记层参数:入口栏目与生成 kind 不是同一套词汇,只有调用方显式给出目标分类
// 才能把产物原位落回入口栏目。非法值(含 `version` / `all` 这类栏目伪值)直接失败关闭。
let target_category = normalize_platform_art_target_category(target_category)?;
Ok(LocalProjectAssetGenerationRequest {
root: PathBuf::from(project_path),
prompt: local_project_asset_prompt(prompt)?,
@@ -4944,8 +4816,6 @@ pub(crate) fn prepare_local_project_asset_generation(
.then(|| "connected-components".to_string()),
grid_x: None,
grid_y: None,
reference_asset_ids,
target_category,
},
})
}
@@ -4967,10 +4837,6 @@ pub(crate) async fn generate_local_project_asset(
image_size: Option<String>,
asset_name: Option<String>,
output_path: Option<String>,
reference_asset_ids: Option<Vec<String>>,
// 前端 IPC 字段 `targetCategory`:本次生成完成登记时要落盘的正式栏目分类,
// 只走 GUI 命令,取值必须是合法素材分类,Agent / Direct 路径不传。
target_category: Option<String>,
) -> Result<UploadLocalAssetResult, String> {
let request = prepare_local_project_asset_generation(
&project_path,
@@ -4980,8 +4846,6 @@ pub(crate) async fn generate_local_project_asset(
image_size.as_deref(),
asset_name.as_deref(),
output_path.as_deref(),
reference_asset_ids.as_deref().unwrap_or_default(),
target_category.as_deref(),
)?;
enforce_project_permission_policy(&request.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&request.root, "asset.register")?;
@@ -5000,17 +4864,7 @@ mod local_project_asset_generation_tests {
use super::*;
fn prepare(kind: &str, prompt: &str) -> Result<LocalProjectAssetGenerationRequest, String> {
prepare_local_project_asset_generation(
"/tmp/project",
kind,
prompt,
None,
None,
None,
None,
&[],
None,
)
prepare_local_project_asset_generation("/tmp/project", kind, prompt, None, None, None, None)
}
#[test]
@@ -5050,8 +4904,6 @@ mod local_project_asset_generation_tests {
Some("2K"),
Some(" 主角图集 "),
Some(" assets/hero.png "),
&[],
None,
)
.expect("explicit options");
assert_eq!(explicit.root, PathBuf::from("/tmp/project"));
@@ -5079,18 +4931,8 @@ mod local_project_asset_generation_tests {
#[test]
fn invalid_toolbar_arguments_are_rejected_before_any_generation() {
assert_eq!(
prepare_local_project_asset_generation(
"",
"image",
"要求",
None,
None,
None,
None,
&[],
None,
)
.expect_err("empty project path"),
prepare_local_project_asset_generation("", "image", "要求", None, None, None, None)
.expect_err("empty project path"),
"项目路径不能为空"
);
assert_eq!(
@@ -5101,60 +4943,6 @@ mod local_project_asset_generation_tests {
prepare("game-art", "要求").expect_err("unverified kind"),
"素材类型不受支持:game-art"
);
// 目标分类只接受合法素材分类枚举:栏目侧伪值 `version` / `all` 与任意其它值都失败关闭。
for rejected in ["version", "all", "bogus", "UI"] {
assert_eq!(
prepare_local_project_asset_generation(
"/tmp/project",
"image",
"要求",
None,
None,
None,
None,
&[],
Some(rejected),
)
.expect_err("illegal target category"),
format!("目标分类不是合法素材分类:{rejected}")
);
}
// 合法值归一成落盘字符串(trim + kebab-case),供 manifest `category` 直接使用。
assert_eq!(
prepare_local_project_asset_generation(
"/tmp/project",
"image",
"要求",
None,
None,
None,
None,
&[],
Some(" ui-interaction "),
)
.expect("legal target category")
.options
.target_category
.as_deref(),
Some("ui-interaction")
);
assert_eq!(
prepare_local_project_asset_generation(
"/tmp/project",
"image",
"要求",
None,
None,
None,
None,
&[],
None,
)
.expect("omitted target category")
.options
.target_category,
None
);
assert_eq!(
prepare(
"spec",
@@ -5171,9 +4959,7 @@ mod local_project_asset_generation_tests {
Some("4:3"),
None,
None,
None,
&[],
None,
None
)
.expect_err("unsupported ratio"),
"图片比例不受支持:4:3"
@@ -5186,9 +4972,7 @@ mod local_project_asset_generation_tests {
None,
Some("4K"),
None,
None,
&[],
None,
None
)
.expect_err("unsupported size"),
"图片尺寸不受支持:4K"
@@ -5201,9 +4985,7 @@ mod local_project_asset_generation_tests {
None,
None,
Some("\u{7}名字"),
None,
&[],
None,
None
)
.expect_err("control character in asset name"),
"素材名称超出安全边界"
@@ -5216,9 +4998,7 @@ mod local_project_asset_generation_tests {
None,
None,
None,
Some(&"a".repeat(LOCAL_PROJECT_ASSET_MAX_OUTPUT_PATH_CHARS + 1)),
&[],
None,
Some(&"a".repeat(LOCAL_PROJECT_ASSET_MAX_OUTPUT_PATH_CHARS + 1))
)
.expect_err("oversized output path"),
"输出路径超出安全边界"
@@ -1756,8 +1756,8 @@ pub(crate) fn build_static_delegate_structured_result_at(
// (实测:option 对象里多写了一个 `id` 字段)就把整条委派判成投递失败,父
// Supervisor 直接进 needs-reconciliation 停下等人。
//
// 信封格式属于「本次 Provider 输出写错」,不是「durable 权威损坏」,和
// plan.submit_gdd 的业务拒绝同类。降级成 needs-repair 并把解析错误当返工理由
// 信封格式属于「本次 Provider 输出写错」,不是「durable 权威损坏」
// 降级成 needs-repair 并把解析错误当返工理由
// 带上:Supervisor 用既有的一次返工额度就能让子 Agent 重写,不需要人工介入。
let (user_input_questions, user_input_questions_sha256, user_input_parse_error) =
match parse_static_delegate_user_input_request(error) {
@@ -3426,11 +3426,11 @@ mod tests {
assert!(failed.contains("客观证据冲突"));
let mut missing = base.clone();
missing.missing_expected_artifacts = vec!["game/fast_gdd.md".to_string()];
missing.missing_expected_artifacts = vec!["design.md".to_string()];
let missing_error = validate_static_delegate_structured_result(
&missing,
"completed",
&["game/fast_gdd.md".to_string()],
&["design.md".to_string()],
)
.expect_err("missing expected artifact must reject a user revision result");
assert!(missing_error.contains("客观证据冲突"));

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