Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70a2246e8b | |||
| b7e3dac661 | |||
| 2e4a1996c8 | |||
| fe4e952853 | |||
| 18654b6806 | |||
| ee7d00c0b1 | |||
| 9cd1a94369 | |||
| 470e85c0ff |
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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,4 +1 @@
|
|||||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
|
||||||
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
|
||||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
|
||||||
npm run format:staged
|
npm run format:staged
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
|
||||||
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
|
||||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
|
||||||
npm run check:pre-push-master -- "$@"
|
npm run check:pre-push-master -- "$@"
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
"schemaVersion": "game-creator-config.v2",
|
"schemaVersion": "game-creator-config.v2",
|
||||||
"agentMode": "codex_app_server",
|
"agentMode": "codex_app_server",
|
||||||
"llm": {
|
"llm": {
|
||||||
"customEnabled": false,
|
|
||||||
"visibleModels": [],
|
|
||||||
"apiKey": "",
|
"apiKey": "",
|
||||||
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
||||||
"model": "gpt-6-astra",
|
"model": "gpt-6-astra",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@genarrative/ai-game-creator-shell",
|
"name": "@genarrative/ai-game-creator-shell",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.45",
|
"version": "0.1.29",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/start-tauri-dev.mjs",
|
"dev": "node scripts/start-tauri-dev.mjs",
|
||||||
@@ -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
-1
@@ -1725,7 +1725,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.45"
|
version = "0.1.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-runtime-core",
|
"agent-runtime-core",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.45"
|
version = "0.1.29"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -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
+1
-1
@@ -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——契约表与资产状态表的登记密度以它为准(同层只读一次)。
|
||||||
|
|
||||||
## 三、怎么写(模板即流程,按节)
|
## 三、怎么写(模板即流程,按节)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ description: 写"数据与配表"(数据侧)分册时使用。与总纲(
|
|||||||
(架构层的定性基准,在本件落成前 N 日验算)。
|
(架构层的定性基准,在本件落成前 N 日验算)。
|
||||||
2. 先读两份提取件:字段字典全套规则与验收模板已在那里成文,本件是
|
2. 先读两份提取件:字段字典全套规则与验收模板已在那里成文,本件是
|
||||||
项目实例化,不是重新发明。
|
项目实例化,不是重新发明。
|
||||||
3. 读取金样 exemplars/stardew-tdd-data.md 了解数据清单、验算表与验收结论包含的信息类型(同层只读一次)。
|
3. 读金样 exemplars/stardew-tdd-data.md——总清单规模、验算表与验收结论的写法以它为准(同层只读一次)。
|
||||||
|
|
||||||
## 三、怎么写(模板即流程,按节)
|
## 三、怎么写(模板即流程,按节)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user