Compare commits

..

26 Commits

Author SHA1 Message Date
k88936 1e992bcdf8 扩大未知 JavaScript 异常上报原则
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
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
明确所有未解决和非契约化 JS 异常必须继续抛出

说明用户 fallback 不得替代全局 error report 链路
2026-09-14 19:47:08 +08:00
k88936 8368aa262c 补充剪贴板异常上报约定
记录 UI fallback 后仍需抛出原始原生错误

明确未知异常必须进入 error report 链路
2026-09-14 19:43:08 +08:00
k88936 e9ddbf16da 重构预览网格步长计算
提取预览网格自适应步长到独立模块

补充缩放网格计算与背景样式测试

同步预览画布缩放交互文档
2026-09-14 19:40:13 +08:00
k88936 22ac1f4c0b 修复 UI 编辑器剪贴板写入权限
为主窗口补充 clipboard-manager 写文本权限

保留手动复制提示并继续抛出原始 Tauri 剪贴板错误
2026-09-14 19:39:34 +08:00
k88936 9910a0eec0 优化 UI 编辑器复制反馈布局
将复制失败提示与操作按钮分离排列
2026-09-14 19:06:15 +08:00
k88936 70981b9ca9 完善 UI 编辑器结果弹窗覆盖
增加普通保存成功与保存后生成失败的场景断言

验证路径弹窗和失败重试按钮的可见行为
2026-09-14 19:01:44 +08:00
k88936 8a0d5600b3 拆分 UI 编辑器路径复制按钮
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m27s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m23s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m5s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m8s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m15s
Project CI / Frontend tests (pull_request) Failing after 3m22s
Project CI / Repository checks (pull_request) Successful in 3m38s
Project CI / Native shell tests (pull_request) Successful in 6m42s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m12s
Project CI / Backend tests (pull_request) Successful in 8m24s
将剪贴板状态与写入逻辑提取为独立小组件

保持保存结果弹窗只负责结果布局与动作编排
2026-09-14 18:59:51 +08:00
k88936 184d88dbb8 修复多树预览受左侧选中图片限制
移除 activeImageId 对预览拖动和缩放手势的门控

按树 wrapper 提供几何上下文并允许任意树操作

保留 activeImageId 仅用于左侧图片与图片级检查
2026-09-14 18:59:51 +08:00
k88936 791794c0a6 记录 UI 编辑器保存结果弹窗约定
补充保存与代码生成结果弹窗及相对路径复制行为

记录复制失败、重试和保存并返回边界
2026-09-14 18:59:51 +08:00
k88936 9cfb47d945 补充 UI 编辑器保存结果测试
覆盖保存和生成成功弹窗与项目相对路径复制

覆盖保存失败、冲突和剪贴板失败反馈
2026-09-14 18:59:51 +08:00
k88936 27859d3e19 接入 UI 编辑器保存结果弹窗
让普通保存和保存并生成流程返回结构化结果

将成功与失败反馈接入独立弹窗并保留保存并返回直返行为
2026-09-14 18:59:51 +08:00
k88936 43f3780a38 新增 UI 编辑器保存结果弹窗
新增保存成功、代码生成成功和失败状态弹窗

提供项目相对路径复制按钮及复制失败提示
2026-09-14 18:59:51 +08:00
k88936 a1f9149c27 UI编辑器移除预览模式残留类型
完成三开关替代 render mode 的类型清理
2026-09-14 18:59:51 +08:00
k88936 f0bba18841 UI编辑器收口多树上下文菜单目标
预览菜单携带所属树 ID

避免非当前图片树的结构操作写错目标
2026-09-14 18:59:51 +08:00
k88936 f679cfa179 UI编辑器完善多树创建与文档契约
统一识别和合并结果的 createTree 流程

按选中节点所属树驱动 Inspector 编辑

补充多树预览与树级偏移权威文档
2026-09-14 18:59:50 +08:00
k88936 f68ffcfecd UI编辑器补充合并树偏移构造
为合并产出的节点补齐 NodeOffset
2026-09-14 18:59:50 +08:00
k88936 ec94a4fe00 UI编辑器重构多树预览与独立开关
预览画布同时渲染全部界面树并支持树级拖动

拆分显示框线、原图和组件开关

预览节点选择与左侧当前图片解耦
2026-09-14 18:59:50 +08:00
k88936 46f8592375 UI编辑器补充树级偏移模型
新增 Node offset min/max 数据结构

统一新树默认横向布局入口

增加树级偏移状态更新操作与实施计划
2026-09-14 18:59:50 +08:00
k88936 cdf2882302 补齐自动切分概览字体属性
向 SeparationOverview 传入 fonts 数据

修复 UI 编辑器 TS2741 类型错误
2026-09-14 18:59:50 +08:00
k88936 d156113e71 修复 UI 编辑器待办循环高亮动画
修复受控树选中回调清除 Inspector 高亮的问题

循环游标同时使用界面树与节点 ID

补充多节点循环和动画重启回归测试
2026-09-14 18:59:50 +08:00
k88936 a8f2ac17be 注释掉 InspectorSidebar 中的 LayoutEditor 组件以暂时禁用布局编辑功能 2026-09-14 18:59:50 +08:00
k88936 43ac9a5761 补充 UI 编辑器交互回归测试
从实际节点事件目标验证删除快捷键

验证 portal 右键菜单启用后会执行操作并关闭
2026-09-14 18:59:50 +08:00
k88936 e992e0b35c 压缩 UI 编辑器概览布局
将识别概览指标改为四列横向排列

将素材切分概览改为四列网格以减少纵向高度
2026-09-14 18:59:50 +08:00
k88936 e389b54a3a 修复 UI 预览画布交互
为无限视口添加跟随平移和缩放的点阵背景

恢复画布快捷缩放与初始适配比例控制

保留节点平移选择并屏蔽浏览器原生拖拽选择
2026-09-14 18:59:50 +08:00
k88936 84c780a6f8 重构 UI 编辑器全屏外壳
通过 portal 将资源编辑器挂载到 body 并覆盖标题栏以下区域

移除外层 FocusTrap,保留内部确认对话框的模态行为

统一左右浮动面板的顶部与底部留白
2026-09-14 18:59:50 +08:00
k88936 03f900fbe5 布局重构:
- 预览改为全区域背景层。
  - 左右面板、顶部流程条改为覆盖层,可遮挡预览。
  - 左右面板开合状态仅保存在当前 React 会话。
  - 增加左右面板“收起/展开”入口。
  - 编辑器关闭入口改为单独的 ×,继续通过 onBack 注入关闭逻辑。
  - 保留全局顶部栏与现有快捷键、业务状态、保存流程。
  - 底层页面仍由现有父级路由控制,不新增 URL 状态。
