同步 UI 文档资产共享契约与前端识别

共享契约新增 ui-design-doc 分类及中英文映射

前端编辑器与资源桥接严格识别文档资产

更新技术方案、实施计划和长期决策记录
This commit is contained in:
2026-09-14 20:34:23 +08:00
parent ee7d00c0b1
commit 18654b6806
10 changed files with 160 additions and 28 deletions
@@ -48,7 +48,7 @@ export function findLinkedUiDesignResource(
manifest.assets
.filter(
(asset) =>
asset.kind === 'UI' &&
asset.kind === 'ui-design-doc' &&
asset.mediaType === 'application/json' &&
asset.source.referenceResourceIds?.some((reference) =>
referenceIds.has(reference),
@@ -5040,7 +5040,7 @@ export default function ProjectDevelopmentView({
if (canvasOpenEpochRef.current !== openEpoch) return;
if (
result.manifest.projectId !== manifest.projectId ||
result.asset.kind !== 'UI' ||
result.asset.kind !== 'ui-design-doc' ||
result.asset.mediaType !== 'application/json'
) {
throw new Error('UI 编辑资源结果与当前项目不一致');
@@ -5112,7 +5112,7 @@ export default function ProjectDevelopmentView({
if (uiEditorRoute) return;
const completed = manifest.assets.find(
(asset) =>
asset.kind === 'UI' &&
asset.kind === 'ui-design-doc' &&
asset.mediaType === 'application/json' &&
asset.source.generationKind === 'ui-workflow.completed',
);
@@ -219,8 +219,13 @@ describe('真机出现的 kind 归类口径', () => {
canonical: 'icon',
category: 'ui-interaction',
},
// 现役写入侧的大写字面量与字体 kind,两者都必须落进明确栏目。
// UI 文档与字体 kind,两者都必须落进明确栏目。
{ kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' },
{
kind: 'ui-design-doc',
canonical: 'ui-design-doc',
category: 'ui-interaction',
},
{ kind: 'font', canonical: 'document', category: 'document' },
// `Object.prototype` 上的键不是别名:TS 侧必须用 `Object.hasOwn` 挡住对象字面量的
// 原型命中,Rust 侧 match 字面量本来就落 `image`;这类极端输入两侧也必须一致。
@@ -277,13 +282,13 @@ describe('写侧 kind 字面量 → 分类的端到端口径', () => {
return kinds;
}
test('UI 设计写点仍直接写 `"UI"`,且它落 UI 交互而不是待归类', () => {
// 8 条真机 UI 资产是 `kind:"UI"` + `mediaType:"application/json"`
// 该 kind 不在别名表里时派生结果也是 unclassified,读时自愈同样救不回来。
test('UI 设计写点统一使用 ui-design-doc 常量,且它落 UI 交互而不是待归类', () => {
for (const file of UI_EDITOR_WRITE_SITES) {
expect(registeredKindLiterals(file)).toContain('UI');
expect(readFileSync(file, 'utf8')).toContain('UI_DESIGN_DOC_ASSET_KIND');
}
expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction');
expect(gameCreationAppAssetCategoryForKind('ui-design-doc')).toBe(
'ui-interaction',
);
});
test('UI 编辑器写点写出的每个 kind 都落明确栏目', () => {
@@ -0,0 +1,51 @@
# 【实施计划】UI设计文档引用代码上下文
| 字段 | 值 |
| --- | --- |
| Version | 1 |
| Status | ready |
| Owner | Codex |
| Milestone | `docs/project-memory/plans/【里程碑】UI设计文档引用代码上下文-2026-09-14.md` |
## 修改边界
允许修改:
- UI Editor persistence/resource bridge/workflow/command 中的 UI JSON 文档 kind 常量与校验。
- `packages/shared` 的 canonical kind 与 Rust 对应映射。
- `agent/direct_codex_references.rs` 的 UI 引用 prompt 生成。
- 相关 Rust、TypeScript 测试和当前 UI workflow/AGC 文档。
- `decision-log.md` 的长期决策记录。
明确不修改:
- `ui-prototype` 图片生成和图片 workflow 语义。
- UI State schema、`render_ui_design_state_js` 输出格式和 `ui/generated-*.js` 路径规则。
- SpacetimeDB schema、External v1 OpenAPI、非 Direct Codex Supervisor 引用行为。
- 用户已有 `.env` 未提交修改。
## 实现顺序
1. 更新 UI workflow 主规范与项目决策,明确 `ui-design-doc``ui-prototype` 的身份边界。
2. 在 persistence 提取并导出 UI 文档 kind/media 常量,替换 Rust UI 文档校验。
3. 同步所有 UI JSON 资源生产者、workflow 校验、命令筛选、shared contract 与前端 bridge。
4. 在 Direct Codex 引用渲染中调用 `generate_ui_design_code_at`;成功追加生成文件相对路径,失败追加原始错误并继续发送。
5. 补充旧 kind、图片、成功、文档错误、多引用和 renderer 输出的测试。
6. 执行定向验证,复核 diff 中无 fallback、迁移或无关 `.env` 修改。
## 验证命令
1. `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check`
2. `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml ui_editor::persistence`
3. `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_codex_references`
4. 相关 shared contract Vitest 测试与前端类型检查
5. `npm run check:doc-index`
6. `npm run check:encoding`
7. `git diff --check`
## 风险与回滚点
- `ui-prototype` 与 UI JSON 文档共用部分分类展示逻辑,需确保分类映射不会让图片进入文档分支。
- `generate_ui_design_code_at` 会持有项目写锁;多引用必须顺序调用,不能并行写同一项目。
- 生成失败继续发送是已确认语义;测试必须证明错误文本进入 prompt 而不是被转成聊天失败。
- 若发现生产者仍写入旧 `UI`,按开发阶段合同直接修生产者和 fixture,不增加运行时兼容分支。
@@ -0,0 +1,49 @@
# 【里程碑】UI设计文档引用代码上下文
| 字段 | 值 |
| --- | --- |
| Version | 1 |
| Status | in-progress |
| Date | 2026-09-14 |
| Parent Spec | `docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md` |
## 目标
将可编辑 UI JSON 文档统一识别为 `ui-design-doc`,并在 Direct Codex 聊天引用该文档时复用 UI Editor 现有代码导出流程,为 LLM 提供生成的、带文档注释的 JS 代码路径或原始生成错误。
## 范围
- UI JSON 文档资产身份固定为 `kind=ui-design-doc``mediaType=application/json`
- 复用 `ui_editor::persistence::generate_ui_design_code_at` 读取并校验文档、调用 `render_ui_design_state_js`、写入 `ui/generated-*.js`
- Direct Codex 引用文本保留稳定 manifest 元数据,并追加代码路径或 `生成代码遇到错误{error}`
- 同步 Rust/TypeScript 的 UI 文档生产者、消费者、分类映射和测试。
- `ui-prototype` 图片继续作为独立源图片类型。
## 不在范围内
- 不接受旧 `UI``ui``ui-design` 作为 UI 文档类型。
- 不增加兼容 fallback、数据迁移或旧数据转换逻辑。
- 不把 `ui-prototype` 图片当作 UI 文档,不从图片生成替代 JSON 文档代码。
- 不改变 UI State schema、renderer 输出格式、项目 revision 或 manifest 阶段。
- 不改变旧 Supervisor 非 Direct Codex 引用链路。
## 依赖与前置条件
- `generate_ui_design_code_at` 已存在并返回 `relative_path`
- `render_ui_design_state_js` 已生成包含文档注释的 JS 模块。
- Direct Codex 结构化引用已传递 `resourceId`manifest 是资源身份权威。
## 验收标准
- [ ] UI 编辑器 JSON 资源创建、加载、保存、workflow 校验和引用分支均只接受 `ui-design-doc + application/json`
- [ ] Direct Codex 成功引用 UI 文档时,prompt 追加 `请先阅读生成的带有文档的代码片段: {relative_path}`
- [ ] UI 文档生成失败时,prompt 保留原 metadata 并追加 `生成代码遇到错误{error}`,不走图片或其它文件 fallback。
- [ ] `ui-prototype` 图片引用不触发 UI 文档代码生成。
- [ ] 多个引用按顺序处理,单个 UI 文档失败不丢失其它引用。
- [ ] Rust/TypeScript kind 映射一致,旧 kind 不被隐式迁移。
## 证据要求
- 自动化:persistence、direct_codex_references、shared contract 定向测试;`cargo fmt --check``npm run check:encoding``git diff --check`
- 运行时:不要求真实 Provider;测试验证生成文件路径、renderer 文档注释和 prompt 注入结果。
- 边界:旧 kind、图片 kind、损坏 JSON、renderer 错误、多引用和路径来源均有测试。
@@ -8677,3 +8677,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 决策:复用判据收窄为**同一条写调用链(同一线程)重入**——按锁路径登记真实持锁线程,只有当前线程就是持锁线程时才返回 advisory guard;本进程其它线程的争用继续走有界等待与终态占用。自主游戏构建流水线的并行专家动作豁免保持不变;跨进程占用、残留回收、权限分类、等待预算和错误文案不变。
- 边界:锁定这些不变量的既有用例(`project_tools` / `command_runtime` / `parallel_actions` / `runtime_state` / `response_stream` / `direct_tool_bridge` / `ui_editor::persistence`)不得为了让锁语义通过而改写;用「同线程自持锁」模拟「另一个写者」的两条用例改为**在另一条线程持锁**,断言语义不变。同进程跨线程重入(持锁链在 `await` / `spawn_blocking` 后于其它线程再取锁)仍会等满预算,出现现场时按 2026-08-27 的既有处置改用 `*_locked` 入口,不放宽判据。
- 关联文档:[项目客户端占用锁收敛里程碑](../plans/【里程碑】项目客户端占用锁收敛-2026-09-14.md)、[踩坑记录](pitfalls.md)。
## 2026-09-14 Direct Codex 引用 UI 设计文档生成代码上下文
- 决策:UI Editor JSON 文档资产唯一使用 `kind:"ui-design-doc"``mediaType:"application/json"``ui-prototype` 保持图片语义,旧 `UI` / `ui` / `ui-design` 不作为该文档分支输入,不做 fallback 或迁移。
- 决策:Direct Codex 结构化资源引用命中该 kind 时,顺序调用 UI Editor persistence 的 `generate_ui_design_code_at`,生成 `ui/generated-*.js`,并把 `请先阅读生成的带有文档的代码片段: {relative_path}` 追加到当前 prompt。生成失败不阻断本轮引用,追加原始 `生成代码遇到错误{error}`,其它引用继续处理。
- 原因:复用 `html_renderer/mod.rs` 统一产物,确保 LLM 读取的代码包含 UI 节点元数据和文档注释;严格 kind + mediaType 判定避免图片资产误走代码生成。
@@ -1221,7 +1221,7 @@ game-project/
## 2026-08-17 UI Editor 项目资源 State 持久化
- UI 编辑器复用现有 manifest `kind: "UI"``mediaType: "application/json"` 资源,不增加平行 asset kind。资源文件固定为严格 `game-creator-ui-design-state.v1` JSON envelope`projectId``assetId`、每资源 `revision` 和 Rust 唯一源 `State`;旧空对象、未知字段、身份错配、超限、无效内部引用和不安全相对路径均失败关闭。内部引用校验同时覆盖 `Image.target_graphic -> sprite_assets``Text.font -> font_assets`,可选引用非空时必须命中同一 State 内已登记资源。
- UI 编辑器使用唯一的 manifest `kind: "ui-design-doc"``mediaType: "application/json"` 资源,不接受旧 `UI` / `ui` / `ui-design`,也不增加 fallback 或迁移。资源文件固定为严格 `game-creator-ui-design-state.v1` JSON envelope`projectId``assetId`、每资源 `revision` 和 Rust 唯一源 `State`;旧空对象、未知字段、身份错配、超限、无效内部引用和不安全相对路径均失败关闭。内部引用校验同时覆盖 `Image.target_graphic -> sprite_assets``Text.font -> font_assets`,可选引用非空时必须命中同一 State 内已登记资源。
- Tauri 专用 load/save command 只接受项目路径、期望项目 ID、manifest asset ID 和(保存时)资源 revisionRust 按 manifest 解析受控本地路径并在项目写锁内做 CAS。相同 `State` 返回 unchanged 且不推进 project revision;不同内容安装并回读一致后才推进 revision,后续推进失败返回 `reconciliation-required`,不伪装为完整保存。
- UI 编辑器代码导出仅写入用户项目目录下的 `ui/generated-*.js` 派生文件,绝不推进项目 revision、UI State revision、manifest 阶段或 Runtime 验证门;写入失败只返回生成错误,不得把生成文件写入冒充为项目 mutation。
- UI State 原子安装保留最近一个可解析、canonical 的 `.previous` 恢复候选,作为最佳努力恢复来源;写入主文件前不把完整 State 语义校验重复执行一遍。主文件损坏时,恢复候选仍必须通过同一严格 schema、project/asset identity、revision、引用和 State 校验后才能安装;恢复安装与保存共用项目写锁,并在持锁后重新读取主文件,已有并发保存的有效新版本时直接返回而不安装旧副本。任一候选均不可信则停在加载错误,前端禁编辑和保存。新建 UI 资源先登记并安装合法 envelope,任一步失败补偿 manifest/文件,避免把空 JSON 留给资源卡。
@@ -2,10 +2,12 @@
## 目标
`ui-prototype``UI` 是两种不同资源,不能通过修改投影 `subtype` 混为一种资源:
`ui-prototype``ui-design-doc` 是两种不同资源,不能通过修改投影 `subtype` 混为一种资源:
- `ui-prototype`:Agent 生成并登记到画布的界面设计图片。
- `UI`UI 编辑器使用的 JSON 资源,保存界面图、UI 树、组件绑定和 State revision。
- `ui-design-doc`UI 编辑器使用的 `application/json` 资源,保存界面图、UI 树、组件绑定和 State revision。
UI 设计文档资产必须严格满足 `kind=ui-design-doc``mediaType=application/json`;旧 `UI``ui``ui-design` 不作为兼容输入,也不提供迁移或图片 fallback。
自然语言生成链路必须把前者桥接为后者,并持续让 manifest 成为客户端资源投影的权威来源。游戏场景的页面清单由 Runtime 自动发现,Agent 不得凭空猜测页面。
@@ -35,7 +37,7 @@ Agent 通过白名单工具 `ui.workflow.run` 发起工作流。项目路径由
处理规则:
1. `prepare` 为每个页面创建确定性的 `kind=UI` JSON 资源,引用源 `ui-prototype` 和页面设计图,载入设计图尺寸与相对路径到 `ui_design_images`,并保存 State。
1. `prepare` 为每个页面创建确定性的 `kind=ui-design-doc` JSON 资源,引用源 `ui-prototype` 和页面设计图,载入设计图尺寸与相对路径到 `ui_design_images`,并保存 State。
2. `recognize` 依次执行 Provider 多模态结构识别、现有多树合并器、最多每批 5 项的图片/图标组件绑定,并把已登记字体的安全元数据提供给绑定器;阶段分别持久化为 `structure-ready``merge-ready``binding-ready`,重复执行从最近真实阶段恢复。
3. `status` 只回读 State、页面阶段和 blockers,不推进项目 revision。
4. `finalize` 只接受 `game/` 下的真实 UTF-8 文件,写入与 UI State revision 绑定的应用标记;所有页面通过应用门禁后才返回 `asset-separation` 最终阶段路由。缺少页面、资源、组件或应用标记时拒绝伪造完成。
@@ -61,15 +63,33 @@ UI 编辑器的分离提示会对节点描述和返工备注做长度与控制
真实 Provider 鉴权失败时,Codex app-server 可能只返回 `codexErrorInfo=other`,而把上游 `401/403` 放在错误正文中。Runtime 必须从受控错误字段识别为 `codex-app-server-error:unauthorized`(公共摘要为 `codex-app-server-unauthorized`),只向公共运行记录暴露错误类别和指纹,不记录 Token 或上游原文。此错误不能伪造为 UI 工作流阶段完成;修复凭据后应从原有 run 的恢复边界重新执行。
## UI 设计文档代码引用
Direct Codex 聊天引用 `ui-design-doc` 时,复用 `ui_editor::persistence::generate_ui_design_code_at`。该入口只读取被引用的 JSON 文档,调用 `render_ui_design_state_js`,并写入既有 `ui/generated-*.js` 派生文件。
成功时在稳定 manifest 元数据后追加:
```text
请先阅读生成的带有文档的代码片段: {relative_path}
```
JSON 文档读取、State 校验或 renderer 失败时,在同一引用后追加原始错误:
```text
生成代码遇到错误{error}
```
错误不阻断本轮 Direct Codex prompt,其它引用继续按顺序处理;不从图片或其它文件 fallback,不把生成文件登记为正式 manifest 资产,也不推进项目 revision。
## 画布跳转与客户端更新
点击画布中的 `ui-prototype` 时,工作台调用 `ensure_ui_design_resource_for_prototype`
- 按 manifest asset id、`source.resourceId``source.assetObjectId` 识别已有关联,避免重复创建。
- 没有关联时原子创建 `ui/UI 设计 N.json`,登记 `kind=UI``application/json`,并把原型图作为首张页面设计图载入 State。
- 没有关联时原子创建 `ui/UI 设计 N.json`,登记 `kind=ui-design-doc``application/json`,并把原型图作为首张页面设计图载入 State。
- 成功后通过 `onManifestChange` 更新客户端资源投影,再打开 UI 编辑器;普通桥接从 `reference-analysis` 开始。
点击已有 `UI` 资源直接打开 UI 编辑器。若 manifest 阶段为 `ui-workflow.completed`,工作台自动打开该资源的 `asset-separation` 阶段(最远步骤为 2),交给用户做最终检查和手动调整。
点击已有 `ui-design-doc` 资源直接打开 UI 编辑器。若 manifest 阶段为 `ui-workflow.completed`,工作台自动打开该资源的 `asset-separation` 阶段(最远步骤为 2),交给用户做最终检查和手动调整。
自动切分达到返工上限的 problematic 节点会随分离 DTO 返回每节点的 `problem_history`,并由编辑器回写为 `component_status = NeedReview(...)``SeparationOverview` 只读取 UI State 中的状态来计数和定位;该结果仍按“已尽力完成”报告成功并执行既有 finalize,剩余节点由用户在概览定位后手动处理或再次发起分离。
@@ -480,6 +480,12 @@ export interface GameCreationAppAssetManifestEntry {
tags?: string[];
}
/** UI Editor JSON 文档资产的唯一 kind 与媒体类型。 */
export const GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND =
'ui-design-doc' as const;
export const GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE =
'application/json' as const;
export const GAME_CREATION_APP_CANONICAL_ASSET_KINDS = [
'image',
'scene',
@@ -489,6 +495,7 @@ export const GAME_CREATION_APP_CANONICAL_ASSET_KINDS = [
'icon-spritesheet',
'icon-spec',
'ui-design',
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
'publication-material',
'spec',
'video',
@@ -542,12 +549,7 @@ const GAME_CREATION_APP_LEGACY_ASSET_KINDS: Record<
export function canonicalGameCreationAppAssetKind(
value: string,
): GameCreationAppCanonicalAssetKind {
/**
* kind 词汇大小写不敏感:UI 设计资产的现役写入侧写的是**大写** `"UI"`
* `src-tauri/src/ui_editor/resource_bridge.rs`、`workflow.rs`、`persistence.rs`)。
* 只按小写收口会让它落 `image` 兜底、再经 `image → unclassified` 永远停在「待归类」,
* 且派生值本身就是 `unclassified`,读时自愈也救不回来。
*/
/** kind 词汇大小写不敏感;UI Editor 文档写入侧统一使用 `ui-design-doc`。 */
const normalized = value.trim().toLowerCase();
/**
* `kind` 是外部输入,可能正好是 `Object.prototype` 上的键(`constructor` / `__proto__` /
@@ -594,6 +596,7 @@ export const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: Record<
'icon-spritesheet': 'ui-interaction',
'icon-spec': 'ui-interaction',
'ui-design': 'ui-interaction',
'ui-design-doc': 'ui-interaction',
'publication-material': 'unclassified',
spec: 'document',
video: 'unclassified',
@@ -565,7 +565,7 @@ impl<'de> Deserialize<'de> for GameCreationAppAssetManifestEntry {
}
}
pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [
pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 17] = [
"image",
"scene",
"character",
@@ -574,6 +574,7 @@ pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [
"icon-spritesheet",
"icon-spec",
"ui-design",
"ui-design-doc",
"publication-material",
"spec",
"video",
@@ -585,11 +586,7 @@ pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [
];
pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str {
// kind 词汇大小写不敏感UI 设计资产的现役写入侧写的是**大写** `"UI"`
// `apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs` 的
// `register_local_asset_at(..., "UI", ...)``workflow.rs` / `persistence.rs` 同),
// 只按小写收口会让它落到 `image` 兜底、再经 `image -> unclassified` 永远停在
// 「待归类」;且派生值本身就是 unclassified,读时自愈也救不回来。
// kind 词汇大小写不敏感UI Editor 文档写入侧统一使用 `ui-design-doc`。
// TS 侧 `canonicalGameCreationAppAssetKind` 用同一口径,由
// `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` 交叉钉住。
let normalized = value.trim().to_lowercase();
@@ -640,7 +637,7 @@ pub const GAME_CREATION_APP_ASSET_CATEGORIES: [GameCreationAppAssetCategory; 6]
];
/// canonical kind 到功能分类的默认映射,必须穷举 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS`。
pub const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: [(&str, GameCreationAppAssetCategory); 16] = [
pub const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: [(&str, GameCreationAppAssetCategory); 17] = [
("image", GameCreationAppAssetCategory::Unclassified),
("scene", GameCreationAppAssetCategory::Scene),
("character", GameCreationAppAssetCategory::Character),
@@ -655,6 +652,7 @@ pub const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: [(&str, GameCreationAppAsset
),
("icon-spec", GameCreationAppAssetCategory::UiInteraction),
("ui-design", GameCreationAppAssetCategory::UiInteraction),
("ui-design-doc", GameCreationAppAssetCategory::UiInteraction),
(
"publication-material",
GameCreationAppAssetCategory::Unclassified,