2026-09-14 18:59:50 +08:00
331 changed files with 6385 additions and 32117 deletions
@@ -32,7 +32,6 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access. - Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either. - Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent. - Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
- Icon spritesheet generation accepts `sliceMode="connected-components"` (default alpha-connectivity detection) or `sliceMode="grid"`. Grid mode requires `gridX` and `gridY` (1-32); use `sliceCount` only to constrain connected-component output.
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs. - For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record. - Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
@@ -52,7 +52,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | | Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` | | Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | | Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceMode`, `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` | | UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
@@ -94,7 +94,7 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL. The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
`sliceMode` controls atlas splitting. Use `"connected-components"` (default) to detect independent opaque regions by alpha connectivity, or `"grid"` with positive `gridX` and `gridY` values (maximum 32 each). `sliceCount` optionally constrains the connected-component result. `sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
## Common Values ## Common Values
@@ -79,9 +79,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context. 1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`. 2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For a fixed four-category game contract it may send `sliceMode: "grid"`; for free-form assets use `sliceMode: "connected-components"` (the default). 3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. When using the fixed four-category contract, require response `sliceMode: "grid"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation. For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG. Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
@@ -881,14 +881,12 @@ def _self_test() -> None:
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"], ["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
canvasSession=session, canvasSession=session,
assetLabel="贪吃蛇透明图集", assetLabel="贪吃蛇透明图集",
sliceMode="connected-components",
referenceId="must-not-override-explicit-reference", referenceId="must-not-override-explicit-reference",
iconDescriptions=["不得覆盖显式图标描述"], iconDescriptions=["不得覆盖显式图标描述"],
) )
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations" assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
assert calls[0]["body"]["referenceId"] == "editor-resource-spec" assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
assert calls[0]["body"]["screenColor"] == "auto" assert calls[0]["body"]["screenColor"] == "auto"
assert calls[0]["body"]["sliceMode"] == "connected-components"
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上" assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo" assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
print("self-test ok") print("self-test ok")
+5 -10
View File
@@ -1,8 +1,8 @@
# Server-side OpenAI-compatible LLM endpoint base URL. # Server-side OpenAI-compatible LLM endpoint base URL.
LLM_BASE_URL="https://api.tiantoken.com/v1" LLM_BASE_URL="https://api.vectorengine.cn/v1"
# Server-side API key used by the local Vite proxy. # Server-side API key used by the local Vite proxy.
# Recommended: set `LLM_API_KEY` locally, or use `TIANTOKEN_API_KEY` # Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
# through the Rust api-server proxy. # through the Rust api-server proxy.
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy, # Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
# but it should not be relied on by browser code. # but it should not be relied on by browser code.
@@ -122,7 +122,7 @@ WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
# Model name for chat completions. # Model name for chat completions.
VITE_LLM_MODEL="gpt-5.4-mini" VITE_LLM_MODEL="gpt-5.4-mini"
GENARRATIVE_LLM_PROVIDER="openai-compatible" GENARRATIVE_LLM_PROVIDER="openai-compatible"
GENARRATIVE_LLM_BASE_URL="https://api.tiantoken.com/v1" GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
GENARRATIVE_LLM_API_KEY="" GENARRATIVE_LLM_API_KEY=""
GENARRATIVE_LLM_MODEL="gpt-5.4-mini" GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
@@ -130,15 +130,10 @@ GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1" DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY" DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
# Tiantoken LLM and GPT-image-2 / Gemini image generation config. # VectorEngine LLM and GPT-image-2 / Gemini image generation config.
TIANTOKEN_BASE_URL="https://api.tiantoken.com"
TIANTOKEN_API_KEY=""
TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS="1000000"
# VectorEngine is retained for Suno audio generation only.
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn" VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
VECTOR_ENGINE_API_KEY="" VECTOR_ENGINE_API_KEY=""
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS="180000" VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
# ElevenLabs editor sound-effect generation is server-side only. # ElevenLabs editor sound-effect generation is server-side only.
ELEVENLABS_BASE_URL="https://api.elevenlabs.io" ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
-1
View File
@@ -164,7 +164,6 @@ module.exports = {
'server-rs/target-*', 'server-rs/target-*',
'apps/desktop-shell/src-tauri/target', 'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**', 'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target', 'target',
'src/main.tsx', 'src/main.tsx',
'src/App.tsx', 'src/App.tsx',
-1
View File
@@ -57,7 +57,6 @@
"react-colorful": "^5.8.0", "react-colorful": "^5.8.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"vite": "^6.2.0", "vite": "^6.2.0",
"zustand": "^5.0.14" "zustand": "^5.0.14"
@@ -113,11 +113,6 @@ const allowedUncalledTauriCommands = [
'chat_with_game_creator_agent', 'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage', 'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource', 'create_ui_design_resource',
// 图片类生成的同步变体:GUI 已改为 `start_local_project_asset_generation` + 项目内任务账本
// (提交即返回、后台生成)。这条命令**没有生产调用方**,只有 Rust 集成测试
// `src/tests/project.rs`)与 `commands.rs` 单测在调;待后续批次删除,或改为转调
// `start_local_project_asset_generation`。
'generate_local_project_asset',
'open_game_creator_launcher_window', 'open_game_creator_launcher_window',
'open_game_creator_workspace_window', 'open_game_creator_workspace_window',
'read_direct_project_conversation', 'read_direct_project_conversation',
@@ -1295,7 +1290,7 @@ for (const requiredSource of [
} }
} }
if (tauriConfig.productName !== '陶泥儿') { if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
throw new Error('AI game creator shell productName drifted'); throw new Error('AI game creator shell productName drifted');
} }
@@ -1307,19 +1302,19 @@ const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent', 'design-agent': 'design-agent',
}; };
const expectedBundledWindowsResources = { const expectedBundledWindowsResources = {
'resources/codex/win-x64/bin/codex.exe': 'coding-agent/win-x64/bin/codex.exe', 'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe': 'resources/codex/win-x64/bin/codex-code-mode-host.exe':
'coding-agent/win-x64/bin/codex-code-mode-host.exe', 'codex/win-x64/bin/codex-code-mode-host.exe',
'resources/codex/win-x64/codex-path/rg.exe': 'resources/codex/win-x64/codex-path/rg.exe':
'coding-agent/win-x64/codex-path/rg.exe', 'codex/win-x64/codex-path/rg.exe',
'resources/codex/win-x64/codex-resources/codex-command-runner.exe': 'resources/codex/win-x64/codex-resources/codex-command-runner.exe':
'coding-agent/win-x64/codex-resources/codex-command-runner.exe', 'codex/win-x64/codex-resources/codex-command-runner.exe',
'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe': 'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe':
'coding-agent/win-x64/codex-resources/codex-windows-sandbox-setup.exe', 'codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe',
'resources/codex/win-x64/codex-package.json': 'resources/codex/win-x64/codex-package.json':
'coding-agent/win-x64/codex-package.json', 'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md', 'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json', 'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins', 'resources/plugins': 'plugins',
}; };
assert.deepEqual( assert.deepEqual(
@@ -9,10 +9,6 @@ import {
const agcDevHost = '127.0.0.1'; const agcDevHost = '127.0.0.1';
const legacyAgcDevPort = 3080; const legacyAgcDevPort = 3080;
const agcVitePortEnvKey = 'GENARRATIVE_AGC_VITE_PORT'; const agcVitePortEnvKey = 'GENARRATIVE_AGC_VITE_PORT';
const agcAdminWebHost = '127.0.0.1';
const legacyAgcAdminWebPort = 3102;
// 与 scripts/dev.mjs 的后台 Web 端口配置保持同一环境变量名。
const agcAdminWebPortEnvKey = 'ADMIN_WEB_PORT';
function readConfiguredAgcDevPort(env = process.env) { function readConfiguredAgcDevPort(env = process.env) {
const rawPort = String(env[agcVitePortEnvKey] ?? '').trim(); const rawPort = String(env[agcVitePortEnvKey] ?? '').trim();
@@ -103,100 +99,13 @@ function withAgcDevEndpointEnv(endpoint, env = process.env) {
}; };
} }
function readConfiguredAgcAdminWebPort(env = process.env) {
const rawPort = String(env[agcAdminWebPortEnvKey] ?? '').trim();
if (!rawPort) {
return null;
}
const port = normalizePort(rawPort, -1);
if (port < 1024) {
throw new Error(`${agcAdminWebPortEnvKey} 必须是 1024-65535 的有效端口`);
}
return port;
}
function createAgcAdminWebEndpoint(port, portRange = null) {
const origin = `http://${agcAdminWebHost}:${port}`;
return {
host: agcAdminWebHost,
port,
origin,
basePath: '/admin/',
url: `${origin}/admin/`,
portRange,
};
}
// AGC 开发态的后台 Web 与 `npm run dev` 的后台 Vite 共用同一套优先端口约定:
// Linux 取当前用户端口段的 `start + 3` 槽位,非 Linux 保留 `3102` 兼容首选并允许统一漂移。
async function resolveAgcAdminWebEndpoint({
env = process.env,
platform = process.platform,
strictConfigured = false,
reservedPorts = [],
reservePortRange = reserveLinuxDevPortRange,
findPort = findAvailablePort,
} = {}) {
const configuredPort = readConfiguredAgcAdminWebPort(env);
let portRange = null;
let preferredPort = configuredPort ?? legacyAgcAdminWebPort;
if (platform === 'linux') {
const allocation = await reservePortRange({ env });
if (!allocation?.range) {
throw new Error('无法取得当前 Linux 用户的 dev 端口段');
}
portRange = allocation.range;
const mappedAdminWebPort = mapDevPortsToPortRange(portRange)?.adminWebPort;
if (!Number.isInteger(mappedAdminWebPort)) {
throw new Error(
`当前 Linux dev 端口段 ${portRange.label} 缺少后台 Web 槽位;请先迁移为至少 6 个端口且不与其它用户重叠的端口段`,
);
}
preferredPort = configuredPort ?? mappedAdminWebPort;
}
const reservedPortSet = new Set(
reservedPorts.filter((value) => Number.isInteger(value) && value > 0),
);
const port = await findPort({
host: agcAdminWebHost,
preferredPort,
portRange,
reservedPorts: reservedPortSet,
strict: strictConfigured && configuredPort != null,
});
console.log(
formatPortDecision({
name: 'ai-game-creator-shell-admin-web',
host: agcAdminWebHost,
preferredPort,
resolvedPort: port,
}),
);
if (portRange) {
console.log(
`[ai-game-creator-shell] admin-web port-range: ${portRange.label}`,
);
}
return createAgcAdminWebEndpoint(port, portRange);
}
export { export {
agcAdminWebHost,
agcAdminWebPortEnvKey,
agcDevHost, agcDevHost,
agcVitePortEnvKey, agcVitePortEnvKey,
createAgcAdminWebEndpoint,
createAgcDevEndpoint, createAgcDevEndpoint,
legacyAgcAdminWebPort,
legacyAgcDevPort, legacyAgcDevPort,
readAgcDevEndpoint, readAgcDevEndpoint,
readConfiguredAgcAdminWebPort,
readConfiguredAgcDevPort, readConfiguredAgcDevPort,
resolveAgcAdminWebEndpoint,
resolveAgcDevEndpoint, resolveAgcDevEndpoint,
withAgcDevEndpointEnv, withAgcDevEndpointEnv,
}; };
@@ -14,14 +14,12 @@ import {
import { import {
agcVitePortEnvKey, agcVitePortEnvKey,
readAgcDevEndpoint, readAgcDevEndpoint,
resolveAgcAdminWebEndpoint,
resolveAgcDevEndpoint, resolveAgcDevEndpoint,
withAgcDevEndpointEnv, withAgcDevEndpointEnv,
} from './dev-port.mjs'; } from './dev-port.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url)); const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..'); const repoRoot = resolve(appRoot, '../..');
const adminWebDir = resolve(repoRoot, 'apps/admin-web');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json'); const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const apiServerExePath = resolve( const apiServerExePath = resolve(
repoRoot, repoRoot,
@@ -34,8 +32,6 @@ const backendSpacetimeDataDir = resolve(
repoRoot, repoRoot,
'server-rs/.spacetimedb/ai-game-creator/data', 'server-rs/.spacetimedb/ai-game-creator/data',
); );
// 后台 Web 默认跟随 AGC 一起起来,便于联调后台页面;`AGC_DEV_ADMIN_WEB=0` 可关闭。
const agcDevAdminWebEnvKey = 'AGC_DEV_ADMIN_WEB';
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const childLifecycles = new WeakMap(); const childLifecycles = new WeakMap();
@@ -204,122 +200,36 @@ function urlPort(url) {
} }
} }
// 端口归属探测脚本。历史实现用 `Get-NetTCPConnection` 取监听进程,而它底层走 // 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如缺少
// WMI:实测单端口单次 11.2 秒、再叠加每个 PID 的 `Get-CimInstance` 3.3 秒, // Get-NetTCPConnection),此时调用方必须退化为旧行为,不能让本地启动直接失败。
// 一轮探测约 43 秒,直接把"配套后端就绪"等待拖到分钟级。改用原生
// `netstat -ano`(约 30 毫秒)取端口 -> PID,再用 .NET `Process` 读进程名和
// 可执行文件路径(毫秒级);只有核对 SpacetimeDB `--data-dir` 归属时才按 PID
// 取命令行,并允许调用方把已知命令行传进来复用。
const windowsPortOwnerProbeCommand = [
'$ErrorActionPreference = "SilentlyContinue"',
'$queriedPorts = @()',
'foreach ($raw in ($env:GENARRATIVE_QUERY_PORTS -split ",")) {',
' if ($raw -match "^\\d+$") { $queriedPorts += [int]$raw }',
'}',
'$knownCommandLines = @{}',
'if ($env:GENARRATIVE_KNOWN_COMMAND_LINES) {',
' try {',
' foreach ($property in (ConvertFrom-Json $env:GENARRATIVE_KNOWN_COMMAND_LINES).PSObject.Properties) {',
' $knownCommandLines[[int]$property.Name] = [string]$property.Value',
' }',
' } catch { }',
'}',
'$listenerPidByPort = @{}',
'foreach ($line in (netstat -ano -p tcp)) {',
' $fields = @($line -split "\\s+" | Where-Object { $_ })',
' if ($fields.Count -lt 4) { continue }',
' if ($fields[0] -ne "TCP") { continue }',
' # A listening socket always has foreign address 0.0.0.0:0 / [::]:0, which',
' # is locale-independent unlike the localized netstat State column.',
' if ($fields[2] -notmatch ":0$") { continue }',
' $localPort = [int]($fields[1].Split(":")[-1])',
' if ($queriedPorts -notcontains $localPort) { continue }',
' # The PID is the last column; do not hardcode its index.',
' if ($fields[-1] -notmatch "^\\d+$") { continue }',
' $listenerPidByPort[$localPort] = [int]$fields[-1]',
'}',
'$result = @()',
'foreach ($port in ($listenerPidByPort.Keys | Sort-Object)) {',
' $processId = $listenerPidByPort[$port]',
' $name = $null',
' $executablePath = $null',
' $commandLine = $null',
' try {',
' $process = [System.Diagnostics.Process]::GetProcessById($processId)',
' $name = $process.ProcessName + ".exe"',
' try { $executablePath = $process.MainModule.FileName } catch { }',
' } catch { }',
' if ($knownCommandLines.ContainsKey($processId)) {',
' $commandLine = $knownCommandLines[$processId]',
' } elseif (($name -like "spacetime*") -or (-not $executablePath)) {',
' try { $commandLine = (Get-CimInstance Win32_Process -Filter ("ProcessId=" + $processId)).CommandLine } catch { }',
' }',
' $result += [pscustomobject]@{ port = [int]$port; processId = $processId; name = $name; executablePath = $executablePath; commandLine = $commandLine }',
'}',
'ConvertTo-Json -InputObject @($result) -Compress',
].join('\n');
// 进程命令行在进程生命周期内不变,但 PID 会被系统复用;按 PID 记 TTL 缓存,
// 让"等配套后端就绪"的轮询只在首个周期付出 WMI 成本。TTL 取 5 分钟:本轮实测
// 这台机器上首次 WMI 调用约 18 秒(热调用 3.3 秒),而 PID 在 5 分钟内被复用
// 成另一个运行本工作树 data dir 的 SpacetimeDB 才能造成误判,概率可忽略。
// 默认实现才缓存,注入实现(测试)与显式 env 始终重新读取。
const WINDOWS_COMMAND_LINE_CACHE_TTL_MS = 300_000;
const windowsPortOwnerCommandLineCache = new Map();
function resolveCommandLineCache({ spawnImpl, env }) {
return spawnImpl === spawnSync && env === process.env
? windowsPortOwnerCommandLineCache
: new Map();
}
// 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如系统缺少
// netstat),此时调用方必须退化为旧行为,不能让本地启动直接失败。
function readWindowsPortOwnerIdentities( function readWindowsPortOwnerIdentities(
ports, ports,
{ { spawnImpl = spawnSync, env = process.env } = {},
spawnImpl = spawnSync,
env = process.env,
now = Date.now,
commandLineTtlMs = WINDOWS_COMMAND_LINE_CACHE_TTL_MS,
commandLineCache = resolveCommandLineCache({ spawnImpl, env }),
} = {},
) { ) {
const uniquePorts = [...new Set(ports.filter((port) => port > 0))]; const uniquePorts = [...new Set(ports.filter((port) => port > 0))];
if (uniquePorts.length === 0) { if (uniquePorts.length === 0) {
return null; return null;
} }
const knownCommandLines = {}; const command = [
for (const [processId, record] of [...commandLineCache]) { '$ErrorActionPreference = "SilentlyContinue"',
if (record && now() - record.at < commandLineTtlMs) { '$ports = ($env:GENARRATIVE_QUERY_PORTS -split ",") | Where-Object { $_ }',
knownCommandLines[processId] = record.commandLine; '$result = @()',
} else { 'foreach ($port in $ports) {',
commandLineCache.delete(processId); ' $connection = Get-NetTCPConnection -State Listen -LocalPort ([int]$port) -ErrorAction SilentlyContinue | Select-Object -First 1',
} ' if (-not $connection) { continue }',
} ' $owner = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $connection.OwningProcess) -ErrorAction SilentlyContinue',
' $result += [pscustomobject]@{ port = [int]$port; processId = [int]$connection.OwningProcess; name = $owner.Name; executablePath = $owner.ExecutablePath; commandLine = $owner.CommandLine }',
const childEnv = { '}',
...env, 'ConvertTo-Json -InputObject @($result) -Compress',
GENARRATIVE_QUERY_PORTS: uniquePorts.join(','), ].join('\n');
};
if (Object.keys(knownCommandLines).length > 0) {
childEnv.GENARRATIVE_KNOWN_COMMAND_LINES =
JSON.stringify(knownCommandLines);
}
const result = spawnImpl( const result = spawnImpl(
'powershell.exe', 'powershell.exe',
[ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
windowsPortOwnerProbeCommand,
],
{ {
encoding: 'utf8', encoding: 'utf8',
env: childEnv, env: { ...env, GENARRATIVE_QUERY_PORTS: uniquePorts.join(',') },
maxBuffer: 8 * 1024 * 1024, maxBuffer: 8 * 1024 * 1024,
}, },
); );
@@ -330,22 +240,9 @@ function readWindowsPortOwnerIdentities(
const owners = new Map(); const owners = new Map();
for (const entry of parseWindowsProcessSnapshot(result.stdout)) { for (const entry of parseWindowsProcessSnapshot(result.stdout)) {
const port = Number(entry?.port); const port = Number(entry?.port);
if (!Number.isInteger(port) || port <= 0) { if (Number.isInteger(port) && port > 0) {
continue; owners.set(port, entry);
} }
const processId = Number(entry?.processId);
if (
Number.isInteger(processId) &&
processId > 0 &&
typeof entry?.commandLine === 'string' &&
entry.commandLine
) {
commandLineCache.set(processId, {
commandLine: entry.commandLine,
at: now(),
});
}
owners.set(port, entry);
} }
return owners; return owners;
} }
@@ -982,102 +879,10 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
); );
} }
function readAdminWebEnabled(env = process.env) {
return String(env[agcDevAdminWebEnvKey] ?? '').trim() !== '0';
}
// 后台 Web 与 AGC Vite 一样直接由本启动器持有,不经过 `dev.mjs admin-web`
// 后者会整体重写 `.app/dev-stack.json`,把本次配套后端的状态覆盖掉。
function startAdminWeb(
apiUrl,
endpoint,
{ env = process.env, spawnImpl = spawnChild } = {},
) {
return spawnImpl(
npm,
[
'--prefix',
'../..',
'exec',
'vite',
'--',
'--host',
endpoint.host,
'--port',
String(endpoint.port),
'--strictPort',
],
{
cwd: adminWebDir,
env: {
...env,
ADMIN_API_TARGET: apiUrl,
GENARRATIVE_API_TARGET: apiUrl,
GENARRATIVE_API_PORT: String(urlPort(apiUrl) || 8082),
ADMIN_WEB_BASE: endpoint.basePath,
},
},
);
}
function formatStartupSummary({
frontendUrl = '',
apiUrl = '',
adminWebUrl = '',
spacetimeUrl = '',
bgfilterWorkerUrl = '',
} = {}) {
const segments = [
['前端', frontendUrl],
['后端', apiUrl],
['后台', adminWebUrl],
['数据库', spacetimeUrl],
['bgfilter-worker', bgfilterWorkerUrl],
]
.filter(([, value]) => Boolean(value))
.map(([label, value]) => `${label} ${value}`);
return `[ai-game-creator-shell] 启动汇总: ${segments.join(' | ')}`;
}
// 后台 Web 是可选联调服务:端口解析或启动失败只告警,不能阻断 AGC 客户端与配套后端。
async function ensureAdminWeb({
apiUrl,
reservedPorts = [],
env = process.env,
enabled = readAdminWebEnabled(env),
resolveEndpoint = resolveAgcAdminWebEndpoint,
spawnAdminWeb = startAdminWeb,
waitForExit = waitForChildTermination,
warn = (message) => console.warn(message),
} = {}) {
if (!enabled) {
return { endpoint: null, child: null };
}
try {
const endpoint = await resolveEndpoint({ env, reservedPorts });
const child = spawnAdminWeb(apiUrl, endpoint, { env });
waitForExit(child).then((failure) => {
warn(
`[ai-game-creator-shell] 后台 Web 已退出(${formatChildFailure(failure)}),AGC 继续运行。`,
);
});
return { endpoint, child };
} catch (error) {
warn(
`[ai-game-creator-shell] 后台 Web 未能启动(${
error instanceof Error ? error.message : String(error)
}),AGC 继续运行。`,
);
return { endpoint: null, child: null };
}
}
async function main() { async function main() {
let backendChild = null; let backendChild = null;
let startedBackend = false; let startedBackend = false;
let viteChild = null; let viteChild = null;
let adminWebChild = null;
let shutdownSignal = ''; let shutdownSignal = '';
const signalHandlers = new Map(); const signalHandlers = new Map();
@@ -1100,7 +905,6 @@ async function main() {
const handler = () => { const handler = () => {
shutdownSignal = signal; shutdownSignal = signal;
stopChild(viteChild, signal); stopChild(viteChild, signal);
stopChild(adminWebChild, signal);
stopChild(backendChild, signal); stopChild(backendChild, signal);
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。 // 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
sweepStartedBackend(); sweepStartedBackend();
@@ -1133,25 +937,6 @@ async function main() {
throw new Error(`启动期收到 ${shutdownSignal},已停止前端服务`); throw new Error(`启动期收到 ${shutdownSignal},已停止前端服务`);
} }
const adminWeb = await ensureAdminWeb({
apiUrl: backend.targets.apiUrl,
// AGC Vite 端口尚未监听,必须显式保留,避免被后台 Web 抢先占用。
reservedPorts: [endpoint.port],
});
adminWebChild = adminWeb.child;
if (shutdownSignal) {
throw new Error(`启动期收到 ${shutdownSignal},已停止后台 Web`);
}
console.log(
formatStartupSummary({
frontendUrl: endpoint.url,
apiUrl: backend.targets.apiUrl,
adminWebUrl: adminWeb.endpoint?.url ?? '',
spacetimeUrl: backend.targets.spacetimeUrl,
bgfilterWorkerUrl: backend.targets.bgfilterWorkerUrl,
}),
);
const children = [backendChild, viteChild].filter(Boolean); const children = [backendChild, viteChild].filter(Boolean);
if (children.length === 0) { if (children.length === 0) {
return 0; return 0;
@@ -1161,12 +946,10 @@ async function main() {
children.map((child) => waitForChildTermination(child)), children.map((child) => waitForChildTermination(child)),
); );
stopChild(viteChild); stopChild(viteChild);
stopChild(adminWebChild);
stopChild(backendChild); stopChild(backendChild);
return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0); return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0);
} catch (error) { } catch (error) {
stopChild(viteChild); stopChild(viteChild);
stopChild(adminWebChild);
stopChild(backendChild); stopChild(backendChild);
console.error( console.error(
`[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`, `[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`,
@@ -1175,7 +958,6 @@ async function main() {
} finally { } finally {
await Promise.all([ await Promise.all([
terminateChildTree(viteChild), terminateChildTree(viteChild),
terminateChildTree(adminWebChild),
terminateChildTree(backendChild), terminateChildTree(backendChild),
]); ]);
sweepStartedBackend(); sweepStartedBackend();
@@ -1193,12 +975,9 @@ function isDirectModuleExecution() {
} }
export { export {
agcDevAdminWebEnvKey,
ensureAdminWeb,
ensureBackend, ensureBackend,
formatChildFailure, formatChildFailure,
formatOwnerLabel, formatOwnerLabel,
formatStartupSummary,
isAiGameCreatorServer, isAiGameCreatorServer,
isBackendReady, isBackendReady,
isDirectModuleExecution, isDirectModuleExecution,
@@ -1206,7 +985,6 @@ export {
isWorktreeApiServerOwner, isWorktreeApiServerOwner,
isWorktreeSpacetimeOwner, isWorktreeSpacetimeOwner,
preflightExistingVite, preflightExistingVite,
readAdminWebEnabled,
readBackendServiceFailure, readBackendServiceFailure,
readChildFailure, readChildFailure,
readExistingViteServer, readExistingViteServer,
@@ -1215,7 +993,6 @@ export {
resolveBackendTargetsFromState, resolveBackendTargetsFromState,
runWindowsTaskkill, runWindowsTaskkill,
spawnChild, spawnChild,
startAdminWeb,
stopChild, stopChild,
terminateChildTree, terminateChildTree,
verifyAgcBackendOwnership, verifyAgcBackendOwnership,
@@ -22,6 +22,7 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..'); const repoRoot = resolve(appRoot, '../..');
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js'); const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG'; const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG';
const designDebugEnabled = const designDebugEnabled =
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1'; process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
@@ -136,6 +137,7 @@ async function runTauriDev(
env: { env: {
...withAgcDevEndpointEnv(endpoint), ...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled, [AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
}, },
}); });
const childResult = waitForCli(child); const childResult = waitForCli(child);
@@ -189,6 +191,7 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
cwd: repoRoot, cwd: repoRoot,
env: { env: {
...withAgcDevEndpointEnv(endpoint), ...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
}, },
}, },
); );
@@ -1,11 +1,12 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main", "identifier": "main",
"description": "AI 游戏创作主窗口允许读系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。", "description": "AI 游戏创作主窗口允许读系统剪贴板,用于粘贴素材附件和复制生成文件路径;允许弹出原生打开/保存对话框用于素材上传与导出。",
"windows": ["client"], "windows": ["client"],
"permissions": [ "permissions": [
"clipboard-manager:allow-read-image", "clipboard-manager:allow-read-image",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:image:allow-rgba", "core:image:allow-rgba",
"core:image:allow-size", "core:image:allow-size",
"core:resources:allow-close", "core:resources:allow-close",
@@ -1,4 +1,4 @@
概念阶段定稿时,创建或更新 `project/速览卡.md`下面是速览卡的参考结构;根据游戏类型、项目规模和用户要求选择字段,同类内容可以合并,项目不需要的字段可以省略,复杂项目可以增加必要字段。表格和列表中的示例行可按实际对象逐行扩展,不代表数量上限 概念阶段定稿时,还必须创建或更新 `project/速览卡.md`Runtime 只检查该文件是否存在,不检查内容。请使用下面的固定结构,不要加入审批操作说明或独立的决定状态段落
# 速览卡:《游戏名》 # 速览卡:《游戏名》
@@ -20,7 +20,7 @@
## 6. 核心循环 ## 6. 核心循环
## 7. 目标用户与情境 ## 7. 目标用户
- 核心用户: - 核心用户:
- 游戏偏好: - 游戏偏好:
- 单次游玩时长: - 单次游玩时长:
File diff suppressed because one or more lines are too long
@@ -33,7 +33,7 @@ description: 写"美术圣经"(美术侧)分册时使用。与总纲(技
对象(资产总清单的范围)、物品表(item_id 绑定依据,数据侧已定)、 对象(资产总清单的范围)、物品表(item_id 绑定依据,数据侧已定)、
画风 skill(全局画风库可引用)。 画风 skill(全局画风库可引用)。
2. 本件在数据侧表结构定稿后开写(素材清单引用 item_id)。 2. 本件在数据侧表结构定稿后开写(素材清单引用 item_id)。
3.金样 exemplars/stardew-tdd-art-bible.md 了解契约表与资产状态表包含的信息类型(同层只读一次)。 3. 读金样 exemplars/stardew-tdd-art-bible.md——契约表与资产状态表的登记密度以它为准(同层只读一次)。
## 三、怎么写(模板即流程,按节) ## 三、怎么写(模板即流程,按节)
@@ -34,7 +34,7 @@ description: 写"数据与配表"(数据侧)分册时使用。与总纲(
(架构层的定性基准,在本件落成前 N 日验算)。 (架构层的定性基准,在本件落成前 N 日验算)。
2. 先读两份提取件:字段字典全套规则与验收模板已在那里成文,本件是 2. 先读两份提取件:字段字典全套规则与验收模板已在那里成文,本件是
项目实例化,不是重新发明。 项目实例化,不是重新发明。
3.金样 exemplars/stardew-tdd-data.md 了解数据清单、验算表与验收结论包含的信息类型(同层只读一次)。 3. 读金样 exemplars/stardew-tdd-data.md——总清单规模、验算表与验收结论的写法以它为准(同层只读一次)。
## 三、怎么写(模板即流程,按节) ## 三、怎么写(模板即流程,按节)
@@ -28,7 +28,7 @@ description: 写"技术实现"(程序侧)分册时使用。与总纲(技
1. 输入齐了吗:架构层系统范围表+P0 清单(拆模块依据)、数据侧表结构契约 1. 输入齐了吗:架构层系统范围表+P0 清单(拆模块依据)、数据侧表结构契约
(加载与校验要引用)、skill 选型卡(实现类需求先查卡,不自造轮子)。 (加载与校验要引用)、skill 选型卡(实现类需求先查卡,不自造轮子)。
2. 读总纲判断立场;本件在数据侧表结构定稿后开写。 2. 读总纲判断立场;本件在数据侧表结构定稿后开写。
3.金样 exemplars/stardew-tdd-tech.md 了解技术实现文档包含的信息类型(同层只读一次)。 3. 读金样 exemplars/stardew-tdd-tech.md——各节的填充密度与"实证参照"写法以它为准(同层只读一次)。
## 三、怎么写(模板即流程,按节) ## 三、怎么写(模板即流程,按节)
@@ -13,13 +13,10 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
> 本文件是系统架构层唯一承载写作流程的教学件。 > 本文件是系统架构层唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。 > 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和顶层设计判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用架构。
## 一、这一层的判断立场 ## 一、这一层的判断立场
你是架构师,切系统的刀在你手里。在这个层里你相信: 你是架构师,切系统的刀在你手里。在这个层里你相信:
- 切分是为了**职责清晰、可独立讨论**,不是为了凑数量。只有确实需要独立职责、状态或数据边界的部分才拆成系统;每个实际拆出的系统应能说明删除后的影响。 - 切分是为了**职责清晰、可独立讨论**,不是为了凑数量——每个系统必须能
一句话答出"删了它,什么塌"(P0 原因)。
- **数据所有权唯一**:同一事实只由一个系统维护,其他系统只引用稳定 ID, - **数据所有权唯一**:同一事实只由一个系统维护,其他系统只引用稳定 ID,
不复制主数据。两个系统管同一件事 = 架构事故。 不复制主数据。两个系统管同一件事 = 架构事故。
- **依赖无环**是硬要求;信息呈现层只读状态、只经行动入口写入。 - **依赖无环**是硬要求;信息呈现层只读状态、只经行动入口写入。
@@ -31,11 +28,11 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
## 二、动笔前 ## 二、动笔前
1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束** 1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束**
摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。 摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。
2. exemplars/stardew-architecture.md 了解内容组织方式 2. 读 exemplars/stardew-architecture.md 做质量锚(模仿密度,不抄内容)
然后往 templates/architecture.md 里填。 然后往 templates/architecture.md 里填。
3. 记住顶层的核心循环图——切完必须跑覆盖检查。 3. 记住顶层的核心循环图——切完必须跑覆盖检查。
## 三、架构设计的组织维度:写什么、为什么、怎么咬合 ## 三、十二节总览:写什么、为什么、怎么咬合
架构文档回答四个问题: 架构文档回答四个问题:
**这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→ **这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→
@@ -77,7 +74,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖 三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖
三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。 三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。
## 四、怎么写(模板参考结构,建议按此组织 ## 四、怎么写(模板即流程,十二节按序
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md (本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md
### 1. 架构定位与目标 ### 1. 架构定位与目标
@@ -88,10 +85,10 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。 → 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
### 2. 系统地图 ### 2. 系统地图
Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。 Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
P0 段五列表: P0 段五列表:
| 系统 | 目的 | 输入 | 输出 | P0 原因 | | 系统 | 目的 | 输入 | 输出 | P0 原因 |
对实际拆出的系统说明删除后的影响;无法形成独立职责的部分合并,不为满足数量新增系统 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并
### 3. 系统职责 ### 3. 系统职责
| 系统 | 主要职责 | 不负责 → 移交谁 | | 系统 | 主要职责 | 不负责 → 移交谁 |
@@ -13,10 +13,6 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
> 本文件是概念层唯一承载写作流程的教学件。 > 本文件是概念层唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。 > 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和上层已定范围判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
## 一、这一层的判断立场 ## 一、这一层的判断立场
你是资深游戏策划,看过上千份概念案,清楚绝大多数死在"什么都说、什么都不尖"。 你是资深游戏策划,看过上千份概念案,清楚绝大多数死在"什么都说、什么都不尖"。
在这个层里你相信: 在这个层里你相信:
@@ -31,11 +27,11 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 二、动笔前 ## 二、动笔前
1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。 1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。
没有 → 先问一个定调问题,禁止自问自答充当用户。 没有 → 先问一个定调问题,禁止自问自答充当用户。
2. exemplars/stardew-concept.md 了解内容组织方式 2. 读 exemplars/stardew-concept.md 做质量锚(模仿密度,不抄内容)
然后往 templates/concept-design.md 里填。 然后往 templates/concept-design.md 里填。
3. 零参照时在文档头注明"零参照"。 3. 零参照时在文档头注明"零参照"。
## 三、概念设计的组织维度:写什么、为什么、怎么咬合 ## 三、九节总览:写什么、为什么、怎么咬合
概念文档回答四个问题: 概念文档回答四个问题:
**这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→ **这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→
@@ -54,7 +50,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
| 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 | | 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 |
| 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 | | 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 |
| 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" | | 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" |
| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 按需记录给顶层的约束 | 收口并检查概念是否写散;把承诺成对下的契约 | 回环呼应 1;把边界和交接约束传给下一层 | | 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 给顶层的约束 | 收口重锤:写完九节重述一遍,检验整份文档有没有写散;把承诺成对下的契约 | 回环呼应 1;把 8 的交接具体化成 2~4 条硬约束 |
咬合一图: 咬合一图:
@@ -73,7 +69,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束; 记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束;
**对上**边界画线防止越层。九节不是清单,是一台咬合的机器。 **对上**边界画线防止越层。九节不是清单,是一台咬合的机器。
## 四、怎么写(模板参考结构,建议按此组织 ## 四、怎么写(模板即流程,九节按序
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md (本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md
### 1. 一句话概念 ### 1. 一句话概念
@@ -84,7 +80,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
**定调记录**(全项目调性真源,此节定死): **定调记录**(全项目调性真源,此节定死):
- 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。 - 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。
- 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。 - 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。
- 调性锚 T 原则:按项目需要提炼并逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。 - 调性锚 T 原则:3~7 条逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。 检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。
→ 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。 → 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。
**设计锚点(六项,争议时的仲裁原则,全部具名)** **设计锚点(六项,争议时的仲裁原则,全部具名)**
@@ -116,7 +112,8 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 7. 核心张力 ### 7. 核心张力
- __ 有限,但 __ - __ 有限,但 __
- __ vs __(两端的代价各是什么)。 - __ vs __(两端的代价各是什么)。
如果项目存在核心张力,保留的每条张力都应说明双方代价;没有形成有效张力时,不为了满足结构新增张力。这些是顶层取舍表的种子,后面按需对应。 每条两端都必须有代价,只有一端的"假张力"删掉。这些是顶层取舍表的
种子,后面要逐条对应。
### 8. 边界与约束 ### 8. 边界与约束
- 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、 - 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、
@@ -127,9 +124,9 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 9. 概念定稿(收口重锤) ### 9. 概念定稿(收口重锤)
这个游戏的核心不是 __,而是: 这个游戏的核心不是 __,而是:
> (一句话重述核心承诺) > (一句话重述核心承诺)
交给下一层的约束:按项目需要记录,顶层据此展开。 交给下一层的约束:__ 必须 __2~4 条,顶层必须围绕它们展开
某节对本项目没意义,直接省略 某节对本项目没意义 → 写一行"略,因为 __",不硬凑
## 五、分析文档(全局一份,按层分节) ## 五、分析文档(全局一份,按层分节)
@@ -161,4 +158,4 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 七、红线(只有三条) ## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。 1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:出现具体数值、按键、界面即删。 2. 不越层:出现具体数值、按键、界面即删。
3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略 3. 不凑数:写不满就说明缺什么,禁止万金油句填充
@@ -2,7 +2,7 @@
--- ---
name: game-gdd-system-doc name: game-gdd-system-doc
description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容 description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架
红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md 红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md
与对应模块的模板.md 里,按需取用。 与对应模块的模板.md 里,按需取用。
--- ---
@@ -12,10 +12,6 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
> 本文件是系统文档层的总纲;各系统的专属写法在 `modules/system-types/` 下对应目录的 `SKILL.md` > 本文件是系统文档层的总纲;各系统的专属写法在 `modules/system-types/` 下对应目录的 `SKILL.md`
专属模板在 `modules/system-types/` 对应目录的 `模板.md`。通用纪律不在各系统 skill 里重复。 专属模板在 `modules/system-types/` 对应目录的 `模板.md`。通用纪律不在各系统 skill 里重复。
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据系统类型、实际复杂度、用户要求和架构职责判断适用项:适用项写入,同类项可合并,若某项对本系统没意义则省略;复杂系统可以拆分补充,简单系统可以压缩为最小可执行规格。
## 一、这一层的判断立场 ## 一、这一层的判断立场
你是写单个系统的策划。在这个层里你相信: 你是写单个系统的策划。在这个层里你相信:
- 系统文档是**执行层**:刀已经在架构层切好——服从系统地图编号、职责表 - 系统文档是**执行层**:刀已经在架构层切好——服从系统地图编号、职责表
@@ -24,15 +20,15 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
防返工价值最高的几行。 防返工价值最高的几行。
- 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。 - 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。
- 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。 - 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。
- 系统文档保持基本可读的一致性,但不要求所有系统使用相同章节;结构应服从系统类型和实际行为 - 所有系统同构:读者读熟一份就能读所有份
## 二、动笔前 ## 二、动笔前
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。 1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗 2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
的判定部分),读取对应的 `SKILL.md``模板.md` 的判定部分),读取对应的 `SKILL.md``模板.md`
3. 该文件夹标注"参考例子"的,先读例子了解写法 3. 该文件夹标注"必读例子"的,先读例子全文做密度锚
## 三、常见内容总览:写什么、为什么、怎么咬合 ## 三、十二节总览:写什么、为什么、怎么咬合
系统文档回答四个问题: 系统文档回答四个问题:
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→ **这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
@@ -47,7 +43,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
| 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 | | 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 |
| 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 | | 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 |
| 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 | | 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 |
| 8 | 反馈 | 关键结果何时、以何种方式反馈 | 让实际结果可理解 | 与本系统实际结果对应 | | 8 | 反馈 | 何时/何强度/何通道 | 无反馈=没发生 | 顶层反馈四层 |
| 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 | | 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 |
| 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 | | 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 |
| 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 | | 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 |
@@ -56,20 +52,20 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越 咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
职责边界;**对下**第 7 节交接喂 TDD。 职责边界;**对下**第 7 节交接喂 TDD。
## 四、常见内容的参考写法 ## 四、十二节通用写法
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md (各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。 1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
2 支撑体验:对应顶层目标第__条、调性原则第__条。 2 支撑体验:对应顶层目标第__条、调性原则第__条。
3 进入与退出:按本系统实际存在的入口、退出和恢复路径记录 3 进入与退出:常规进入/读档恢复/特殊事件后返回,三入口必写
4 玩家行动:记录本系统实际存在的具名动词组;编排类写"安排"动词,活动类写"操作"动词。 4 玩家行动:≥4 个具名动词组;编排类写"安排"动词,活动类写"操作"动词。
5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。 5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。
6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。 6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。
7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。 7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。
8 反馈:记录本系统关键结果的可理解反馈;存在失败时说明原因和恢复路径。 8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
9 内部循环:动词链;可拆单次/区域/长期三层。 9 内部循环:动词链;可拆单次/区域/长期三层。
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。 10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
11 边界与非目标:参考该类型 skill 的三不”说明边界;建议说明字段数值的交接边界 11 边界与非目标:该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条
12 开放问题:结构级才留;手感数值类标"待原型验证"。 12 开放问题:结构级才留;手感数值类标"待原型验证"。
## 五、分析文档(全局一份,按层分节) ## 五、分析文档(全局一份,按层分节)
@@ -104,4 +100,4 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。 1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD), 2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD),
不替别的系统定规则。 不替别的系统定规则。
3. 不凑数:写不出"删了塌什么"的系统直接删除;章节对项目有意义但信息不足时,记录已确定内容与待补问题 3. 不凑数:写不出"删了塌什么"、填不满的节,说明缺料——停笔说明,不硬凑
File diff suppressed because it is too large Load Diff
@@ -13,10 +13,6 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
> 本文件是顶层设计唯一承载写作流程的教学件。 > 本文件是顶层设计唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。 > 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
## 〇、结构适配原则
本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和概念层定稿判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
## 一、这一层的判断立场 ## 一、这一层的判断立场
你是资深游戏策划,正在写全 GDD 最重要的一份文档——概念说"凭什么成立", 你是资深游戏策划,正在写全 GDD 最重要的一份文档——概念说"凭什么成立",
顶层说"好玩在哪"。核心循环无趣,后面写再多系统也救不回来。在这个层里你相信: 顶层说"好玩在哪"。核心循环无趣,后面写再多系统也救不回来。在这个层里你相信:
@@ -30,11 +26,11 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
## 二、动笔前 ## 二、动笔前
1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。 1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。
2. exemplars/stardew-top-design.md 了解内容组织方式 2. 读 exemplars/stardew-top-design.md 做质量锚(模仿密度,不抄内容)
然后往 templates/top-design.md 里填。 然后往 templates/top-design.md 里填。
3. 把概念层已确认的核心张力作为输入;存在对应取舍时再挂上编号。 3. 把概念层的核心张力清单摊开放在手边——取舍表必须逐条挂上编号。
## 三、顶层设计的组织维度:写什么、为什么、怎么咬合 ## 三、十六节总览:写什么、为什么、怎么咬合
顶层文档回答四个问题: 顶层文档回答四个问题:
**玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→ **玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→
@@ -47,14 +43,14 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
|---|---|---|---|---| |---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 | | 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 | | 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 | | 3 | 核心推动力 | 动机主次 + 即时/日程/季节/长期四层推动 | 玩家"什么时候被什么推着走"的完整图谱 | 时间四层对应 10 节奏结构的四层 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 | | 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
| 5 | 小循环 | 按项目实际存在的局内或短周期动词链组织 | 记录真正被玩到的循环 | 按实际循环层级互检 | | 5 | 小循环 | 几十秒到几分钟的具名动词链 ×3+ | 真正被玩到的那层;动词链可直接复制进实现 | 检验:删掉某条,游戏是否少了一块可命名的乐趣 |
| 6 | 资源流与输入输出 | 按项目实际存在的资源流、输入输出和反馈组织 | 说明循环中的实际供给与结果 | 与实际循环环节对应 | | 6 | 资源流与输入输出 | 资源流图(来源→储存→消耗)+ 输入输出清单 + 反馈四层 | 资源是循环的血液;防白给、防废物、防套利 | 供血给 4~5 的每个循环环节 |
| 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 | | 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 |
| 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 | | 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 |
| 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) | | 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) |
| 10 | 节奏结构 | 按项目实际存在的时间层级和情绪变化组织 | 说明玩法节奏如何变化 | 与实际推动力层级对应 | | 10 | 节奏结构 | 日内/周内/季节/长期四层 + 情绪摆动 | 防止"一直紧张"或"一直平";摆动才有呼吸 | 四层对应 3 的推动力四层 |
| 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 | | 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 |
| 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 | | 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 |
| 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 | | 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 |
@@ -84,7 +80,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
(大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、 (大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、
顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。 顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。
## 四、怎么写(模板参考结构,建议按此组织 ## 四、怎么写(模板即流程,十六节按序
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md (本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md
### 1. 顶层定位与规模锚点 ### 1. 顶层定位与规模锚点
@@ -100,24 +96,24 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
### 3. 核心推动力 ### 3. 核心推动力
- 动机主次:__。 - 动机主次:__。
- 即时推动 __;日程推动 __;季节推动 __;长期推动 __ - 即时推动 __;日程推动 __;季节推动 __;长期推动 __
只展开项目实际存在的时间层级;不存在的层级不设字段 四层都要有实指;空着的那层就是将来留存崩塌的地方
### 4. 大循环 ### 4. 大循环
**__ → ______ → 回到 __。**(附核心循环图) **__ → ______ → 回到 __。**(附核心循环图)
→ 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。 → 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。
### 5. 小循环(按项目实际数量 ### 5. 小循环(具名动词链 ×3+
**__循环**__ → ________ **__循环**__ → ________
→ 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。 → 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。
### 6. 资源流与输入输出 ### 6. 资源流与输入输出
(资源流图:每种核心资源 来源 → 储存 → 消耗 三段全) (资源流图:每种核心资源 来源 → 储存 → 消耗 三段全)
主要输入 __;主要输出 __按项目需要记录反馈层级 主要输入 __;主要输出 __反馈四层:立即 __ / 短期 __ / 中期 __ / 长期 __
→ 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。 → 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。
### 7. 最小体验单位 ### 7. 最小体验单位
__(多短一段玩法体现独有乐趣——原型只做这一个单位)。 __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
保留的玩家行动应有与玩法相称的可理解反馈;反馈形式和数量按项目决定 单个行动必须至少提供一种清晰反馈:资源/进度/能力/关系/信息/视觉状态之一
### 8. 核心活动流程(段落表) ### 8. 核心活动流程(段落表)
| 阶段 | 玩家行为 | 设计目的 | | 阶段 | 玩家行为 | 设计目的 |
@@ -157,7 +153,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。 顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。
后续架构必须围绕 __ 拆系统;不得 __ 后续架构必须围绕 __ 拆系统;不得 __
某节对本项目没意义,直接省略 某节对本项目没意义 → 写一行"略,因为 __",不硬凑
## 五、分析文档(全局一份,按层分节) ## 五、分析文档(全局一份,按层分节)
@@ -191,4 +187,4 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
## 七、红线(只有三条) ## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。 1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。 2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。
3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略 3. 不凑数:写不满就说明缺什么,禁止万金油句填充
@@ -1,7 +1,5 @@
### C1 模板_系统架构.md(→ templates/architecture.md ### C1 模板_系统架构.md(→ templates/architecture.md
本模板是参考结构,不是固定清单。只有需要独立职责、状态或数据边界的部分才拆成系统;简单项目可以合并系统和章节,复杂项目可以增加必要的系统与校验。表格中的示例行可按实际系统、风险和问题扩展,不代表数量上限。
# 系统架构:《游戏名》 # 系统架构:《游戏名》
## 架构定位与目标 ## 架构定位与目标
@@ -20,7 +18,6 @@
|---|---|---|---| |---|---|---|---|
| S01 | __ | __ | P0 | | S01 | __ | __ | P0 |
| S02 | __ | __ | | | S02 | __ | __ | |
(以上为示例,可按实际系统删减或扩充。)
支撑层(不拥有核心规则):__。 支撑层(不拥有核心规则):__。
@@ -108,8 +105,6 @@ flowchart LR
| 风险 | 校验方式 | | 风险 | 校验方式 |
|---|---| |---|---|
| __ | __ | | __ | __ |
(按实际风险逐行补充。)
## 开放的结构问题 ## 开放的结构问题
- __ - __
(按实际问题逐条补充。)
@@ -1,7 +1,5 @@
### C1 模板_概念设计.md(→ templates/concept-design.md ### C1 模板_概念设计.md(→ templates/concept-design.md
本模板是参考结构,不是固定清单。填写前按项目类型、规模和用户要求筛选章节与字段;同类内容可合并,若某节对项目没有实际意义则删除,复杂项目可增加必要内容。表格和列表中的示例项可按实际内容扩展,不代表数量上限。
# 概念设计:《游戏名》 # 概念设计:《游戏名》
## 一句话概念 ## 一句话概念
@@ -12,14 +10,14 @@
### 定调记录(全项目调性真源,级联决策的依据库) ### 定调记录(全项目调性真源,级联决策的依据库)
- 参照选择:以《__》为主(__, 学 __);不参考 __ - 参照选择:以《__》为主(__, 学 __);不参考 __
- 调性滑杆:压力感 __ / 战斗比重 __ / 管理深度 __ / 叙事比重 __ / 节奏 __ - 调性滑杆:压力感 __ / 战斗比重 __ / 管理深度 __ / 叙事比重 __ / 节奏 __
- 调性锚(按项目需要逐条具名,下游开放问题按需从这里级联): - 调性锚(T 原则,逐条具名,下游每个开放问题先来这里级联):
T__ __ T1 __T2 __T3 __T4 __T5 __
### 设计锚点(六仲裁位) ### 设计锚点(六仲裁位)
- 核心幻想:__。 - 核心幻想:__。
玩家念头:"__" 玩家念头:"__"
- 目标体验:__。 - 目标体验:__。
- 玩家动机(按项目实际存在的时间尺度填写):__ - 玩家动机:短期 __;长期 __
- 核心循环:__ → ______ → 回到 __ - 核心循环:__ → ______ → 回到 __
- 跑偏风险:__。 - 跑偏风险:__。
- 非目标:__(详见《不是什么》)。 - 非目标:__(详见《不是什么》)。
@@ -1,7 +1,5 @@
### C3 02_美术圣经/模板.md(→ templates/tdd-art-bible.md ### C3 02_美术圣经/模板.md(→ templates/tdd-art-bible.md
本模板是美术实施的参考结构。按项目实际需要选择角色、场景、UI、动画和素材契约;没有对应资产类型时删除相应章节,复杂项目可增加必要的视觉规则。表格和资产条目可按实际内容扩展,不代表数量上限。
# 美术圣经:《游戏名》 # 美术圣经:《游戏名》
> 状态:{drafting / reviewed / frozen} | 定调锚:概念层@v{N} 第 2 节 | style_id`__` > 状态:{drafting / reviewed / frozen} | 定调锚:概念层@v{N} 第 2 节 | style_id`__`
@@ -12,7 +10,7 @@ __(一段话:从定调记录翻译的视觉气质;参考图位 __ 张)
## 视觉锚 ## 视觉锚
- 关键词:__按项目需要)。 - 关键词:__3~5 个)。
- 禁用关键词:__。 - 禁用关键词:__。
- 色板:主色 __ / 辅色 __ / 点缀 __(配比 __);昼夜·天气·季节表现 __ - 色板:主色 __ / 辅色 __ / 点缀 __(配比 __);昼夜·天气·季节表现 __
- 形状语言:__。 - 形状语言:__。
@@ -42,7 +40,6 @@ __(承 UI 系统文档的界面清单;视觉语言与信息分层对齐)
| 素材 | 规格(尺寸/帧数/方向数) | 命名规则 | atlas 格式 | 验收 | 绑定 | | 素材 | 规格(尺寸/帧数/方向数) | 命名规则 | atlas 格式 | 验收 | 绑定 |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| __ | __ | __ | __ | __ | `item_ __` / 豁免:__ | | __ | __ | __ | __ | __ | `item_ __` / 豁免:__ |
(以上为示例,可按实际素材删减或扩充。)
- 绘制工艺:__(用陶泥儿 MCP 的路径与参数;封装流程)。 - 绘制工艺:__(用陶泥儿 MCP 的路径与参数;封装流程)。
- 豁免类型仅限:程序化生成 / UI 文本 / 本期不需要。 - 豁免类型仅限:程序化生成 / UI 文本 / 本期不需要。
@@ -52,19 +49,17 @@ __(承 UI 系统文档的界面清单;视觉语言与信息分层对齐)
| asset_id | 规格 | 绑定 | 状态 | 验收记录 | contract_version | | asset_id | 规格 | 绑定 | 状态 | 验收记录 | contract_version |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| __ | __ | `item_ __` / 豁免 | 缺失/草稿/已交付/已验收/已接入 | 技术过/视觉过 @__ | __ | | __ | __ | `item_ __` / 豁免 | 缺失/草稿/已交付/已验收/已接入 | 技术过/视觉过 @__ | __ |
(以上为示例,可按实际资产删减或扩充。)
- 状态单向流转:缺失 → 草稿 → 已交付 → 已验收 → 已接入;驳回退回草稿并记原因。 - 状态单向流转:缺失 → 草稿 → 已交付 → 已验收 → 已接入;驳回退回草稿并记原因。
- 验收两维:技术(尺寸/透明/帧数/命名)+ 视觉(对照视觉锚);两维都过才进"已验收"。 - 验收两维:技术(尺寸/透明/帧数/命名)+ 视觉(对照视觉锚);两维都过才进"已验收"。
- 需要登记的 gameplay 可见对象有一行;不需要资产登记的对象不建立空记录 - 每个 gameplay 可见对象有一行,或显式豁免——没有第三种状态
- 程序接入后填消费点(哪个模块加载、事件映射),`contract_version` 变更须重验收。 - 程序接入后填消费点(哪个模块加载、事件映射),`contract_version` 变更须重验收。
## 量产流程与验证 ## 量产流程与验证
1. 概念候选 __ 张 → 2. 人选方向 → 3. 锚点图 __ 张 → 4. 锁圣经 → 1. 概念候选 __ 张 → 2. 人选方向 → 3. 锚点图 __ 张 → 4. 锁圣经 →
5. 写契约 → 6. 小批 __ 张 → 7. 技术检查(__)→ 8. 接入程序 → 5. 写契约 → 6. 小批 __ 张 → 7. 技术检查(__)→ 8. 接入程序 →
9. 运行时验收(按项目支持的平台)→ 10. 扩产。 9. 运行时截图验收(桌面/移动双视口下 __ 可辨)→ 10. 扩产。
(以上为示例,可按实际流程删减或扩充。)
## 开放问题回执 ## 开放问题回执

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