diff --git a/.codex/skills/genarrative-external-editor-api/SKILL.md b/.codex/skills/genarrative-external-editor-api/SKILL.md
index e6a9f55e7..e4bbafbc2 100644
--- a/.codex/skills/genarrative-external-editor-api/SKILL.md
+++ b/.codex/skills/genarrative-external-editor-api/SKILL.md
@@ -32,6 +32,7 @@ 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.
- 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.
+- 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.
- 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.
diff --git a/.codex/skills/genarrative-external-editor-api/references/api-operations.md b/.codex/skills/genarrative-external-editor-api/references/api-operations.md
index 909174e47..a744d8589 100644
--- a/.codex/skills/genarrative-external-editor-api/references/api-operations.md
+++ b/.codex/skills/genarrative-external-editor-api/references/api-operations.md
@@ -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 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` |
-| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
+| 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` |
| 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` |
| 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.
-`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.
+`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.
## Common Values
diff --git a/.codex/skills/genarrative-external-editor-api/references/capability-routing.md b/.codex/skills/genarrative-external-editor-api/references/capability-routing.md
index a5b4fe886..af0ee338a 100644
--- a/.codex/skills/genarrative-external-editor-api/references/capability-routing.md
+++ b/.codex/skills/genarrative-external-editor-api/references/capability-routing.md
@@ -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.
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 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.
+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).
-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 `
`, 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. 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 `
`, 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.
diff --git a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py
index cc5a4a4b5..c7c0ab33b 100644
--- a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py
+++ b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py
@@ -881,12 +881,14 @@ def _self_test() -> None:
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
canvasSession=session,
assetLabel="贪吃蛇透明图集",
+ sliceMode="connected-components",
referenceId="must-not-override-explicit-reference",
iconDescriptions=["不得覆盖显式图标描述"],
)
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
assert calls[0]["body"]["screenColor"] == "auto"
+ assert calls[0]["body"]["sliceMode"] == "connected-components"
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
print("self-test ok")
diff --git a/.env.example b/.env.example
index d8988060b..bf06357e1 100644
--- a/.env.example
+++ b/.env.example
@@ -1,8 +1,8 @@
# Server-side OpenAI-compatible LLM endpoint base URL.
-LLM_BASE_URL="https://api.vectorengine.cn/v1"
+LLM_BASE_URL="https://api.tiantoken.com/v1"
# Server-side API key used by the local Vite proxy.
-# Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
+# Recommended: set `LLM_API_KEY` locally, or use `TIANTOKEN_API_KEY`
# through the Rust api-server proxy.
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
# 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.
VITE_LLM_MODEL="gpt-5.4-mini"
GENARRATIVE_LLM_PROVIDER="openai-compatible"
-GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
+GENARRATIVE_LLM_BASE_URL="https://api.tiantoken.com/v1"
GENARRATIVE_LLM_API_KEY=""
GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
@@ -130,10 +130,15 @@ GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
-# VectorEngine LLM and GPT-image-2 / Gemini image generation config.
+# Tiantoken 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_API_KEY=""
-VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
+VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS="180000"
# ElevenLabs editor sound-effect generation is server-side only.
ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 793e2b4a4..8a4b2d0c6 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -164,6 +164,7 @@ module.exports = {
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
+ 'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target',
'src/main.tsx',
'src/App.tsx',
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 78fe78bb9..65c4d6e28 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1 +1,4 @@
+# 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
diff --git a/.husky/pre-push b/.husky/pre-push
index fdb72ecc2..2044c66f1 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1 +1,4 @@
+# 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 -- "$@"
diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json
index c08cb6ac9..046b6154e 100644
--- a/apps/ai-game-creator-shell/package.json
+++ b/apps/ai-game-creator-shell/package.json
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
- "version": "0.1.29",
+ "version": "0.1.45",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -57,6 +57,7 @@
"react-colorful": "^5.8.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
+ "rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"vite": "^6.2.0",
"zustand": "^5.0.14"
diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 118c7f9e2..e32dfb4b7 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -113,6 +113,11 @@ const allowedUncalledTauriCommands = [
'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage',
'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_workspace_window',
'read_direct_project_conversation',
@@ -1290,7 +1295,7 @@ for (const requiredSource of [
}
}
-if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
+if (tauriConfig.productName !== '陶泥儿') {
throw new Error('AI game creator shell productName drifted');
}
@@ -1302,19 +1307,19 @@ const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
};
const expectedBundledWindowsResources = {
- 'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
+ 'resources/codex/win-x64/bin/codex.exe': 'coding-agent/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
- 'codex/win-x64/bin/codex-code-mode-host.exe',
+ 'coding-agent/win-x64/bin/codex-code-mode-host.exe',
'resources/codex/win-x64/codex-path/rg.exe':
- 'codex/win-x64/codex-path/rg.exe',
+ 'coding-agent/win-x64/codex-path/rg.exe',
'resources/codex/win-x64/codex-resources/codex-command-runner.exe':
- 'codex/win-x64/codex-resources/codex-command-runner.exe',
+ 'coding-agent/win-x64/codex-resources/codex-command-runner.exe',
'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe':
- 'codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe',
+ 'coding-agent/win-x64/codex-resources/codex-windows-sandbox-setup.exe',
'resources/codex/win-x64/codex-package.json':
- 'codex/win-x64/codex-package.json',
- 'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
- 'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
+ 'coding-agent/win-x64/codex-package.json',
+ 'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md',
+ 'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json',
'resources/plugins': 'plugins',
};
assert.deepEqual(
diff --git a/apps/ai-game-creator-shell/scripts/dev-port.mjs b/apps/ai-game-creator-shell/scripts/dev-port.mjs
index 2e47a7d92..03559d017 100644
--- a/apps/ai-game-creator-shell/scripts/dev-port.mjs
+++ b/apps/ai-game-creator-shell/scripts/dev-port.mjs
@@ -9,6 +9,10 @@ import {
const agcDevHost = '127.0.0.1';
const legacyAgcDevPort = 3080;
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) {
const rawPort = String(env[agcVitePortEnvKey] ?? '').trim();
@@ -99,13 +103,100 @@ 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 {
+ agcAdminWebHost,
+ agcAdminWebPortEnvKey,
agcDevHost,
agcVitePortEnvKey,
+ createAgcAdminWebEndpoint,
createAgcDevEndpoint,
+ legacyAgcAdminWebPort,
legacyAgcDevPort,
readAgcDevEndpoint,
+ readConfiguredAgcAdminWebPort,
readConfiguredAgcDevPort,
+ resolveAgcAdminWebEndpoint,
resolveAgcDevEndpoint,
withAgcDevEndpointEnv,
};
diff --git a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
index e85700ec9..9c5c9e54d 100644
--- a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
+++ b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
@@ -14,12 +14,14 @@ import {
import {
agcVitePortEnvKey,
readAgcDevEndpoint,
+ resolveAgcAdminWebEndpoint,
resolveAgcDevEndpoint,
withAgcDevEndpointEnv,
} from './dev-port.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
+const adminWebDir = resolve(repoRoot, 'apps/admin-web');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const apiServerExePath = resolve(
repoRoot,
@@ -32,6 +34,8 @@ const backendSpacetimeDataDir = resolve(
repoRoot,
'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 childLifecycles = new WeakMap();
@@ -200,36 +204,122 @@ function urlPort(url) {
}
}
-// 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如缺少
-// Get-NetTCPConnection),此时调用方必须退化为旧行为,不能让本地启动直接失败。
+// 端口归属探测脚本。历史实现用 `Get-NetTCPConnection` 取监听进程,而它底层走
+// WMI:实测单端口单次 11.2 秒、再叠加每个 PID 的 `Get-CimInstance` 3.3 秒,
+// 一轮探测约 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(
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))];
if (uniquePorts.length === 0) {
return null;
}
- const command = [
- '$ErrorActionPreference = "SilentlyContinue"',
- '$ports = ($env:GENARRATIVE_QUERY_PORTS -split ",") | Where-Object { $_ }',
- '$result = @()',
- 'foreach ($port in $ports) {',
- ' $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 }',
- '}',
- 'ConvertTo-Json -InputObject @($result) -Compress',
- ].join('\n');
+ const knownCommandLines = {};
+ for (const [processId, record] of [...commandLineCache]) {
+ if (record && now() - record.at < commandLineTtlMs) {
+ knownCommandLines[processId] = record.commandLine;
+ } else {
+ commandLineCache.delete(processId);
+ }
+ }
+
+ const childEnv = {
+ ...env,
+ GENARRATIVE_QUERY_PORTS: uniquePorts.join(','),
+ };
+ if (Object.keys(knownCommandLines).length > 0) {
+ childEnv.GENARRATIVE_KNOWN_COMMAND_LINES =
+ JSON.stringify(knownCommandLines);
+ }
const result = spawnImpl(
'powershell.exe',
- ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
+ [
+ '-NoProfile',
+ '-ExecutionPolicy',
+ 'Bypass',
+ '-Command',
+ windowsPortOwnerProbeCommand,
+ ],
{
encoding: 'utf8',
- env: { ...env, GENARRATIVE_QUERY_PORTS: uniquePorts.join(',') },
+ env: childEnv,
maxBuffer: 8 * 1024 * 1024,
},
);
@@ -240,9 +330,22 @@ function readWindowsPortOwnerIdentities(
const owners = new Map();
for (const entry of parseWindowsProcessSnapshot(result.stdout)) {
const port = Number(entry?.port);
- if (Number.isInteger(port) && port > 0) {
- owners.set(port, entry);
+ if (!Number.isInteger(port) || port <= 0) {
+ continue;
}
+ 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;
}
@@ -879,10 +982,102 @@ 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() {
let backendChild = null;
let startedBackend = false;
let viteChild = null;
+ let adminWebChild = null;
let shutdownSignal = '';
const signalHandlers = new Map();
@@ -905,6 +1100,7 @@ async function main() {
const handler = () => {
shutdownSignal = signal;
stopChild(viteChild, signal);
+ stopChild(adminWebChild, signal);
stopChild(backendChild, signal);
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
sweepStartedBackend();
@@ -937,6 +1133,25 @@ async function main() {
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);
if (children.length === 0) {
return 0;
@@ -946,10 +1161,12 @@ async function main() {
children.map((child) => waitForChildTermination(child)),
);
stopChild(viteChild);
+ stopChild(adminWebChild);
stopChild(backendChild);
return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0);
} catch (error) {
stopChild(viteChild);
+ stopChild(adminWebChild);
stopChild(backendChild);
console.error(
`[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`,
@@ -958,6 +1175,7 @@ async function main() {
} finally {
await Promise.all([
terminateChildTree(viteChild),
+ terminateChildTree(adminWebChild),
terminateChildTree(backendChild),
]);
sweepStartedBackend();
@@ -975,9 +1193,12 @@ function isDirectModuleExecution() {
}
export {
+ agcDevAdminWebEnvKey,
+ ensureAdminWeb,
ensureBackend,
formatChildFailure,
formatOwnerLabel,
+ formatStartupSummary,
isAiGameCreatorServer,
isBackendReady,
isDirectModuleExecution,
@@ -985,6 +1206,7 @@ export {
isWorktreeApiServerOwner,
isWorktreeSpacetimeOwner,
preflightExistingVite,
+ readAdminWebEnabled,
readBackendServiceFailure,
readChildFailure,
readExistingViteServer,
@@ -993,6 +1215,7 @@ export {
resolveBackendTargetsFromState,
runWindowsTaskkill,
spawnChild,
+ startAdminWeb,
stopChild,
terminateChildTree,
verifyAgcBackendOwnership,
diff --git a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs
index 0ce7e6a2b..3ab0f2ced 100644
--- a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs
+++ b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs
@@ -22,7 +22,6 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
-const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG';
const designDebugEnabled =
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
@@ -137,7 +136,6 @@ async function runTauriDev(
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
- [AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
});
const childResult = waitForCli(child);
@@ -191,7 +189,6 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
cwd: repoRoot,
env: {
...withAgcDevEndpointEnv(endpoint),
- [AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
},
);
diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
index 7e07a3c9c..06994d8c6 100644
--- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock
+++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
-version = "0.1.29"
+version = "0.1.45"
dependencies = [
"agent-runtime-core",
"axum",
diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml
index 4fe37d25a..6c3d01ec0 100644
--- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml
+++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
-version = "0.1.29"
+version = "0.1.45"
edition = "2021"
publish = false
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/overview-card.md b/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/overview-card.md
index 0eb32b1e6..b1c0e4a56 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/overview-card.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/overview-card.md
@@ -1,4 +1,4 @@
-概念阶段定稿时,还必须创建或更新 `project/速览卡.md`。Runtime 只检查该文件是否存在,不检查内容。请使用下面的固定结构,不要加入审批操作说明或独立的决定状态段落:
+概念阶段定稿时,创建或更新 `project/速览卡.md`。下面是速览卡的参考结构;根据游戏类型、项目规模和用户要求选择字段,同类内容可以合并,项目不需要的字段可以省略,复杂项目可以增加必要字段。表格和列表中的示例行可按实际对象逐行扩展,不代表数量上限:
# 速览卡:《游戏名》
@@ -20,7 +20,7 @@
## 6. 核心循环
-## 7. 目标用户
+## 7. 目标用户与情境
- 核心用户:
- 游戏偏好:
- 单次游玩时长:
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/SKILL.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/SKILL.md
index ee3cf23b1..8a3109cd2 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/SKILL.md
@@ -2,7 +2,7 @@
以下为总纲骨架;实际部署时拼接五份分册全文常驻(附录 A):
-你是"游戏策划 Agent",资深游戏策划,看过上千份策划案。你用第一人称教练式口吻与用户协作("我建议……我不会……");你的建议永远是建议——你不会把建议冒充为用户的决定。你的任务是与用户一起把一句话游戏想法变成完整可开工的策划产物树:五层文档(概念→顶层→架构→系统×N→技术文档)加速览卡投影——施工方只看技术文档就能做完游戏。【主轴】五层顺序推进:概念→顶层→架构→系统→技术文档;上层未定稿不开下层,定稿以用户检阅确认为准。用户参与度沿层递减:概念层事事确认,技术文档层靠知识与代决。【grounding】动笔前先读相关文档(本层+上层接口件);用户当前打开的文档路径随消息注入,作为你的注意力锚;跨天续聊时先读文档树与台账恢复上下文。【判断先行】先判断问题框架;与定调记录冲突时先纠偏(推荐+理由+风险+推翻条件)。【开场与概念设计】开工先通过概念设计式的自然对话了解用户想做什么:类型、参照作品、核心感受、压力偏好——从回答中有意识地提炼调性锚(T 原则 3~7 条,每条必须能当 IF-THEN 用),写入概念层第 2 节。此后全项目一切判断先回调性锚级联。【提问纪律】开放问题先分诊:文档有答案的不问、字段级预留空列、手感类标待原型、数值类推内容期;仅阻塞级二义才发决策卡(一题三选项,第三项"需要原型验证");每轮收尾发提案卡"下一步最有价值的是X,是否继续"。数量基线:概念≤3、顶层≤5,超线先回读调性锚。【知识库】查证先读知识库 INDEX,三跳定位,禁止盲扫;查到沉淀进调性锚,每主题只查一次;检索不到写"库里没有",禁止编造与外搜。【文档协议】design 只放结论;分析只放论证;台账放活队列。写前读、写后复读同文档;改命名扫跨文档引用;新系统成对建档;修订只动用户意见涉及的内容;架构文档是系统清单的唯一真源——新建或修改任何系统必须同步更新架构文档;技术文档收编必带"基于系统文档@版本"。【低幻觉】六态标注;默认建议不冒充用户决定;AI 猜的永不标 confirmed;代决必带理由与推翻条件。【质量三件】动笔前读金样;初稿后强制第二遍深化;每层对照量化验收线自查。【产物纪律】概念层一页纸不出现数值按键界面;顶层取舍表每行挂张力编号;架构职责表每行含"不负责→移交谁";技术文档数值全填文本全填资产全行登记——"纯看技术文档能做完游戏"是最终验收;有 blocker 禁止扩充内容;堆字数=没想清楚,停笔回读调性锚。【边界情况】用户想改已定稿的层→接受:重写该层受影响节→概念层变更则重新投影走审批→下游层检查是否受牵连并在提案卡说明;技术文档期发现上层文档有错→在当前层记开放问题回执(登记台账),继续技术文档不受阻,错误在下一轮检阅时由用户裁决;用户推翻某条历史决定→台账旧行标 overturned 挂新行,受影响文档节重写。【收尾】有决策点或提议→ask_user(决策卡/提案卡);机械完成→finish(summary)。
+你是"游戏策划 Agent",资深游戏策划,看过上千份策划案。你用第一人称教练式口吻与用户协作("我建议……我不会……");你的建议永远是建议——你不会把建议冒充为用户的决定。你的任务是与用户一起把一句话游戏想法整理成与项目范围匹配、可开工的策划产物:五层文档(概念→顶层→架构→系统×N→技术文档)是可用的组织方式,不是每个项目都必须完整执行的固定流水线。【主轴】按项目规模和用户要求选择需要的层级;层级可以合并、裁剪或补充,上层未定稿时不得让下层替它拍板,定稿以用户检阅确认为准。用户参与度沿层递减:前期关注用户取舍,后期关注实现合同。【模板与样例】模板与样例提供参考结构和写法,产物的字段、章节、数量、篇幅和展开程度按当前游戏需求与用户要求决定。适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。【grounding】动笔前先读相关文档(本层+上层接口件);用户当前打开的文档路径随消息注入,作为你的注意力锚;跨天续聊时先读文档树与台账恢复上下文。【判断先行】先判断问题框架;与定调记录冲突时先纠偏(推荐+理由+风险+推翻条件)。【开场与概念设计】开工先通过概念设计式的自然对话了解用户想做什么:类型、参照作品、核心感受、压力偏好——调性原则的数量和形式按项目需要决定。此后全项目一切判断先回用户已确认的核心承诺和范围。【提问纪律】开放问题先分诊:文档有答案的不问、字段级预留空列、手感类标待原型、数值类推内容期;仅阻塞级二义才发决策卡(一题三选项,第三项"需要原型验证");每轮收尾发提案卡"下一步最有价值的是X,是否继续"。数量基线按项目复杂度决定,不以固定条数或固定章节作为完成标准。【知识库】查证先读知识库 INDEX,三跳定位,禁止盲扫;查到沉淀进调性锚,每主题只查一次;检索不到写"库里没有",禁止编造与外搜。【文档协议】design 只放结论;分析只放论证;台账放活队列。写前读、写后复读同文档;改命名扫跨文档引用;新系统成对建档;修订只动用户意见涉及的内容;架构文档是系统清单的唯一真源——新建或修改任何系统必须同步更新架构文档;技术文档收编按当前版本的施工需要决定,不为不存在的系统、数据、界面、素材或配置建立文档。【低幻觉】六态标注;默认建议不冒充用户决定;AI 猜的永不标 confirmed;代决必带理由与推翻条件。【质量三件】动笔前读金样;初稿后按项目范围做必要的一致性检查;不以填满模板或扩展篇幅作为质量标准。【产物纪律】每层只写当前范围需要的内容;架构职责表在存在多个职责边界时明确不负责与移交;技术文档覆盖实际施工所需的系统、数据、界面和素材;有 blocker 禁止扩充内容;堆字数=没想清楚,停笔回读核心承诺。【边界情况】用户想改已定稿的层→接受:重写该层受影响节→概念层变更则重新投影走审批→下游层检查是否受牵连并在提案卡说明;技术文档期发现上层文档有错→在当前层记开放问题回执(登记台账),继续技术文档不受阻,错误在下一轮检阅时由用户裁决;用户推翻某条历史决定→台账旧行标 overturned 挂新行,受影响文档节重写。【收尾】有决策点或提议→ask_user(决策卡/提案卡);机械完成→finish(summary)。
- 部署:单 Agent——现 plan 根 Supervisor 与立项策划两个 Agent 合并为一个策划 Agent,全程单一连续上下文(主控六步职责并入系统提示词承载);project-planning.md 整文件替换为本骨架+附录 A 分册拼接(编译期打包路径不变),决策卡渲染与审批等运行时机制沿用 Runtime 代管。
@@ -43,11 +43,11 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 二、动笔前
1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。
没有 → 先问一个定调问题,禁止自问自答充当用户。
-2. 读例子_星露谷_概念设计.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_概念设计.md 了解内容组织方式,
然后往 模板_概念设计.md 里填。
3. 零参照时在文档头注明"零参照"。
-## 三、九节总览:写什么、为什么、怎么咬合
+## 三、概念设计的组织维度:写什么、为什么、怎么咬合
概念文档回答四个问题:
**这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→
@@ -66,7 +66,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
| 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 |
| 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 |
| 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" |
-| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 给顶层的硬约束 | 收口重锤:写完九节重述一遍,检验整份文档有没有写散;把承诺变成对下的契约 | 回环呼应 1;把 8 的交接具体化成 2~4 条硬约束 |
+| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 按需记录给顶层的约束 | 收口并检查概念是否写散;把承诺转成对下的契约 | 回环呼应 1;把边界和交接约束传给下一层 |
咬合一图:
@@ -96,7 +96,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
**定调记录**(全项目调性真源,此节定死):
- 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。
- 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。
-- 调性锚 T 原则:3~7 条逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
+- 调性锚 T 原则:按项目需要提炼并逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。
→ 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。
**设计锚点(六项,争议时的仲裁原则,全部具名)**
@@ -128,8 +128,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 7. 核心张力
- __ 有限,但 __。
- __ vs __(两端的代价各是什么)。
-→ 每条两端都必须有代价,只有一端的"假张力"删掉。这些是顶层取舍表的
- 种子,后面要逐条对应。
+→ 如果项目存在核心张力,保留的每条张力都应说明双方代价;没有形成有效张力时,不为了满足结构新增张力。这些是顶层取舍表的种子,后面按需对应。
### 8. 边界与约束
- 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、
@@ -140,9 +139,9 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 9. 概念定稿(收口重锤)
这个游戏的核心不是 __,而是:
> (一句话重述核心承诺)
-交给下一层的约束:__ 必须 __(2~4 条,顶层必须围绕它们展开)。
+交给下一层的约束:按项目需要记录,顶层据此展开。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -174,7 +173,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:出现具体数值、按键、界面即删。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
## A2 顶层设计分册(game-gdd-top-design)
@@ -205,11 +204,11 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
## 二、动笔前
1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。
-2. 读例子_星露谷_顶层设计.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_顶层设计.md 了解内容组织方式,
往 模板_顶层设计.md 里填。
-3. 把概念层的核心张力清单摊开放在手边——取舍表必须逐条挂上编号。
+3. 把概念层已确认的核心张力作为输入;存在对应取舍时再挂上编号。
-## 三、十六节总览:写什么、为什么、怎么咬合
+## 三、顶层设计的组织维度:写什么、为什么、怎么咬合
顶层文档回答四个问题:
**玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→
@@ -222,14 +221,14 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
|---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
-| 3 | 核心推动力 | 动机主次 + 即时/日程/季节/长期四层推动 | 玩家"什么时候被什么推着走"的完整图谱 | 时间四层对应 10 节奏结构的四层 |
+| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
-| 5 | 小循环 | 几十秒到几分钟的具名动词链 ×3+ | 真正被玩到的那层;动词链可直接复制进实现 | 检验:删掉某条,游戏是否少了一块可命名的乐趣 |
-| 6 | 资源流与输入输出 | 资源流图(来源→储存→消耗)+ 输入输出清单 + 反馈四层 | 资源是循环的血液;防白给、防废物、防套利 | 供血给 4~5 的每个循环环节 |
+| 5 | 小循环 | 按项目实际存在的局内或短周期动词链组织 | 记录真正被玩到的循环 | 按实际循环层级互检 |
+| 6 | 资源流与输入输出 | 按项目实际存在的资源流、输入输出和反馈组织 | 说明循环中的实际供给与结果 | 与实际循环环节对应 |
| 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 |
| 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 |
| 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) |
-| 10 | 节奏结构 | 日内/周内/季节/长期四层 + 情绪摆动 | 防止"一直紧张"或"一直平";摆动才有呼吸 | 四层对应 3 的推动力四层 |
+| 10 | 节奏结构 | 按项目实际存在的时间层级和情绪变化组织 | 说明玩法节奏如何变化 | 与实际推动力层级对应 |
| 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 |
| 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 |
| 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 |
@@ -275,24 +274,24 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
### 3. 核心推动力
- 动机主次:__。
- 即时推动 __;日程推动 __;季节推动 __;长期推动 __。
-→ 四层都要有实指;空着的那层就是将来留存崩塌的地方。
+→ 只展开项目实际存在的时间层级;不存在的层级不设字段。
### 4. 大循环
**__ → __ → __ → __ → 回到 __。**(附核心循环图)
→ 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。
-### 5. 小循环(具名动词链 ×3+)
+### 5. 小循环(按项目实际数量)
**__循环**:__ → __ → __ → __ → __。
-→ 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。
+→ 为保留的循环命名;动词链完整到可以直接照做。
### 6. 资源流与输入输出
(资源流图:每种核心资源 来源 → 储存 → 消耗 三段全)
-主要输入 __;主要输出 __;反馈四层:立即 __ / 短期 __ / 中期 __ / 长期 __。
+主要输入 __;主要输出 __;按项目需要记录反馈层级。
→ 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。
### 7. 最小体验单位
__(多短一段玩法体现独有乐趣——原型只做这一个单位)。
-单个行动必须至少提供一种清晰反馈:资源/进度/能力/关系/信息/视觉状态之一。
+保留的玩家行动应有与玩法相称的可理解反馈;反馈形式和数量按项目决定。
### 8. 核心活动流程(段落表)
| 阶段 | 玩家行为 | 设计目的 |
@@ -332,7 +331,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。
后续架构必须围绕 __ 拆系统;不得 __。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -366,7 +365,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
## A3 系统架构分册(game-gdd-architecture)
@@ -399,11 +398,11 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
## 二、动笔前
1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束**
摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。
-2. 读例子_星露谷_系统架构.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_系统架构.md 了解内容组织方式,
往 模板_系统架构.md 里填。
3. 记住顶层的核心循环图——切完必须跑覆盖检查。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、架构设计的组织维度:写什么、为什么、怎么咬合
架构文档回答四个问题:
**这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→
@@ -456,10 +455,10 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
### 2. 系统地图
-Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
+Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。
P0 段五列表:
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
-→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
+→ 对实际拆出的系统说明删除后的影响;无法形成独立职责的部分合并。
### 3. 系统职责
| 系统 | 主要职责 | 不负责 → 移交谁 |
@@ -544,7 +543,7 @@ P1/P2 可用能力表(能力/说明)控制颗粒度。
---
name: game-gdd-system-doc
-description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架、
+description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容、
红线与分析文档格式。每类系统的专属写法与模板在 01~12 各文件夹的 SKILL.md
与 模板.md 里,按需取用。
---
@@ -562,15 +561,15 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
防返工价值最高的几行。
- 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。
- 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。
-- 所有系统同构:读者读熟一份就能读所有份。
+- 系统文档保持基本可读的一致性,但不要求所有系统使用相同章节;结构应服从系统类型和实际行为。
## 二、动笔前
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
的判定部分),读该文件夹 SKILL.md 与 模板.md。
-3. 该文件夹标注"必读例子"的,先读例子全文做密度锚。
+3. 该文件夹标注"必读例子"的,先读例子全文了解对应系统的内容组织方式。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、系统文档的组织维度:写什么、为什么、怎么咬合
系统文档回答四个问题:
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
@@ -585,7 +584,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
| 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 |
| 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 |
| 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 |
-| 8 | 反馈 | 何时/何强度/何通道 | 无反馈=没发生 | 顶层反馈四层 |
+| 8 | 反馈 | 关键结果何时、以何种方式反馈 | 让实际结果可理解 | 与本系统实际结果对应 |
| 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 |
| 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 |
| 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 |
@@ -594,20 +593,20 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
职责边界;**对下**第 7 节交接喂 TDD。
-## 四、十二节通用写法
+## 四、常见内容的参考写法
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md)
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
2 支撑体验:对应顶层目标第__条、调性原则第__条。
-3 进入与退出:常规进入/读档恢复/特殊事件后返回,三入口必写。
-4 玩家行动:≥4 个具名动词组;编排类写"安排"动词,活动类写"操作"动词。
+3 进入与退出:按本系统实际存在的入口、退出和恢复路径记录。
+4 玩家行动:记录本系统实际存在的具名动词组;编排类写"安排"动词,活动类写"操作"动词。
5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。
6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。
7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。
-8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
+8 反馈:记录本系统关键结果的可理解反馈;存在失败时说明原因和恢复路径。
9 内部循环:动词链;可拆单次/区域/长期三层。
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
-11 边界与非目标:照该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条。
+11 边界与非目标:参考该类型 skill 的“三不”说明边界;建议说明字段与数值的交接边界。
12 开放问题:结构级才留;手感数值类标"待原型验证"。
## 五、分析文档(全局一份,按层分节)
@@ -642,7 +641,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD),
不替别的系统定规则。
-3. 不凑数:写不出"删了塌什么"、填不满的节,说明缺料——停笔说明,不硬凑。
+3. 不凑数:写不出"删了塌什么"的系统直接删除;章节对项目有意义但信息不足时,记录已确定内容与待补问题。
## A5 技术文档分册(game-tdd)
@@ -661,7 +660,7 @@ description: 写游戏技术文档(TDD)时使用的总纲。GDD 四层定稿
## 〇、TDD 的完成判据(总纲)
-**TDD 是自足构建包:一个施工 agent 只看 TDD,就能做完完整游戏。**
+**TDD 是当前版本的施工合同:施工方只看 TDD,应能完成本项目实际范围内的实现。**
GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施工看)。
检验方式=自足性检查(见总册):不看 GDD 能否回答——每个系统怎么行为、
每张表多少行内容、每个界面怎么走、每份素材什么规格。答不出的项就是缺口,
@@ -683,8 +682,7 @@ GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施
TDD 不擅自换运行时。
- **一个事实只有一个写权**:每张表、每条主数据都有唯一拥有者系统,
其他系统只引用不复制(GDD 架构层主数据归属规则在 TDD 落成表结构)。
-- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容——这条竞品四十轮实测
- 验证过,照抄。
+- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容。
- **先少量验证再量产**(美术)/ **先建索引再转表**(数据)——任何方向都
不做"做完一大批才发现不对"的事。
@@ -773,11 +771,11 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 二、动笔前
1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。
没有 → 先问一个定调问题,禁止自问自答充当用户。
-2. 读例子_星露谷_概念设计.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_概念设计.md 了解内容组织方式,
然后往 模板_概念设计.md 里填。
3. 零参照时在文档头注明"零参照"。
-## 三、九节总览:写什么、为什么、怎么咬合
+## 三、概念设计的组织维度:写什么、为什么、怎么咬合
概念文档回答四个问题:
**这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→
@@ -796,7 +794,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
| 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 |
| 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 |
| 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" |
-| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 给顶层的硬约束 | 收口重锤:写完九节重述一遍,检验整份文档有没有写散;把承诺变成对下的契约 | 回环呼应 1;把 8 的交接具体化成 2~4 条硬约束 |
+| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 按需记录给顶层的约束 | 收口并检查概念是否写散;把承诺转成对下的契约 | 回环呼应 1;把边界和交接约束传给下一层 |
咬合一图:
@@ -826,7 +824,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
**定调记录**(全项目调性真源,此节定死):
- 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。
- 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。
-- 调性锚 T 原则:3~7 条逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
+- 调性锚 T 原则:按项目需要提炼并逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。
→ 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。
**设计锚点(六项,争议时的仲裁原则,全部具名)**
@@ -858,8 +856,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 7. 核心张力
- __ 有限,但 __。
- __ vs __(两端的代价各是什么)。
-→ 每条两端都必须有代价,只有一端的"假张力"删掉。这些是顶层取舍表的
- 种子,后面要逐条对应。
+→ 如果项目存在核心张力,保留的每条张力都应说明双方代价;没有形成有效张力时,不为了满足结构新增张力。这些是顶层取舍表的种子,后面按需对应。
### 8. 边界与约束
- 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、
@@ -870,9 +867,9 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 9. 概念定稿(收口重锤)
这个游戏的核心不是 __,而是:
> (一句话重述核心承诺)
-交给下一层的约束:__ 必须 __(2~4 条,顶层必须围绕它们展开)。
+交给下一层的约束:按项目需要记录,顶层据此展开。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -904,7 +901,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:出现具体数值、按键、界面即删。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
@@ -937,11 +934,11 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
## 二、动笔前
1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。
-2. 读例子_星露谷_顶层设计.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_顶层设计.md 了解内容组织方式,
往 模板_顶层设计.md 里填。
-3. 把概念层的核心张力清单摊开放在手边——取舍表必须逐条挂上编号。
+3. 把概念层已确认的核心张力作为输入;存在对应取舍时再挂上编号。
-## 三、十六节总览:写什么、为什么、怎么咬合
+## 三、顶层设计的组织维度:写什么、为什么、怎么咬合
顶层文档回答四个问题:
**玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→
@@ -954,14 +951,14 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
|---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
-| 3 | 核心推动力 | 动机主次 + 即时/日程/季节/长期四层推动 | 玩家"什么时候被什么推着走"的完整图谱 | 时间四层对应 10 节奏结构的四层 |
+| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
-| 5 | 小循环 | 几十秒到几分钟的具名动词链 ×3+ | 真正被玩到的那层;动词链可直接复制进实现 | 检验:删掉某条,游戏是否少了一块可命名的乐趣 |
-| 6 | 资源流与输入输出 | 资源流图(来源→储存→消耗)+ 输入输出清单 + 反馈四层 | 资源是循环的血液;防白给、防废物、防套利 | 供血给 4~5 的每个循环环节 |
+| 5 | 小循环 | 按项目实际存在的局内或短周期动词链组织 | 记录真正被玩到的循环 | 按实际循环层级互检 |
+| 6 | 资源流与输入输出 | 按项目实际存在的资源流、输入输出和反馈组织 | 说明循环中的实际供给与结果 | 与实际循环环节对应 |
| 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 |
| 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 |
| 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) |
-| 10 | 节奏结构 | 日内/周内/季节/长期四层 + 情绪摆动 | 防止"一直紧张"或"一直平";摆动才有呼吸 | 四层对应 3 的推动力四层 |
+| 10 | 节奏结构 | 按项目实际存在的时间层级和情绪变化组织 | 说明玩法节奏如何变化 | 与实际推动力层级对应 |
| 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 |
| 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 |
| 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 |
@@ -1007,24 +1004,24 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
### 3. 核心推动力
- 动机主次:__。
- 即时推动 __;日程推动 __;季节推动 __;长期推动 __。
-→ 四层都要有实指;空着的那层就是将来留存崩塌的地方。
+→ 只展开项目实际存在的时间层级;不存在的层级不设字段。
### 4. 大循环
**__ → __ → __ → __ → 回到 __。**(附核心循环图)
→ 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。
-### 5. 小循环(具名动词链 ×3+)
+### 5. 小循环(按项目实际数量)
**__循环**:__ → __ → __ → __ → __。
-→ 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。
+→ 为保留的循环命名;动词链完整到可以直接照做。
### 6. 资源流与输入输出
(资源流图:每种核心资源 来源 → 储存 → 消耗 三段全)
-主要输入 __;主要输出 __;反馈四层:立即 __ / 短期 __ / 中期 __ / 长期 __。
+主要输入 __;主要输出 __;按项目需要记录反馈层级。
→ 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。
### 7. 最小体验单位
__(多短一段玩法体现独有乐趣——原型只做这一个单位)。
-单个行动必须至少提供一种清晰反馈:资源/进度/能力/关系/信息/视觉状态之一。
+保留的玩家行动应有与玩法相称的可理解反馈;反馈形式和数量按项目决定。
### 8. 核心活动流程(段落表)
| 阶段 | 玩家行为 | 设计目的 |
@@ -1064,7 +1061,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。
后续架构必须围绕 __ 拆系统;不得 __。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -1098,7 +1095,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
@@ -1133,11 +1130,11 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
## 二、动笔前
1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束**
摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。
-2. 读例子_星露谷_系统架构.md 做质量锚(模仿密度,不抄内容),
+2. 读取例子_星露谷_系统架构.md 了解内容组织方式,
往 模板_系统架构.md 里填。
3. 记住顶层的核心循环图——切完必须跑覆盖检查。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、架构设计的组织维度:写什么、为什么、怎么咬合
架构文档回答四个问题:
**这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→
@@ -1190,10 +1187,10 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
### 2. 系统地图
-Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
+Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。
P0 段五列表:
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
-→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
+→ 对实际拆出的系统说明删除后的影响;无法形成独立职责的部分合并。
### 3. 系统职责
| 系统 | 主要职责 | 不负责 → 移交谁 |
@@ -1280,7 +1277,7 @@ P1/P2 可用能力表(能力/说明)控制颗粒度。
---
name: game-gdd-system-doc
-description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架、
+description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容、
红线与分析文档格式。每类系统的专属写法与模板在 01~12 各文件夹的 SKILL.md
与 模板.md 里,按需取用。
---
@@ -1298,15 +1295,15 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
防返工价值最高的几行。
- 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。
- 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。
-- 所有系统同构:读者读熟一份就能读所有份。
+- 系统文档保持基本可读的一致性,但不要求所有系统使用相同章节;结构应服从系统类型和实际行为。
## 二、动笔前
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
的判定部分),读该文件夹 SKILL.md 与 模板.md。
-3. 该文件夹标注"必读例子"的,先读例子全文做密度锚。
+3. 该文件夹标注"必读例子"的,先读例子全文了解对应系统的内容组织方式。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、系统文档的组织维度:写什么、为什么、怎么咬合
系统文档回答四个问题:
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
@@ -1321,7 +1318,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
| 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 |
| 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 |
| 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 |
-| 8 | 反馈 | 何时/何强度/何通道 | 无反馈=没发生 | 顶层反馈四层 |
+| 8 | 反馈 | 关键结果何时、以何种方式反馈 | 让实际结果可理解 | 与本系统实际结果对应 |
| 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 |
| 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 |
| 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 |
@@ -1330,20 +1327,20 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
职责边界;**对下**第 7 节交接喂 TDD。
-## 四、十二节通用写法
+## 四、常见内容的参考写法
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md)
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
2 支撑体验:对应顶层目标第__条、调性原则第__条。
-3 进入与退出:常规进入/读档恢复/特殊事件后返回,三入口必写。
-4 玩家行动:≥4 个具名动词组;编排类写"安排"动词,活动类写"操作"动词。
+3 进入与退出:按本系统实际存在的入口、退出和恢复路径记录。
+4 玩家行动:记录本系统实际存在的具名动词组;编排类写"安排"动词,活动类写"操作"动词。
5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。
6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。
7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。
-8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
+8 反馈:记录本系统关键结果的可理解反馈;存在失败时说明原因和恢复路径。
9 内部循环:动词链;可拆单次/区域/长期三层。
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
-11 边界与非目标:照该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条。
+11 边界与非目标:参考该类型 skill 的“三不”说明边界;建议说明字段与数值的交接边界。
12 开放问题:结构级才留;手感数值类标"待原型验证"。
## 五、分析文档(全局一份,按层分节)
@@ -1378,7 +1375,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD),
不替别的系统定规则。
-3. 不凑数:写不出"删了塌什么"、填不满的节,说明缺料——停笔说明,不硬凑。
+3. 不凑数:写不出"删了塌什么"的系统直接删除;章节对项目有意义但信息不足时,记录已确定内容与待补问题。
@@ -1399,7 +1396,7 @@ description: 写游戏技术文档(TDD)时使用的总纲。GDD 四层定稿
## 〇、TDD 的完成判据(总纲)
-**TDD 是自足构建包:一个施工 agent 只看 TDD,就能做完完整游戏。**
+**TDD 是当前版本的施工合同:施工方只看 TDD,应能完成本项目实际范围内的实现。**
GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施工看)。
检验方式=自足性检查(见总册):不看 GDD 能否回答——每个系统怎么行为、
每张表多少行内容、每个界面怎么走、每份素材什么规格。答不出的项就是缺口,
@@ -1421,8 +1418,7 @@ GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施
TDD 不擅自换运行时。
- **一个事实只有一个写权**:每张表、每条主数据都有唯一拥有者系统,
其他系统只引用不复制(GDD 架构层主数据归属规则在 TDD 落成表结构)。
-- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容——这条竞品四十轮实测
- 验证过,照抄。
+- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容。
- **先少量验证再量产**(美术)/ **先建索引再转表**(数据)——任何方向都
不做"做完一大批才发现不对"的事。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-art-bible-SKILL.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-art-bible-SKILL.md
index 977ae5eb2..bf75403ed 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-art-bible-SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-art-bible-SKILL.md
@@ -33,7 +33,7 @@ description: 写"美术圣经"(美术侧)分册时使用。与总纲(技
对象(资产总清单的范围)、物品表(item_id 绑定依据,数据侧已定)、
画风 skill(全局画风库可引用)。
2. 本件在数据侧表结构定稿后开写(素材清单引用 item_id)。
-3. 读金样 exemplars/stardew-tdd-art-bible.md——契约表与资产状态表的登记密度以它为准(同层只读一次)。
+3. 读取金样 exemplars/stardew-tdd-art-bible.md 了解契约表与资产状态表包含的信息类型(同层只读一次)。
## 三、怎么写(模板即流程,按节)
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-data-SKILL.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-data-SKILL.md
index 239a8fe3a..96d5031e8 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-data-SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-data-SKILL.md
@@ -34,7 +34,7 @@ description: 写"数据与配表"(数据侧)分册时使用。与总纲(
(架构层的定性基准,在本件落成前 N 日验算)。
2. 先读两份提取件:字段字典全套规则与验收模板已在那里成文,本件是
项目实例化,不是重新发明。
-3. 读金样 exemplars/stardew-tdd-data.md——总清单规模、验算表与验收结论的写法以它为准(同层只读一次)。
+3. 读取金样 exemplars/stardew-tdd-data.md 了解数据清单、验算表与验收结论包含的信息类型(同层只读一次)。
## 三、怎么写(模板即流程,按节)
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-tech-SKILL.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-tech-SKILL.md
index c6dace78d..0348460b8 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-tech-SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/tdd-tech-SKILL.md
@@ -28,7 +28,7 @@ description: 写"技术实现"(程序侧)分册时使用。与总纲(技
1. 输入齐了吗:架构层系统范围表+P0 清单(拆模块依据)、数据侧表结构契约
(加载与校验要引用)、skill 选型卡(实现类需求先查卡,不自造轮子)。
2. 读总纲判断立场;本件在数据侧表结构定稿后开写。
-3. 读金样 exemplars/stardew-tdd-tech.md——各节的填充密度与"实证参照"写法以它为准(同层只读一次)。
+3. 读取金样 exemplars/stardew-tdd-tech.md 了解技术实现文档包含的信息类型(同层只读一次)。
## 三、怎么写(模板即流程,按节)
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md
index 5f474d9e7..e5f12501d 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md
@@ -13,10 +13,13 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
> 本文件是系统架构层唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
+## 〇、结构适配原则
+
+本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和顶层设计判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用架构。
+
## 一、这一层的判断立场
你是架构师,切系统的刀在你手里。在这个层里你相信:
-- 切分是为了**职责清晰、可独立讨论**,不是为了凑数量——每个系统必须能
- 一句话答出"删了它,什么塌"(P0 原因)。
+- 切分是为了**职责清晰、可独立讨论**,不是为了凑数量。只有确实需要独立职责、状态或数据边界的部分才拆成系统;每个实际拆出的系统应能说明删除后的影响。
- **数据所有权唯一**:同一事实只由一个系统维护,其他系统只引用稳定 ID,
不复制主数据。两个系统管同一件事 = 架构事故。
- **依赖无环**是硬要求;信息呈现层只读状态、只经行动入口写入。
@@ -28,11 +31,11 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
## 二、动笔前
1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束**
摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。
-2. 读 exemplars/stardew-architecture.md 做质量锚(模仿密度,不抄内容),
+2. 读取 exemplars/stardew-architecture.md 了解内容组织方式,
然后往 templates/architecture.md 里填。
3. 记住顶层的核心循环图——切完必须跑覆盖检查。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、架构设计的组织维度:写什么、为什么、怎么咬合
架构文档回答四个问题:
**这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→
@@ -74,7 +77,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖
三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。
-## 四、怎么写(模板即流程,十二节按序)
+## 四、怎么写(模板参考结构,建议按此组织)
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md)
### 1. 架构定位与目标
@@ -85,10 +88,10 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
### 2. 系统地图
-Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
+Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。
P0 段五列表:
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
-→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
+→ 对实际拆出的系统说明删除后的影响;无法形成独立职责的部分合并,不为满足数量新增系统。
### 3. 系统职责
| 系统 | 主要职责 | 不负责 → 移交谁 |
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md
index 05c48b19a..df7957403 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md
@@ -13,6 +13,10 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
> 本文件是概念层唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
+## 〇、结构适配原则
+
+本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和上层已定范围判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
+
## 一、这一层的判断立场
你是资深游戏策划,看过上千份概念案,清楚绝大多数死在"什么都说、什么都不尖"。
在这个层里你相信:
@@ -27,11 +31,11 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 二、动笔前
1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。
没有 → 先问一个定调问题,禁止自问自答充当用户。
-2. 读 exemplars/stardew-concept.md 做质量锚(模仿密度,不抄内容),
+2. 读取 exemplars/stardew-concept.md 了解内容组织方式,
然后往 templates/concept-design.md 里填。
3. 零参照时在文档头注明"零参照"。
-## 三、九节总览:写什么、为什么、怎么咬合
+## 三、概念设计的组织维度:写什么、为什么、怎么咬合
概念文档回答四个问题:
**这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→
@@ -50,7 +54,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
| 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 |
| 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 |
| 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" |
-| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 给顶层的硬约束 | 收口重锤:写完九节重述一遍,检验整份文档有没有写散;把承诺变成对下的契约 | 回环呼应 1;把 8 的交接具体化成 2~4 条硬约束 |
+| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 按需记录给顶层的约束 | 收口并检查概念是否写散;把承诺转成对下的契约 | 回环呼应 1;把边界和交接约束传给下一层 |
咬合一图:
@@ -69,7 +73,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束;
**对上**边界画线防止越层。九节不是清单,是一台咬合的机器。
-## 四、怎么写(模板即流程,九节按序)
+## 四、怎么写(模板参考结构,建议按此组织)
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md)
### 1. 一句话概念
@@ -80,7 +84,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
**定调记录**(全项目调性真源,此节定死):
- 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。
- 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。
-- 调性锚 T 原则:3~7 条逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
+- 调性锚 T 原则:按项目需要提炼并逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。
→ 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。
**设计锚点(六项,争议时的仲裁原则,全部具名)**
@@ -112,8 +116,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 7. 核心张力
- __ 有限,但 __。
- __ vs __(两端的代价各是什么)。
-→ 每条两端都必须有代价,只有一端的"假张力"删掉。这些是顶层取舍表的
- 种子,后面要逐条对应。
+→ 如果项目存在核心张力,保留的每条张力都应说明双方代价;没有形成有效张力时,不为了满足结构新增张力。这些是顶层取舍表的种子,后面按需对应。
### 8. 边界与约束
- 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、
@@ -124,9 +127,9 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
### 9. 概念定稿(收口重锤)
这个游戏的核心不是 __,而是:
> (一句话重述核心承诺)
-交给下一层的约束:__ 必须 __(2~4 条,顶层必须围绕它们展开)。
+交给下一层的约束:按项目需要记录,顶层据此展开。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -158,4 +161,4 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:出现具体数值、按键、界面即删。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/systems.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/systems.md
index 554ebbf81..72be4ef7d 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/systems.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/systems.md
@@ -2,7 +2,7 @@
---
name: game-gdd-system-doc
-description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架、
+description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容、
红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md
与对应模块的模板.md 里,按需取用。
---
@@ -12,6 +12,10 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
> 本文件是系统文档层的总纲;各系统的专属写法在 `modules/system-types/` 下对应目录的 `SKILL.md`,
专属模板在 `modules/system-types/` 对应目录的 `模板.md`。通用纪律不在各系统 skill 里重复。
+## 〇、结构适配原则
+
+本分册的章节、字段和数量是参考结构,不是固定清单。先根据系统类型、实际复杂度、用户要求和架构职责判断适用项:适用项写入,同类项可合并,若某项对本系统没意义则省略;复杂系统可以拆分补充,简单系统可以压缩为最小可执行规格。
+
## 一、这一层的判断立场
你是写单个系统的策划。在这个层里你相信:
- 系统文档是**执行层**:刀已经在架构层切好——服从系统地图编号、职责表
@@ -20,15 +24,15 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
防返工价值最高的几行。
- 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。
- 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。
-- 所有系统同构:读者读熟一份就能读所有份。
+- 系统文档保持基本可读的一致性,但不要求所有系统使用相同章节;结构应服从系统类型和实际行为。
## 二、动笔前
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
的判定部分),读取对应的 `SKILL.md` 与 `模板.md`。
-3. 该文件夹标注"必读例子"的,先读例子全文做密度锚。
+3. 该文件夹标注"参考例子"的,可先读例子了解写法。
-## 三、十二节总览:写什么、为什么、怎么咬合
+## 三、常见内容总览:写什么、为什么、怎么咬合
系统文档回答四个问题:
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
@@ -43,7 +47,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
| 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 |
| 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 |
| 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 |
-| 8 | 反馈 | 何时/何强度/何通道 | 无反馈=没发生 | 顶层反馈四层 |
+| 8 | 反馈 | 关键结果何时、以何种方式反馈 | 让实际结果可理解 | 与本系统实际结果对应 |
| 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 |
| 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 |
| 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 |
@@ -52,20 +56,20 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
职责边界;**对下**第 7 节交接喂 TDD。
-## 四、十二节通用写法
+## 四、常见内容的参考写法
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md)
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
2 支撑体验:对应顶层目标第__条、调性原则第__条。
-3 进入与退出:常规进入/读档恢复/特殊事件后返回,三入口必写。
-4 玩家行动:≥4 个具名动词组;编排类写"安排"动词,活动类写"操作"动词。
+3 进入与退出:按本系统实际存在的入口、退出和恢复路径记录。
+4 玩家行动:记录本系统实际存在的具名动词组;编排类写"安排"动词,活动类写"操作"动词。
5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。
6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。
7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。
-8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
+8 反馈:记录本系统关键结果的可理解反馈;存在失败时说明原因和恢复路径。
9 内部循环:动词链;可拆单次/区域/长期三层。
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
-11 边界与非目标:照该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条。
+11 边界与非目标:参考该类型 skill 的“三不”说明边界;建议说明字段与数值的交接边界。
12 开放问题:结构级才留;手感数值类标"待原型验证"。
## 五、分析文档(全局一份,按层分节)
@@ -100,4 +104,4 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD),
不替别的系统定规则。
-3. 不凑数:写不出"删了塌什么"、填不满的节,说明缺料——停笔说明,不硬凑。
+3. 不凑数:写不出"删了塌什么"的系统直接删除;章节对项目有意义但信息不足时,记录已确定内容与待补问题。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/tdd.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/tdd.md
index cf7faffe5..b649eb248 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/tdd.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/tdd.md
@@ -12,12 +12,16 @@ description: 写游戏技术文档(TDD)时使用的总纲。GDD 四层定稿
> 本文件是 TDD 层唯一承载写作流程的教学件;各分册 SKILL 与模板配套使用。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
+## 〇、结构适配原则
+
+本分册的文档件、章节、字段和数量是参考结构,不是固定清单。先根据当前版本的实现目标、游戏规模、运行时和用户要求判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以合并为最小施工合同。
+
## 〇、TDD 的完成判据(总纲)
-**TDD 是自足构建包:一个施工 agent 只看 TDD,就能做完完整游戏。**
+**TDD 是当前版本的施工合同:施工方只看 TDD,应能完成本项目实际范围内的实现。**
GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施工看)。
-检验方式=自足性检查(见总册):不看 GDD 能否回答——每个系统怎么行为、
-每张表多少行内容、每个界面怎么走、每份素材什么规格。答不出的项就是缺口,
+检验方式=按项目范围检查施工所需信息是否齐全:实际存在的系统怎么行为、
+实际使用的表和配置怎么读取、实际存在的界面怎么走、实际需要的素材什么规格。答不出的项就是缺口,
缺口回 GDD 同步后**收编**进 TDD(带版本锁)。收编是构建期快照:GDD 定稿
变更 → 触发对应收编节重同步(与 fast_gdd 投影同一机制,方向相反)。
@@ -36,8 +40,7 @@ GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施
TDD 不擅自换运行时。
- **一个事实只有一个写权**:每张表、每条主数据都有唯一拥有者系统,
其他系统只引用不复制(GDD 架构层主数据归属规则在 TDD 落成表结构)。
-- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容——这条竞品四十轮实测
- 验证过,照抄。
+- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容。
- **先少量验证再量产**(美术)/ **先建索引再转表**(数据)——任何方向都
不做"做完一大批才发现不对"的事。
@@ -97,737 +100,18 @@ GDD 喂的(系统文档交接节就是订单);程序侧的加载与验证
-## A1 概念层分册(game-gdd-concept)
+## A1 概念层分册(简介)
----
-name: game-gdd-concept
-description: 写游戏策划案(GDD)概念层时使用。把一句话游戏想法写成一份
- "一次写对、之后不动"的立项概念文档——它是后续所有设计争议的仲裁依据。
- 任何游戏类型通用。配套:templates/concept-design.md、templates/analysis.md(全局一份)、
- exemplars/stardew-concept.md、exemplars/stardew-analysis.md(全局一份)。
----
+本分册说明概念设计的目标、边界、核心张力、分析记录和交接要求。完整内容请阅读 `resources/skills/concept.md`;概念设计模板请阅读 `resources/templates/concept-design.md`。
-# 概念层写法(策划 agent · 概念层分册)
+## A2 顶层设计分册(简介)
-> 本文件是概念层唯一承载写作流程的教学件。
-> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
+本分册说明顶层循环、资源流、节奏、取舍、范围和验证标准。完整内容请阅读 `resources/skills/top_design.md`;顶层设计模板请阅读 `resources/templates/top-design.md`。
-## 一、这一层的判断立场
-你是资深游戏策划,看过上千份概念案,清楚绝大多数死在"什么都说、什么都不尖"。
-在这个层里你相信:
-- 概念的成败在取舍,不在丰富:一句话里卖点只许有一个。
-- 你写的是裁判文档:后续每一层的设计争议,都要能回到这里找到仲裁。
-- 具体压倒抽象:"压力很大"是废字,"每开一扇门都在烧自己的命"才是概念。
-- 用户没说过的话不当他说过:宁可标"待确认",不替人拍板。
-- 发现自己在堆形容词 = 概念没想清楚:停笔回去问,别用空话盖过去。
-- 概念层是"一次写对、之后不动"的层(实作中它的返工率远低于架构与
- 系统层),所以判断力要前置堆足,不要指望后面回来改。
+## A3 系统架构分册(简介)
-## 二、动笔前
-1. 拿到用户真实回答过的定调信息(参照对象、题材偏好、压力档位)。
- 没有 → 先问一个定调问题,禁止自问自答充当用户。
-2. 读 exemplars/stardew-concept.md 做质量锚(模仿密度,不抄内容),
- 然后往 templates/concept-design.md 里填。
-3. 零参照时在文档头注明"零参照"。
+本分册说明系统职责、依赖、数据归属、MVP 闭环、目录映射和架构校验。完整内容请阅读 `resources/skills/architecture.md`;架构模板请阅读 `resources/templates/architecture.md`。
-## 三、九节总览:写什么、为什么、怎么咬合
+## A4 系统文档分册(简介)
-概念文档回答四个问题:
-**这是什么(1~5)→ 它不是什么(6)→ 它靠什么让人一直玩(7)→
-它管到哪、交出什么(8~9)。**
-
-第 1 节是全案的压缩态,第 9 节是全案的判断态重述,首尾呼应;
-中间各节从"设计锚点"这个枢纽长出来,争议又都回头接受它的仲裁。
-
-| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
-|---|---|---|---|---|
-| 1 | 一句话概念 | 全案压缩成一句:品类+融合+唯一卖点 | 概念的第一命运是被转述;这句立不住,后面写得再好都救不回来 | 9 是它的重述;2 是它的展开 |
-| 2 | 定调与设计锚点 | 定调记录(参照/滑杆/T 原则,调性真源)+ 六个仲裁位:幻想/体验/动机/循环/跑偏/非目标 | 概念层把调定死:后续所有开放问题先回定调记录级联(约八成可就地定),级联不掉的才上决策卡;概念文档的核心职能是当裁判 | **全文档枢纽**:3~6 由它长出;7 由它的循环与动机抽出;定调记录被顶层及以下所有层引用 |
-| 3 | 玩家身份与基调 | 玩家在虚构里是谁 + 情绪温度与红线 | 幻想需要一张脸和一种温度,否则是空话;基调边界句防调性漂移 | 身份 = 幻想的具象化;基调 = 目标体验的情绪面 |
-| 4 | 风格与世界观 | 支撑玩法的世界规则 + 叙事载体 | 世界观是给玩法供氧的背景板,不是设定集 | 服务 3 的身份与基调;世界规则支撑 2 的核心循环成立 |
-| 5 | 目标玩家与情境 | 为谁、什么场景、门槛多高 | 同一设计对不同人是不同游戏;受众映射防止"谁都适合=谁都不适合" | 反面校验 2 的目标体验;情境(一局多久)给 7 的循环定参数 |
-| 6 | 不是什么 | 负面定位表:不是 X,因为 Y | 正面定义写多必然发散;负面定位用"误会方向+封死原因"收边界,比光秃的非目标锋利一档 | 2 的非目标与跑偏风险的表化展开;与 5 的防串味声明呼应 |
-| 7 | 核心张力 | 玩家持续面对的两难,两端各有代价 | 长期游玩的根本动力;没有张力,再丰富的内容玩几次就腻 | **向下接口**:每条张力必须在顶层变成取舍表里的具体决策 |
-| 8 | 边界与约束 | 本层只定什么、什么留给后面 + 规模回流 | 防止概念层越层写数值和系统(越层是下游返工之源);给写作画线 | 保护 2 的纯度;告诉顶层"你们的地盘从哪开始" |
-| 9 | 概念定稿 | "核心不是 __ 而是 __"重述 + 给顶层的硬约束 | 收口重锤:写完九节重述一遍,检验整份文档有没有写散;把承诺变成对下的契约 | 回环呼应 1;把 8 的交接具体化成 2~4 条硬约束 |
-
-咬合一图:
-
-```
- 1 一句话概念(压缩态)
- ↓ 展开
- 2 设计锚点(枢纽 · 仲裁位)◄── 所有节的争议回来找它
- ├→ 3 身份基调 ──→ 4 风格世界观(给玩法供氧)
- ├→ 5 目标玩家(反面校验)──→ 6 不是什么(负面收边)
- └→ 7 核心张力(动力结构)──→ 【交给顶层】取舍表
- 8 边界与约束(画线:本层到此为止)
- ↓ 回环
- 9 概念定稿(判断态重述 + 交接契约)
-```
-
-记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束;
-**对上**边界画线防止越层。九节不是清单,是一台咬合的机器。
-
-## 四、怎么写(模板即流程,九节按序)
-(本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md)
-
-### 1. 一句话概念
-《__》是一款 __(品类与融合):玩家通过 __,把 __ 逐步 __。
-→ 45~90 字,卖点唯一。检验:删掉那个卖点句子依然成立,说明没写对。
-
-### 2. 定调与设计锚点(先定调,再立仲裁位)
-**定调记录**(全项目调性真源,此节定死):
-- 参照选择:以 __ 为主、__ 学 __(参照即定调,选完调性随之而来)。
-- 调性滑杆:压力感/战斗比重/管理深度/叙事比重/节奏,各一档。
-- 调性锚 T 原则:3~7 条逐条具名(如"T2 不劝退——凡惩罚类问题默认取最轻档")。
- 检验:每条 T 都能当一句 IF-THEN 用——"凡__类问题默认__";写不出口径的 T 是空话。
- → 下游每个开放问题先来这里级联批量起草,级联不了的才升级提问。
-**设计锚点(六项,争议时的仲裁原则,全部具名)**
-- 核心幻想:一句描述 + 一句玩家念头(引号写出玩家脑中的自言自语)。
- 检验:念头句写不出来 = 幻想没立住,回去重想,不要用描述糊弄。
-- 目标体验:何时感到什么。
-- 玩家动机:短期 __;长期 __。
-- 核心循环:__ → __ → __ → __ → 回到 __(箭头式)。
-- 跑偏风险:本项目可能的真实偏航,不放万金油。
-- 非目标:一行带过,详表见第 6 节。
-
-### 3. 玩家身份与基调
-- 玩家身份:玩家在虚构里是谁 + 本项目的核心节奏,一口气说清。
-- 情绪基调:正面定调 + 边界句——"可以 __,不可以 __"。
-
-### 4. 风格与世界观
-世界观为 __(玩法)服务;叙事通过 __(载体)展开。禁编年史、种族志。
-
-### 5. 目标玩家与情境(受众映射三件套)
-- 与谁的受众重合;吸收了谁的什么需求;**为什么不会变成它**(防串味声明,
- 参照越多越必须有这句)。
-- 情境与门槛:单人/多人;一局多久;需要理解 __,不应要求 __。
-
-### 6. 不是什么(负面定位表)
-| 不是 | 因为 |
-→ 每行原因要封死一条具体误会方向(例:不是武器店经营|武器主要拿去
- 战斗,不是卖给顾客)。从锚点的非目标与跑偏风险长出来,通常 4~6 行。
-
-### 7. 核心张力
-- __ 有限,但 __。
-- __ vs __(两端的代价各是什么)。
-→ 每条两端都必须有代价,只有一端的"假张力"删掉。这些是顶层取舍表的
- 种子,后面要逐条对应。
-
-### 8. 边界与约束
-- 概念边界放首位:本层只定幻想、用户、基调与排除方向;具体数值、
- 系统清单、MVP 内容留给顶层及以后。
-- 规模与回流:单人可维护;所有系统回流核心循环。
-- 参照声明:学组织方式,不复制角色/文本/美术/数值。
-
-### 9. 概念定稿(收口重锤)
-这个游戏的核心不是 __,而是:
-> (一句话重述核心承诺)
-交给下一层的约束:__ 必须 __(2~4 条,顶层必须围绕它们展开)。
-
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
-
-## 五、分析文档(全局一份,按层分节)
-
-**全局唯一一份《分析.md》**(项目根),本层不另设分析文件(2026-09-06 收敛:
-原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
-只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`、
-`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账,
-不放分析文档——本文件只放已决论证与登记。
-
-- 条目格式:`## 问题:<一句话>` + 状态(agent_proposal / user_confirmed /
- superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
- (逐候选利弊依据,必须引 T 原则/锚点/张力编号,写不出依据的偏好不进分析)
- + 综合判断(建议取 __ 因为 __;推翻条件:__)。
-- 分诊三条件全满足才进:① 影响项目方向或边界;② ≥2 合理候选;③ 一时定不了。
- 不满足的:就地小权衡直接进登记表一行,不写条目。
-- 本层标准两问:① 什么是本项目不可替代的核心承诺;② 什么内容扩张会稀释它。
-- 数量纪律:概念期问题通常 ≤3;开始堆第 4 问时先怀疑概念层没想清楚,重读定调记录而不是继续开新争议。
-- user_confirmed 后三件事:结论一句话迁入 design.md 对应节(留修订痕迹);
- 登记表加行(编号全项目连续,跨层引用写 D-__);本条目改状态记 D 号保留不删。
- 推翻时新增行挂旧行编号,旧行不删。
-
-
-## 六、写完自查(参考,不是闸门)
-- 卖点唯一吗?念头句立得住吗?
-- 随便挑一个后续设计问题,锚点六项之一能当裁判吗?
-- "不是什么"表封死了最可能的误会方向吗?
-- 张力每条都两端有代价吗?
-
-## 七、红线(只有三条)
-1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
-2. 不越层:出现具体数值、按键、界面即删。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
-
-
-
-
-## A2 顶层设计分册(game-gdd-top-design)
-
----
-name: game-gdd-top-design
-description: 写游戏策划案(GDD)顶层设计时使用。在概念层定稿之后,
- 回答"玩家为什么一直玩"——把概念变成可玩的时间结构(循环/资源/取舍/节奏),
- 并向架构层交付系统范围。配套:templates/top-design.md、templates/analysis.md(全局一份)、
- exemplars/stardew-top-design.md、exemplars/stardew-analysis.md(全局一份)。
----
-
-# 顶层设计写法(策划 agent · 顶层设计分册)
-
-> 本文件是顶层设计唯一承载写作流程的教学件。
-> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
-
-## 一、这一层的判断立场
-你是资深游戏策划,正在写全 GDD 最重要的一份文档——概念说"凭什么成立",
-顶层说"好玩在哪"。核心循环无趣,后面写再多系统也救不回来。在这个层里你相信:
-- 循环优先:先把大循环、小循环、最小体验单位三层跑通,再谈其他一切。
-- 用玩家的手写,不用系统的嘴写:写"玩家在做什么、在想什么",
- 不写"系统提供了什么功能"。
-- 每个时间段的痛苦和甜都要有来处:取舍表接概念层的张力,节奏接情绪摆动。
-- 资源守恒直觉:每种资源必问来源、储存、消耗——无来源是白给,
- 无消耗是废物,环环相扣成套利。
-- 你不替概念层翻案(张力与定稿已定),也不替架构层拆系统(只划边界)。
-
-## 二、动笔前
-1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。
-2. 读 exemplars/stardew-top-design.md 做质量锚(模仿密度,不抄内容),
- 往 templates/top-design.md 里填。
-3. 把概念层的核心张力清单摊开放在手边——取舍表必须逐条挂上编号。
-
-## 三、十六节总览:写什么、为什么、怎么咬合
-
-顶层文档回答四个问题:
-**玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→
-交给架构什么(12~14)→ 没想清什么、定了什么(15~16)。**
-
-第 1 节承概念定稿开篇,第 16 节给架构硬约束收口,首尾呼应;
-中段三层循环互检,资源流从底下供血。
-
-| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
-|---|---|---|---|---|
-| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
-| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
-| 3 | 核心推动力 | 动机主次 + 即时/日程/季节/长期四层推动 | 玩家"什么时候被什么推着走"的完整图谱 | 时间四层对应 10 节奏结构的四层 |
-| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
-| 5 | 小循环 | 几十秒到几分钟的具名动词链 ×3+ | 真正被玩到的那层;动词链可直接复制进实现 | 检验:删掉某条,游戏是否少了一块可命名的乐趣 |
-| 6 | 资源流与输入输出 | 资源流图(来源→储存→消耗)+ 输入输出清单 + 反馈四层 | 资源是循环的血液;防白给、防废物、防套利 | 供血给 4~5 的每个循环环节 |
-| 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 |
-| 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 |
-| 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) |
-| 10 | 节奏结构 | 日内/周内/季节/长期四层 + 情绪摆动 | 防止"一直紧张"或"一直平";摆动才有呼吸 | 四层对应 3 的推动力四层 |
-| 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 |
-| 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 |
-| 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 |
-| 14 | 验证标准 | 验证点/成功标准(行为判据) | "好玩"不可测,"玩家能复述循环"可测 | 判据对象=7 的最小体验单位 |
-| 15 | 开放问题 | 留给架构前必须想清的 | 显式债务清单 | 进分析文档或架构层开题 |
-| 16 | 顶层定稿 | 收口重锤 + 给架构的硬约束(必须__/不得__) | 检验全文档没写散;架构的紧箍咒 | 回环呼应 1;承概念层定稿的接力棒 |
-
-咬合一图:
-
-```
-概念层定稿(硬约束 + 张力)
- ↓ 承接
-1 定位与规模锚点 ───张力落位───► 9 取舍表(逐条对应)
- ↓ 展开
-2 设计目标 → 3 核心推动力 → 4 大循环 ⇄ 5 小循环 ⇄ 7 最小体验单位
- ↓ 供血
-6 资源流与输入输出(防无来源/无消耗/套利)
- ↓ 后果侧
-8 活动流程(段落表)→ 10 节奏结构 → 11 失败与回收
- ↓ 交付
-12 系统范围(→架构系统地图的种子)+ 13 范围 + 14 验证标准
- ↓ 收口
-15 开放问题 → 16 顶层定稿(给架构的硬约束)
-```
-
-三个接口:**对上**承概念定稿、张力逐条变取舍表;**对内**三层循环互检
-(大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、
-顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。
-
-## 四、怎么写(模板即流程,十六节按序)
-(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md)
-
-### 1. 顶层定位与规模锚点
-顶层不是做 __,也不是做 __,而是让玩家每天都在想:
-> "__(玩家每天惦记的那件事)"
-规模锚点表:循环单位 / 段落构成 / 操作复杂度 / 经营复杂度 / 长期主轴排序。
-→ 循环单位先行,定错全盘错。复杂度行可内联参照与"不做"。
-
-### 2. 设计目标
-玩家在 __ 循环中同时获得 __、__、__——三者不是并列小游戏,而是互相供给:__。
-→ 检验:砍掉任何一种回报,另外两种是否受伤。
-
-### 3. 核心推动力
-- 动机主次:__。
-- 即时推动 __;日程推动 __;季节推动 __;长期推动 __。
-→ 四层都要有实指;空着的那层就是将来留存崩塌的地方。
-
-### 4. 大循环
-**__ → __ → __ → __ → 回到 __。**(附核心循环图)
-→ 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。
-
-### 5. 小循环(具名动词链 ×3+)
-**__循环**:__ → __ → __ → __ → __。
-→ 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。
-
-### 6. 资源流与输入输出
-(资源流图:每种核心资源 来源 → 储存 → 消耗 三段全)
-主要输入 __;主要输出 __;反馈四层:立即 __ / 短期 __ / 中期 __ / 长期 __。
-→ 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。
-
-### 7. 最小体验单位
-__(多短一段玩法体现独有乐趣——原型只做这一个单位)。
-单个行动必须至少提供一种清晰反馈:资源/进度/能力/关系/信息/视觉状态之一。
-
-### 8. 核心活动流程(段落表)
-| 阶段 | 玩家行为 | 设计目的 |
-→ 设计目的列必填;写不出目的的段落删掉。这份表要能让陌生人复述
-"玩这个游戏的一天"。
-
-### 9. 取舍表
-| 决策 | 立即收益 | 延迟收益 | 主要代价 |
-→ 每行挂概念层张力编号;避免唯一最优解;不同选择应产生不同但都合理的玩法方式。
-
-### 10. 节奏结构
-日内 __ → 周内 __ → 季节/章节 __ → 长期 __。
-整体情绪在"__"与"__"之间摆动(恢复来源 __;变化来源 __)。
-
-### 11. 失败与回收
-先定性:失败主要表现为 __(少拿收益 / 延迟成长 / 毁掉积累——三选一档位),
-再列表:
-| 情况 | 结果 |
-→ 亏损档位必须与概念层情绪基调一致;治愈基调配"少拿"档。
-
-### 12. 系统范围(架构层接口)
-| 系统 | 顶层目的 | 边界(本层不做什么) |
-→ 只写目的与边界,不写系统内部规则;每行将来对应架构层一个 Sxx。
-
-### 13. 范围与非目标
-最小完整版本包含:__。不做清单:__。
-
-### 14. 验证标准
-| 验证点 | 成功标准 |
-→ 成功标准必须是行为判据("玩家能复述__""玩家出现__行为"),
- "感觉好玩"不算。
-
-### 15. 开放问题
-→ 逐条列出;值得跨轮保留的进分析文档,其余留待架构层开题。
-
-### 16. 顶层定稿(收口重锤)
-顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。
-后续架构必须围绕 __ 拆系统;不得 __。
-
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
-
-## 五、分析文档(全局一份,按层分节)
-
-**全局唯一一份《分析.md》**(项目根),本层不另设分析文件(2026-09-06 收敛:
-原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
-只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`、
-`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账,
-不放分析文档——本文件只放已决论证与登记。
-
-- 条目格式:`## 问题:<一句话>` + 状态(agent_proposal / user_confirmed /
- superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
- (逐候选利弊依据,必须引 T 原则/锚点/张力编号,写不出依据的偏好不进分析)
- + 综合判断(建议取 __ 因为 __;推翻条件:__)。
-- 分诊三条件全满足才进:① 影响项目方向或边界;② ≥2 合理候选;③ 一时定不了。
- 不满足的:就地小权衡直接进登记表一行,不写条目。
-- 本层标准两问:① 一天/一局怎样形成清楚但不拖沓的循环;② 风险、收益与长期成长怎样互相支撑。
-- 数量纪律:顶层期问题通常 ≤5(结构性争议天然更多);堆问题时先回读第 1 节定位句。
-- user_confirmed 后三件事:结论一句话迁入 design.md 对应节(留修订痕迹);
- 登记表加行(编号全项目连续,跨层引用写 D-__);本条目改状态记 D 号保留不删。
- 推翻时新增行挂旧行编号,旧行不删。
-
-
-## 六、写完自查(参考,不是闸门)
-- 三层循环互检了吗:大循环每环有小循环供血?最小单位切得出来?
-- 概念层张力每条都在取舍表有对应行吗?
-- 每种资源三段全吗(来源/储存/消耗)?
-- 验证标准是行为判据吗,还是写了"好玩"?
-- 架构层拿到系统范围表能直接开工吗——有没有该划没划的系统?
-- 失败档位和概念层基调一致吗?
-
-## 七、红线(只有三条)
-1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
-2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
-
-
-
-
-## A3 系统架构分册(game-gdd-architecture)
-
----
-name: game-gdd-architecture
-description: 写游戏策划案(GDD)系统架构时使用。在顶层设计定稿之后,
- 把顶层的系统范围表正式切成 Sxx 系统:编号、职责、依赖、数据流、优先级,
- 并向系统文档站交付目录映射与 MVP 闭环。配套:templates/architecture.md、
- templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、exemplars/stardew-analysis.md(全局一份)。
----
-
-# 系统架构写法(策划 agent · 系统架构分册)
-
-> 本文件是系统架构层唯一承载写作流程的教学件。
-> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
-
-## 一、这一层的判断立场
-你是架构师,切系统的刀在你手里。在这个层里你相信:
-- 切分是为了**职责清晰、可独立讨论**,不是为了凑数量——每个系统必须能
- 一句话答出"删了它,什么塌"(P0 原因)。
-- **数据所有权唯一**:同一事实只由一个系统维护,其他系统只引用稳定 ID,
- 不复制主数据。两个系统管同一件事 = 架构事故。
-- **依赖无环**是硬要求;信息呈现层只读状态、只经行动入口写入。
-- 架构是全项目返工最多的一份(实测 11 版 vs 概念层 2 版)——所以每次改刀
- 都要写变更记录,让"为什么这么切"可追溯。
-- 你不越层:上不重定义玩法循环(那是顶层的),下不写单系统内部规则
- (那是系统文档的),字段定义与数值配置归技术文档层(数值策划)。
-
-## 二、动笔前
-1. 顶层设计已定稿可用——把它的**系统范围表**(粗清单)和**顶层定稿约束**
- 摊开当输入;切分是对粗清单的正式化(拆、并、裁都在这层做)。
-2. 读 exemplars/stardew-architecture.md 做质量锚(模仿密度,不抄内容),
- 往 templates/architecture.md 里填。
-3. 记住顶层的核心循环图——切完必须跑覆盖检查。
-
-## 三、十二节总览:写什么、为什么、怎么咬合
-
-架构文档回答四个问题:
-**这个架构为什么这样切(1~3)→ 系统是什么、怎么连接(4~6)→
-怎么落地、怎么验证(7~11)→ 还有什么没想清(12)。**
-
-第 1 节承顶层的定稿约束开篇,MVP 闭环在中间当守门员,开放问题收尾。
-
-| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
-|---|---|---|---|---|
-| 1 | 架构定位与目标 | 阶段边界(定哪些系统、不展开内部)+ 划分原则 + 一句话架构 + **变更记录** | 防止架构漂离顶层;改刀可追溯 | 承顶层定稿;变更记录引登记编号 |
-| 2 | 系统地图 | Sxx 编号清单(=系统文档目录真源)+ 支撑层 + P0 段五列表(目的/输入/输出/P0原因) | 编号让系统可引用;P0 原因逼答"删了塌什么" | **对下真源**:Sxx ↔ 04 系统文档一一对应 |
-| 3 | 系统职责 | 职责表(负责/不负责→移交谁)+ 逐系统说明段 | 边界写死,防两个系统管同一件事 | 系统文档的"边界与非目标"必须与此对齐 |
-| 4 | 依赖与数据流 | 依赖图(无环)+ 数据流图 + 主要状态 + 主数据归属规则 | 谁读谁、数据从哪到哪——接口的真源 | 顶层的资源流图在此展开成系统级 |
-| 5 | 核心循环覆盖检查 | 顶层每个循环环节 → 认领系统 | 顶层→架构的验收线,防切系统切碎循环 | 对上接口:逐环节对照顶层循环图 |
-| 6 | 目录映射 | 职责 → 物理文档目录的归并表 | 职责数≠文档数;归并规则显式化 | **对下接口**:系统文档站照此开工 |
-| 7 | MVP 最小闭环 | 编号验证链 + 守门句("闭环不成立不许加东西") | 立项后第一条要跑通的链 | 对应顶层验证标准;失败回顶层而非加系统 |
-| 8 | 统一数值基准 | 单位清单 + 四类定性基准(时间/货币/成长/体力风险的风格约束) | 各系统单独配数值会互相失衡;先定全局尺度 | **数值换算与验算归技术文档层**,此处只到定性 |
-| 9 | 系统边界 | 哪些功能明确不属于任何系统/归引擎层/归呈现层 | 显式排除,防范围蔓延 | 承概念层"不是什么" |
-| 10 | 优先级与范围 | P0/P1/P2 三档(P1/P2 可用能力表) | 拆分≠全做;裁剪顺序显式化 | P0 = MVP 闭环的系统集 |
-| 11 | 风险与校验 | 风险/校验方式表 | 架构级风险提前挂出,每条带检验法 | 对应顶层验证标准与概念层跑偏风险 |
-| 12 | 开放的结构问题 | 结构级未定案 | 显式债务 | 进分析文档或系统文档开题 |
-
-咬合一图:
-
-```
-顶层定稿 + 系统范围表(粗清单)
- ↓ 正式切分(拆/并/裁)
-1 定位与目标 ──► 2 系统地图(Sxx 真源)──► 3 职责表
- ↓ ↓ ↓
-5 循环覆盖检查 ◄── 4 依赖与数据流(接口真源)
- ↓
-6 目录映射 ──► 7 MVP 最小闭环(守门员)
- ↓
-8 数值基准(定性)· 9 边界 · 10 优先级 · 11 风险校验
- ↓
-12 开放问题 →(进分析文档 / 系统文档站开题)
-```
-
-三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖
-三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。
-
-## 四、怎么写(模板即流程,十二节按序)
-(本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md)
-
-### 1. 架构定位与目标
-本阶段确定"哪些系统支撑一轮玩法",不展开单系统内部规则。
-划分原则:__。一句话架构:
-> (玩家通过哪些系统、以什么因果,把一轮玩法的输入变成下一轮的选择)
-变更记录:日期 + 改了什么 + 为什么(引登记编号)。
-→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
-
-### 2. 系统地图
-Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
-P0 段五列表:
-| 系统 | 目的 | 输入 | 输出 | P0 原因 |
-→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
-
-### 3. 系统职责
-| 系统 | 主要职责 | 不负责 → 移交谁 |
-→ "不负责"列必填且指向具名系统;再为争议最大的 2~3 个系统各写一段
-说明(负责什么 / 不负责什么 / 只负责什么)。
-
-### 4. 依赖与数据流
-依赖图(mermaid,呈现层用虚线"读取状态")+ 数据流图(资源从产到耗)。
-主要状态:全局/玩家/场景/社会 四类。
-主数据归属规则:规则与数据表分工 / 稳定 ID 关联 / 任何系统不复制他系统主数据。
-→ 依赖图出现环 = 回去重切。
-
-### 5. 核心循环覆盖检查
-| 顶层循环环节 | 认领系统 |
-→ 逐环节对照顶层循环图;有环节无人认领或多人认领都是切分错误。
-
-### 6. 目录映射
-| 目录 | 本阶段定位 |
-→ 职责可以归并进同一文档目录(官方版 8 职责→3 文档);归并规则写明。
-系统文档站以此开工:地图上没有的系统不许有文档。
-
-### 7. MVP 最小闭环
-1. __ 2. __ …(编号验证链,一条玩家可走的完整因果)
-守门句:如果这条闭环不成立,不应继续增加 __。
-→ 闭环失败回顶层改设计,不是加系统打补丁。
-
-### 8. 统一数值基准(定性)
-全局单位清单(如时间片/游戏日/货币/体力/经验)+ 四类风格约束
-(时间节奏/货币量级感/成长回报取向/体力风险档位)。
-→ 只写到定性;具体换算、验算数值由技术文档层(数值策划)承接。
-
-### 9. 系统边界
-明确排除项(不拆出独立 __ 系统 / __ 归引擎层 / __ 归呈现层)。
-
-### 10. 优先级与范围
-P0(最小闭环必需):__;P1(完整体验):__;P2(扩展内容):__。
-P1/P2 可用能力表(能力/说明)控制颗粒度。
-
-### 11. 风险与校验
-| 风险 | 校验方式 |
-→ 从概念层跑偏风险和顶层失败档位反推;校验方式要可观察。
-
-### 12. 开放的结构问题
-→ 结构级(接口归属/统一格式/合并拆分)才留这里;数值细节不留。
-
-## 五、分析文档(全局一份,按层分节)
-
-**全局唯一一份《分析.md》**(项目根),本层不另设分析文件(2026-09-06 收敛:
-原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
-只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`、
-`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账,
-不放分析文档——本文件只放已决论证与登记。
-
-- 条目格式:`## 问题:<一句话>` + 状态(agent_proposal / user_confirmed /
- superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
- (逐候选利弊依据,必须引 T 原则/锚点/张力编号,写不出依据的偏好不进分析)
- + 综合判断(建议取 __ 因为 __;推翻条件:__)。
-- 分诊三条件全满足才进:① 影响项目方向或边界;② ≥2 合理候选;③ 一时定不了。
- 不满足的:就地小权衡直接进登记表一行,不写条目。
-- 本层标准问题:结构级争议——接口统一、系统归并、主数据归属划分。(本层原本不配独立分析文件,结构争议全归全局文件本节。)
-- 数量纪律:按需;架构期问题多为接口与归属二义。
-- user_confirmed 后三件事:结论一句话迁入 design.md 对应节(留修订痕迹);
- 登记表加行(编号全项目连续,跨层引用写 D-__);本条目改状态记 D 号保留不删。
- 推翻时新增行挂旧行编号,旧行不删。
-
-
-## 六、写完自查(参考,不是闸门)
-- 每个 Sxx 都能一句话答"删了它什么塌"吗?
-- 顶层的循环环节全覆盖、无重复认领吗?
-- 依赖图无环?主数据无一物两管?
-- 系统文档站拿到目录映射能直接开工吗?
-- 有没有字段定义或数值配置偷偷写进来?(该在技术文档层)
-- 变更记录补了吗——这次切分和上次的差异说得清吗?
-
-## 七、红线(只有三条)
-1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
-2. 不越层:向上不翻顶层的案,向下不写系统内部规则,数值字段归技术文档层。
-3. 不凑数:系统数量不是成绩,写不出 P0 原因的系统就是该删的系统。
-
-
-
-
-## A4 系统文档分册(game-gdd-system-doc)
-
----
-name: game-gdd-system-doc
-description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架、
- 红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md
- 与对应模块的模板.md 里,按需取用。
----
-
-# 系统文档写法(策划 agent · 系统文档分册 · 总纲)
-
-> 本文件是系统文档层的总纲;各系统的专属写法在 `modules/system-types/` 下对应目录的 `SKILL.md`,
- 专属模板在 `modules/system-types/` 对应目录的 `模板.md`。通用纪律不在各系统 skill 里重复。
-
-## 一、这一层的判断立场
-你是写单个系统的策划。在这个层里你相信:
-- 系统文档是**执行层**:刀已经在架构层切好——服从系统地图编号、职责表
- 边界、依赖图方向,无权改刀;发现切错了,提分析、记登记,不私自扩边界。
-- 一个系统文档的成败在**边界节**:"不负责什么、移交给谁"那几行是
- 防返工价值最高的几行。
-- 接口纪律:引用具名系统与具名数据,禁泛称;别家主数据只引 ID 不复制。
-- 字段定义、数值配置、表结构不归你——写交接声明,交技术文档层(数值策划)。
-- 所有系统同构:读者读熟一份就能读所有份。
-
-## 二、动笔前
-1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
-2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
- 的判定部分),读取对应的 `SKILL.md` 与 `模板.md`。
-3. 该文件夹标注"必读例子"的,先读例子全文做密度锚。
-
-## 三、十二节总览:写什么、为什么、怎么咬合
-
-系统文档回答四个问题:
-**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
-它怎么和别人连接、不碰什么(9~12)。**
-
-| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
-|---|---|---|---|---|
-| 1 | 系统目的 | 一句话:删了它什么塌 | 存在性检验 | 架构 P0 原因的展开 |
-| 2 | 支撑的玩家体验 | 对应顶层目标第几条 | 防系统自嗨 | 顶层设计目标 ↔ 本系统 |
-| 3 | 进入与退出 | 何时进入、何时/如何退出 | 循环的接口时刻 | 顶层的循环环节 |
-| 4 | 玩家行动 | 具名动词组 | 玩家用手玩 | 系统类型卡给动词组 |
-| 5 | 取舍表 | 玩家在本系统内的决策 | 张力在系统内的落地 | 概念张力→顶层取舍表→本表 |
-| 6 | 状态与规则 | 对象/状态/转换/异常,枚举表达 | 定性规则真源 | 架构职责表对齐 |
-| 7 | 数值与数据交接 | 本系统交 TDD 的数据类别+定性约束 | 分层边界 | 技术文档层承接 |
-| 8 | 反馈 | 何时/何强度/何通道 | 无反馈=没发生 | 顶层反馈四层 |
-| 9 | 内部循环 | 本系统内的小循环 | 系统自己的心跳 | 顶层小循环的组成 |
-| 10 | 输入、输出与依赖 | 消费/交付/依赖谁 | 接口真源 | 架构依赖图逐边对齐 |
-| 11 | 边界与非目标 | 不负责什么→移交谁 | **防返工价值最高** | 架构职责表"不负责"列 |
-| 12 | 开放问题 | 本系统未定案 | 显式债务 | 进分析文档 |
-
-咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
-职责边界;**对下**第 7 节交接喂 TDD。
-
-## 四、十二节通用写法
-(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md)
-
-1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
-2 支撑体验:对应顶层目标第__条、调性原则第__条。
-3 进入与退出:常规进入/读档恢复/特殊事件后返回,三入口必写。
-4 玩家行动:≥4 个具名动词组;编排类写"安排"动词,活动类写"操作"动词。
-5 取舍表:决策/立即收益/延迟收益/主要代价;挂顶层张力编号。
-6 状态与规则:对象-状态-转换-异常,全部枚举表达,不许整段散文。
-7 数值与数据交接:列数据类别名 + 设计侧定性约束;字段定义归 TDD。
-8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
-9 内部循环:动词链;可拆单次/区域/长期三层。
-10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
-11 边界与非目标:照该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条。
-12 开放问题:结构级才留;手感数值类标"待原型验证"。
-
-## 五、分析文档(全局一份,按层分节)
-
-**全局唯一一份《分析.md》**(项目根),本层不另设分析文件(2026-09-06 收敛:
-原每层一份 analysis 合并为全局一份——论证按发生层归节,决定登记表全项目
-只此一张,跨层引用只查这里)。模板与例子:资源 `templates/analysis.md`、
-`exemplars/stardew-analysis.md`。状态池(灵感池/代决/待原型等活队列)在决策台账,
-不放分析文档——本文件只放已决论证与登记。
-
-- 条目格式:`## 问题:<一句话>` + 状态(agent_proposal / user_confirmed /
- superseded,登记 D-__)+ 广度分析(牵动面+候选 ≥2)+ 深度分析
- (逐候选利弊依据,必须引 T 原则/锚点/张力编号,写不出依据的偏好不进分析)
- + 综合判断(建议取 __ 因为 __;推翻条件:__)。
-- 分诊三条件全满足才进:① 影响项目方向或边界;② ≥2 合理候选;③ 一时定不了。
- 不满足的:就地小权衡直接进登记表一行,不写条目。
-- 本层标准问题:① 本系统与相邻系统的边界在哪;② 本系统内部哪个规则影响顶层取舍。条目标系统号(如 S06)。
-- 数量纪律:按需;每系统通常 0~1 条,超了先回读架构职责表。
-- user_confirmed 后三件事:结论一句话迁入 design.md 对应节(留修订痕迹);
- 登记表加行(编号全项目连续,跨层引用写 D-__);本条目改状态记 D 号保留不删。
- 推翻时新增行挂旧行编号,旧行不删。
-
-
-## 六、写完自查(参考,不是闸门)
-- 目的一句话成立吗?边界节和架构职责表逐行对齐吗?
-- 输入输出和依赖图逐边对上吗?有没有泛称漏网?
-- 状态是枚举还是散文?失败路径给了原因和恢复吗?
-- 有没有字段或数值偷偷写进来?(该在 TDD)
-- 同构检查:另一份系统文档的读者能按同样方式读这份吗?
-
-## 七、红线(只有三条)
-1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
-2. 不越层:不翻架构的案(要改走分析文档+登记表),不写字段数值(归 TDD),
- 不替别的系统定规则。
-3. 不凑数:写不出"删了塌什么"、填不满的节,说明缺料——停笔说明,不硬凑。
-
-
-
-
-## A5 技术文档分册(game-tdd)
-
----
-name: game-tdd
-description: 写游戏技术文档(TDD)时使用的总纲。GDD 四层定稿后的第五步:把
- "怎么做"写实——程序怎么写、美术怎么做、字段怎么定义、怎么配表。
- 三大件各有专属分册:技术实现(程序侧)/ 美术圣经(美术侧)/ 数据与配表(数据侧)。
----
-
-# 技术文档写法(策划 agent · TDD 分册 · 总纲)
-
-> 本文件是 TDD 层唯一承载写作流程的教学件;各分册 SKILL 与模板配套使用。
-> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
-
-## 〇、TDD 的完成判据(总纲)
-
-**TDD 是自足构建包:一个施工 agent 只看 TDD,就能做完完整游戏。**
-GDD 是设计真源(给人看、给迭代看);TDD 是构建真源(给施工看)。
-检验方式=自足性检查(见总册):不看 GDD 能否回答——每个系统怎么行为、
-每张表多少行内容、每个界面怎么走、每份素材什么规格。答不出的项就是缺口,
-缺口回 GDD 同步后**收编**进 TDD(带版本锁)。收编是构建期快照:GDD 定稿
-变更 → 触发对应收编节重同步(与 fast_gdd 投影同一机制,方向相反)。
-
-## 一、这一层的判断立场
-
-你是工程师思维的策划。GDD 是"用户视角的功能描述",TDD 是"实现者视角的
-架构性描述"——你不重复设计的论证(为什么这样设计,去 GDD 和 analysis 查),
-只写怎么落地。你相信:
-
-- **交接契约是 TDD 最大的价值**:美术交给程序的素材、程序读的表、加载的
- 顺序——每一条缝都写死。缝上不写死,返工就在缝里发生。
-- **平台事实优先**:目标运行时由 GDD 平台事实锁定——**HTML / Unity / Godot /
- Cocos 四选一**。HTML 项纯 HTML/CSS/JS 交付;引擎项支持打开引擎工程、自然
- 语言协作改素材与代码,由陶泥儿驱动引擎**弹窗预览**、驱动引擎 **CLI 导出**。
- 一切技术选择先过所选运行时这道闸,不推荐该运行时做不出来的东西;
- TDD 不擅自换运行时。
-- **一个事实只有一个写权**:每张表、每条主数据都有唯一拥有者系统,
- 其他系统只引用不复制(GDD 架构层主数据归属规则在 TDD 落成表结构)。
-- **验收是硬闸不是仪式**:有 blocker 禁止扩充内容——这条竞品四十轮实测
- 验证过,照抄。
-- **先少量验证再量产**(美术)/ **先建索引再转表**(数据)——任何方向都
- 不做"做完一大批才发现不对"的事。
-
-## 二、TDD 与 GDD 的接口(输入从哪来)
-
-| 输入 | 来自 | 喂给哪件 |
-|---|---|---|
-| 系统范围表 + P0 清单 + 主数据归属规则 | 架构层 | 三件共用(拆表与拆模块依据) |
-| 各系统「数值与数据交接」节 + 定性约束 | 系统文档 | 数据侧(直接订单) |
-| 定调记录(参照/滑杆/T 原则)+ 身份基调 | 概念层 | 美术圣经(视觉翻译源头) |
-| 技能选型卡 | skill 库 | 程序侧+美术圣经(@版本+参数实例化) |
-
-TDD 不回头改 GDD:发现 GDD 没写清楚的点,走「开放问题回执」——该问用户
-的升级决策卡,该代决的记台账(带理由和推翻条件),结论回写对应层,TDD 只
-登记去向。顾问期(开发阶段)同一出口:程序美术卡点、成品与文档偏差,都从
-回执进、修订出(v{N+1})。
-
-## 三、三大件与开工顺序
-
-| 件 | 管什么 | 读者 | 分册 |
-|---|---|---|---|
-| 数据与配表 | 字段定义、表结构、数值、验收 | 数值策划 + 程序 | 03 |
-| 技术实现 | 代码组织、场景镜头、输入、音频、性能预算、验证 | 程序 | 01 |
-| 美术圣经 | 视觉锚、素材规格契约、量产流程 | 美术 | 02 |
-
-**顺序:数据侧 → 程序侧 → 美术圣经**。数据侧先开的理由:它是唯一直接被
-GDD 喂的(系统文档交接节就是订单);程序侧的加载与验证要引用表结构;美术
-圣经的素材总清单要引用物品表(每个可见对象绑定 item_id 或显式豁免)。小型
-项目三件可交叉,但**表结构永远先于数值填充**。
-
-## 四、怎么写(总纲级;细节在各分册)
-
-1. 数据侧:总清单拆表 → ID 与字段字典 → 公共条件表 → 建表顺序(物品表
- 起步)→ 表结构契约(程序签名)→ 数值填充(代决+台账)→ 验收七查。
-2. 程序侧:系统实现总览(每系统一段话写死怎么做)→ 技术选型与 skill 引用
- → 场景与镜头 → 输入与操作 → 音频 → 验证方式与性能预算。
-3. 美术圣经:视觉锚(从概念层定调翻译)→ 素材规格契约逐素材一行 →
- 量产流程(概念候选→锚点确认→小批→验收→扩产)→ 资产总清单。
-
-## 五、写完自查(参考,不是闸门)
-
-- 任意一条缝(美术→程序、表→代码、表→表引用)是否都写死了规格?
-- 每张表是否答得出"谁是拥有者系统"?每个 ID 是否全局唯一?
-- 程序侧验证方式是否可执行(跑什么命令、看什么输出)?
-- 素材契约是否覆盖了 GDD 里全部可见对象(或显式豁免)?
-- 验收是否跑过且无 blocker?
-
-## 六、红线(只有四条)
-
-1. **收编必带版本锁**:从 GDD 收编的任何内容标注"基于系统文档@v{N}";
- 无锁收编=违规(双源漂移之源)。TDD 不产生设计观点,只汇集与落实施工。
-2. 引用必带版本:skill 引用必须 `名字@版本 + 实例化参数`,选型时与执行时
- 用的一致性靠此保证。
-3. 不越权拍板:产品级取舍回 GDD 层走决策流程;TDD 只做技术代决且记台账。
-4. 表里不写散文:单元格只有数据和枚举;规则写在契约文档,不写在表里。
+本分册说明单个系统的职责、规则、输入输出、反馈、边界、验证和分析记录。完整内容请阅读 `resources/skills/systems.md`;系统类型的专属写法和模板请按需阅读 `modules/system-types/` 下对应分册。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md
index 0f720e181..9fec11e4a 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md
@@ -13,6 +13,10 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
> 本文件是顶层设计唯一承载写作流程的教学件。
> 模板与例子文件保持纯净:不含任何步骤、检验提示与标记。
+## 〇、结构适配原则
+
+本分册的章节、字段和数量是参考结构,不是固定清单。先根据游戏类型、项目规模、用户要求和概念层定稿判断适用项:适用项写入,同类项可合并,若某项对本项目没意义则省略;复杂项目可以拆分补充,简单项目可以压缩为最小可用规格。
+
## 一、这一层的判断立场
你是资深游戏策划,正在写全 GDD 最重要的一份文档——概念说"凭什么成立",
顶层说"好玩在哪"。核心循环无趣,后面写再多系统也救不回来。在这个层里你相信:
@@ -26,11 +30,11 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
## 二、动笔前
1. 概念层 design.md 已定稿可用——顶层定位与取舍表直接从它长出来。
-2. 读 exemplars/stardew-top-design.md 做质量锚(模仿密度,不抄内容),
+2. 读取 exemplars/stardew-top-design.md 了解内容组织方式,
然后往 templates/top-design.md 里填。
-3. 把概念层的核心张力清单摊开放在手边——取舍表必须逐条挂上编号。
+3. 把概念层已确认的核心张力作为输入;存在对应取舍时再挂上编号。
-## 三、十六节总览:写什么、为什么、怎么咬合
+## 三、顶层设计的组织维度:写什么、为什么、怎么咬合
顶层文档回答四个问题:
**玩家在玩什么(1~9)→ 玩家面对什么选择与后果(10~11)→
@@ -43,14 +47,14 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
|---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 不是X不是Y + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
-| 3 | 核心推动力 | 动机主次 + 即时/日程/季节/长期四层推动 | 玩家"什么时候被什么推着走"的完整图谱 | 时间四层对应 10 节奏结构的四层 |
+| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
-| 5 | 小循环 | 几十秒到几分钟的具名动词链 ×3+ | 真正被玩到的那层;动词链可直接复制进实现 | 检验:删掉某条,游戏是否少了一块可命名的乐趣 |
-| 6 | 资源流与输入输出 | 资源流图(来源→储存→消耗)+ 输入输出清单 + 反馈四层 | 资源是循环的血液;防白给、防废物、防套利 | 供血给 4~5 的每个循环环节 |
+| 5 | 小循环 | 按项目实际存在的局内或短周期动词链组织 | 记录真正被玩到的循环 | 按实际循环层级互检 |
+| 6 | 资源流与输入输出 | 按项目实际存在的资源流、输入输出和反馈组织 | 说明循环中的实际供给与结果 | 与实际循环环节对应 |
| 7 | 最小体验单位 | 多短一段玩法就能体现独有乐趣 + 反馈铁律 | 原型只做这一个单位——定原型规模 | 是 5 的最小切片;14 验证标准的试验对象 |
| 8 | 核心活动流程 | 段落表:阶段/玩家行为/**设计目的** | "玩这个游戏的一天"的可复述剧本 | 设计目的列写不出的段=该删的段 |
| 9 | 取舍表 | 决策/立即收益/延迟收益/主要代价 | 张力的具体化——玩家决策的路口 | **逐条对应概念层核心张力**(对上接口) |
-| 10 | 节奏结构 | 日内/周内/季节/长期四层 + 情绪摆动 | 防止"一直紧张"或"一直平";摆动才有呼吸 | 四层对应 3 的推动力四层 |
+| 10 | 节奏结构 | 按项目实际存在的时间层级和情绪变化组织 | 说明玩法节奏如何变化 | 与实际推动力层级对应 |
| 11 | 失败与回收 | 亏损定性 + 情况/结果表 | 失败的形态决定调性——"少拿"还是"毁掉" | 对齐概念层情绪基调的边界句 |
| 12 | 系统范围 | 系统/顶层目的/**边界** 表 | 架构层接口:系统地图的种子 | **对下接口**:架构照此拆系统 |
| 13 | 范围与非目标 | 最小完整版本清单 + 不做清单 | 立项交付物的边界 | 承概念层"不是什么";给 14 提供验证范围 |
@@ -80,7 +84,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
(大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、
顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。
-## 四、怎么写(模板即流程,十六节按序)
+## 四、怎么写(模板参考结构,建议按此组织)
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md)
### 1. 顶层定位与规模锚点
@@ -96,24 +100,24 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
### 3. 核心推动力
- 动机主次:__。
- 即时推动 __;日程推动 __;季节推动 __;长期推动 __。
-→ 四层都要有实指;空着的那层就是将来留存崩塌的地方。
+→ 只展开项目实际存在的时间层级;不存在的层级不设字段。
### 4. 大循环
**__ → __ → __ → __ → 回到 __。**(附核心循环图)
→ 检验:断掉任何一环,后面是否塌;每一环应有对应小循环供血。
-### 5. 小循环(具名动词链 ×3+)
+### 5. 小循环(按项目实际数量)
**__循环**:__ → __ → __ → __ → __。
→ 必须具名("农务循环"不是"资源循环");动词链完整到可以直接照做。
### 6. 资源流与输入输出
(资源流图:每种核心资源 来源 → 储存 → 消耗 三段全)
-主要输入 __;主要输出 __;反馈四层:立即 __ / 短期 __ / 中期 __ / 长期 __。
+主要输入 __;主要输出 __;按项目需要记录反馈层级。
→ 三问:这资源哪来的?存在哪?花在哪去?答不出=资源设计未完成。
### 7. 最小体验单位
__(多短一段玩法体现独有乐趣——原型只做这一个单位)。
-单个行动必须至少提供一种清晰反馈:资源/进度/能力/关系/信息/视觉状态之一。
+保留的玩家行动应有与玩法相称的可理解反馈;反馈形式和数量按项目决定。
### 8. 核心活动流程(段落表)
| 阶段 | 玩家行为 | 设计目的 |
@@ -153,7 +157,7 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
顶层当前定稿为:__(循环单位、核心结构、关键档位一句话说全)。
后续架构必须围绕 __ 拆系统;不得 __。
-某节对本项目没意义 → 写一行"略,因为 __",不硬凑。
+若某节对本项目没意义,直接省略。
## 五、分析文档(全局一份,按层分节)
@@ -187,4 +191,4 @@ __(多短一段玩法体现独有乐趣——原型只做这一个单位)。
## 七、红线(只有三条)
1. 不冒充用户决定:用户没说的方向标"待确认",正文不写死。
2. 不越层:向上不翻概念层的案,向下不写系统内部规则与具体数值。
-3. 不凑数:写不满就说明缺什么,禁止万金油句填充。
+3. 不凑数:章节对项目有意义但信息不足时,记录已确定内容与待补问题;章节对项目无意义时,直接省略。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/architecture.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/architecture.md
index 516a740bb..e6d8d5e85 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/architecture.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/architecture.md
@@ -1,5 +1,7 @@
### C1 模板_系统架构.md(→ templates/architecture.md)
+本模板是参考结构,不是固定清单。只有需要独立职责、状态或数据边界的部分才拆成系统;简单项目可以合并系统和章节,复杂项目可以增加必要的系统与校验。表格中的示例行可按实际系统、风险和问题扩展,不代表数量上限。
+
# 系统架构:《游戏名》
## 架构定位与目标
@@ -18,6 +20,7 @@
|---|---|---|---|
| S01 | __ | __ | P0 |
| S02 | __ | __ | |
+(以上为示例,可按实际系统删减或扩充。)
支撑层(不拥有核心规则):__。
@@ -105,6 +108,8 @@ flowchart LR
| 风险 | 校验方式 |
|---|---|
| __ | __ |
+(按实际风险逐行补充。)
## 开放的结构问题
- __
+(按实际问题逐条补充。)
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/concept-design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/concept-design.md
index 229eccd32..36fc4dc79 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/concept-design.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/concept-design.md
@@ -1,5 +1,7 @@
### C1 模板_概念设计.md(→ templates/concept-design.md)
+本模板是参考结构,不是固定清单。填写前按项目类型、规模和用户要求筛选章节与字段;同类内容可合并,若某节对项目没有实际意义则删除,复杂项目可增加必要内容。表格和列表中的示例项可按实际内容扩展,不代表数量上限。
+
# 概念设计:《游戏名》
## 一句话概念
@@ -10,14 +12,14 @@
### 定调记录(全项目调性真源,级联决策的依据库)
- 参照选择:以《__》为主(__, 学 __);不参考 __。
- 调性滑杆:压力感 __ / 战斗比重 __ / 管理深度 __ / 叙事比重 __ / 节奏 __。
-- 调性锚(T 原则,逐条具名,下游每个开放问题先来这里级联):
- T1 __;T2 __;T3 __;T4 __;T5 __。
+- 调性锚(按项目需要逐条具名,下游开放问题按需从这里级联):
+ T__ __。
### 设计锚点(六仲裁位)
- 核心幻想:__。
玩家念头:"__"
- 目标体验:__。
-- 玩家动机:短期 __;长期 __。
+- 玩家动机(按项目实际存在的时间尺度填写):__。
- 核心循环:__ → __ → __ → __ → 回到 __。
- 跑偏风险:__。
- 非目标:__(详见《不是什么》)。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-art-bible.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-art-bible.md
index 510ad2608..891dfbf16 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-art-bible.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-art-bible.md
@@ -1,5 +1,7 @@
### C3 02_美术圣经/模板.md(→ templates/tdd-art-bible.md)
+本模板是美术实施的参考结构。按项目实际需要选择角色、场景、UI、动画和素材契约;没有对应资产类型时删除相应章节,复杂项目可增加必要的视觉规则。表格和资产条目可按实际内容扩展,不代表数量上限。
+
# 美术圣经:《游戏名》
> 状态:{drafting / reviewed / frozen} | 定调锚:概念层@v{N} 第 2 节 | style_id:`__`
@@ -10,7 +12,7 @@ __(一段话:从定调记录翻译的视觉气质;参考图位 __ 张)
## 视觉锚
-- 关键词:__(3~5 个)。
+- 关键词:__(按项目需要)。
- 禁用关键词:__。
- 色板:主色 __ / 辅色 __ / 点缀 __(配比 __);昼夜·天气·季节表现 __。
- 形状语言:__。
@@ -40,6 +42,7 @@ __(承 UI 系统文档的界面清单;视觉语言与信息分层对齐)
| 素材 | 规格(尺寸/帧数/方向数) | 命名规则 | atlas 格式 | 验收 | 绑定 |
|---|---|---|---|---|---|
| __ | __ | __ | __ | __ | `item_ __` / 豁免:__ |
+(以上为示例,可按实际素材删减或扩充。)
- 绘制工艺:__(用陶泥儿 MCP 的路径与参数;封装流程)。
- 豁免类型仅限:程序化生成 / UI 文本 / 本期不需要。
@@ -49,17 +52,19 @@ __(承 UI 系统文档的界面清单;视觉语言与信息分层对齐)
| asset_id | 规格 | 绑定 | 状态 | 验收记录 | contract_version |
|---|---|---|---|---|---|
| __ | __ | `item_ __` / 豁免 | 缺失/草稿/已交付/已验收/已接入 | 技术过/视觉过 @__ | __ |
+(以上为示例,可按实际资产删减或扩充。)
- 状态单向流转:缺失 → 草稿 → 已交付 → 已验收 → 已接入;驳回退回草稿并记原因。
- 验收两维:技术(尺寸/透明/帧数/命名)+ 视觉(对照视觉锚);两维都过才进"已验收"。
-- 每个 gameplay 可见对象必有一行,或显式豁免——没有第三种状态。
+- 需要登记的 gameplay 可见对象有一行;不需要资产登记的对象不建立空记录。
- 程序接入后填消费点(哪个模块加载、事件映射),`contract_version` 变更须重验收。
## 量产流程与验证
1. 概念候选 __ 张 → 2. 人选方向 → 3. 锚点图 __ 张 → 4. 锁圣经 →
5. 写契约 → 6. 小批 __ 张 → 7. 技术检查(__)→ 8. 接入程序 →
-9. 运行时截图验收(桌面/移动双视口下 __ 可辨)→ 10. 扩产。
+9. 运行时验收(按项目支持的平台)→ 10. 扩产。
+(以上为示例,可按实际流程删减或扩充。)
## 开放问题回执
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-data.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-data.md
index f7eb05b2a..244e8063b 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-data.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-data.md
@@ -1,5 +1,7 @@
### C3 03_数据与配表/模板.md(→ templates/tdd-data.md)
+本模板是数据与配表的参考结构。只有项目实际存在配置、枚举、关系或条件数据时才建立对应表和校验;简单项目可以直接写配置约定,复杂项目再拆分表结构与验算流程。表格中的示例行可按实际数据、字段和验算项扩展,不代表数量上限。
+
# 数据与配表:《游戏名》
> 状态:{structuring / filling / accepted} | 基于:各系统交接节汇总 | 验收:check@{id} 最新结论 __
@@ -9,6 +11,7 @@
| 表格组 | 建议表名 | 主要维护系统 |
|---|---|---|
| __ | __ | __ |
+(以上为示例,可按实际数据表删减或扩充。)
(表格拆分是生产组织方式,不改变主数据归属。)
@@ -26,7 +29,7 @@
|---|---|---|---|---|---|---|
| __ | date_day / progress_flag / skill_level / schedule_open / quest_completed / __ | __ | __ | __ | active | __ |
-(复杂条件拆条件组+条件行;全项目只此一个条件入口,程序实现一次 `check(condition_id)`。)
+(存在复杂条件时再拆条件组与条件行;没有条件系统时删除本节。)
## 工作簿组织与建表顺序
@@ -36,6 +39,7 @@
建表顺序:①物品表(公共 item_id)→ ②__ → ③__ → ④__ → ⑤__ → ⑥__ → ⑦__ → ⑧__。
每完成一组查三件事:引用 ID 存在 / 条件有负责系统 / 同一数值只有一个系统维护。
+(以上为示例,可按实际表结构删减或扩充。)
## 表格-程序契约
@@ -54,12 +58,14 @@
| 表 | 字段 | 默认值 | 依据 | 推翻条件 |
|---|---|---|---|---|
| __ | __ | __ | T__ / 台账 id | __ |
+(以上为示例,可按实际验算字段删减或扩充。)
- 前五日闭环验算:
| 日期 | 主目标 | 关键行动 | 主要成本 | 主要获得 | 结果 |
|---|---|---|---|---|---|
| 第 1 日 | __ | __ | __ | __ | __ |
+(以上为示例,可按实际循环或阶段删减或扩充。)
- 收益链校验:`__ → __ → __ → __ → __`(逐环引 ID)。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-master.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-master.md
index cfae209d9..b8ecd9049 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-master.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-master.md
@@ -1,5 +1,7 @@
### C3 模板_TDD总册.md(→ templates/tdd-master.md)
+本模板是 TDD 总册的参考结构。只建立当前项目实际需要的技术、美术、数据和索引内容;没有对应方向时不创建空分册,复杂项目可以增加施工所需的分册。表格中的示例行可按实际分册、问题和验收项扩展,不代表数量上限。
+
# TDD 总册:《游戏名》
> 本册是 TDD 层的封面与索引:正文在三件分册(01 技术实现 / 02 美术圣经 / 03 数据与配表),
@@ -7,19 +9,19 @@
## 自足性检查(TDD 的完成判据)
-> 标准:一个施工 agent 只看 TDD,能做完完整游戏。逐项模拟它必问的问题,
+> 标准:施工方只看当前 TDD,能完成项目实际范围内的实现。逐项检查当前项目真正需要的问题,
> 答得出=过;答不出=缺口(列 GDD 来源与同步动作)。
| # | 施工 agent 的问题 | 答案在哪 | 状态 |
|---|---|---|---|
-| 1 | 每个系统怎么行为(规则/行动/反馈)? | 01 收编章(@v{N}) | __ |
-| 2 | 每张表有多少行内容、文本全填了吗? | 03 全量填充+完成度验收 | __ |
-| 3 | 每个界面长什么样、怎么走? | 01 UI 交互规格 | __ |
-| 4 | 每份素材什么规格、谁验收过? | 02 资产状态表(全行非缺失) | __ |
+| 1 | 实际存在的系统怎么行为? | 01 收编章(@v{N}) | __ |
+| 2 | 实际使用的表和配置是否可施工? | 03 数据与配表 | __ |
+| 3 | 实际存在的界面怎么走? | 01 UI 交互规格 | __ |
+| 4 | 实际需要的素材什么规格? | 02 资产状态表 | __ |
| 5 | 代码怎么组织、跑在哪? | 01 代码组织+能力边界 | __ |
| 6 | 怎么算做完了(判据)? | 01 里程碑+各件验收 | __ |
-全部为"过"时,TDD 进入 frozen——构建可以完全脱离 GDD 进行。
+当前项目所需检查全部为"过"时,TDD 进入 frozen——构建可以在本版本范围内脱离 GDD 进行。
## 三件状态
@@ -54,7 +56,7 @@
| 件 | 最近验收 | blocker | 结论 |
|---|---|---|---|
-| 01 | __(构建+双视口验证 @__) | __ | __ |
+| 01 | __(按项目平台验证 @__) | __ | __ |
| 02 | __(技术+视觉两维 @__) | __ | __ |
| 03 | __(七查 @check_id) | __ | __ |
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-tech.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-tech.md
index 42cc17b24..e8fb80ba8 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-tech.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/tdd-tech.md
@@ -1,9 +1,11 @@
### C3 01_技术实现/模板.md(→ templates/tdd-tech.md)
+本模板是技术实现的参考结构。按当前运行时、系统复杂度和用户要求选择章节;没有对应系统、界面、输入、音频或存档需求时,删除相应内容,复杂项目可增加施工所需章节。表格和系统条目可按实际实现范围扩展,不代表数量上限。
+
# 技术实现:《游戏名》
> 状态:{drafting / reviewed / frozen} | 基于 GDD:架构层@v{N} | 数据侧契约:data/contracts@v{M}
-> **目标运行时:{HTML / Unity / Godot / Cocos}(由 GDD 平台事实锁定)** | 预览:{HTML=双视口浏览器 / 引擎=陶泥儿驱动弹窗} | 导出:{HTML=自包含 / 引擎=陶泥儿驱动 CLI}
+> **目标运行时:{HTML / Unity / Godot / Cocos}(由 GDD 平台事实锁定)** | 预览:{按项目平台验证 / 引擎=陶泥儿驱动弹窗} | 导出:{HTML=自包含 / 引擎=陶泥儿驱动 CLI}
## 系统行为规格(收编章)
@@ -28,7 +30,7 @@
## 技术目标与平台事实
-- 平台事实(注入,禁改):自包含 Web · 双视口(桌面/移动)· 键鼠/触屏双输入 · 本地 HTTP 预览。
+- 平台事实(由 GDD 平台事实锁定):__。
- 技术目标:__(可测量,如"首屏可玩 ≤ __ 秒")。
## 技术风险
@@ -39,7 +41,7 @@
## 运行时能力边界
-| 能力(P0 七件) | 落位(按所选运行时) | 状态(原生/自封装/受限) | 说明 |
+| 能力(按项目实际使用的能力填写) | 落位(按所选运行时) | 状态(原生/自封装/受限) | 说明 |
|---|---|---|---|
| 瓦片地图渲染 | __ | __ | __ |
| 寻路 | __ | __ | __ |
@@ -85,7 +87,7 @@
## 构建与验证
- 构建:__(命令/流程)。
-- 验证分级:自动__(跑什么、看什么输出为过);半自动__(双视口浏览器验证步骤);手测__(谁试玩、观察什么)。
+- 验证分级:自动__(跑什么、看什么输出为过);半自动__(按项目平台验证步骤);手测__(谁试玩、观察什么)。
## 版本里程碑
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md
index a343332ca..0d25e1887 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md
@@ -1,5 +1,7 @@
### C1 模板_顶层设计.md(→ templates/top-design.md)
+本模板是参考结构,不是固定清单。填写前按项目实际存在的循环、资源、时间层级和用户要求筛选章节;同类内容可合并,若某项不存在则删除,复杂项目可增加必要内容。表格、列表和循环示例可按实际内容扩展,不代表数量上限。
+
# 顶层设计:《游戏名》
## 顶层定位与规模锚点
@@ -43,6 +45,7 @@ __ → __ → __ → __ → __。
### __循环
__ → __ → __ → __。
+(以上为示例,可按实际循环删减或扩充。)
## 资源流与输入输出
@@ -58,13 +61,14 @@ flowchart LR
## 最小体验单位
__。
-单个行动必须至少提供一种清晰反馈:__。
+保留的玩家行动应有与玩法相称的可理解反馈:__。
## 核心活动流程
| 阶段 | 玩家行为 | 设计目的 |
|---|---|---|
| __ | __ | __ |
+(按实际阶段逐行补充。)
## 取舍表
@@ -77,6 +81,7 @@ __。
- 周内节奏:__。
- 季节/章节节奏:__。
- 长期节奏:__。
+(以上为示例,可按实际节奏层级删减或扩充。)
整体情绪在"__"与"__"之间摆动(恢复来源:__;变化来源:__)。
@@ -103,9 +108,11 @@ __。
| 验证点 | 成功标准 |
|---|---|
| __ | __ |
+(按实际验证点逐行补充。)
## 开放问题
- __
+(按实际问题逐条补充。)
## 顶层定稿
顶层当前定稿为:__。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md b/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md
index 0850865e8..3807f565e 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md
@@ -1,12 +1,16 @@
你是游戏策划协作 Agent,与用户持续协作完成游戏设计。像普通策划同事一样交流,使用工作区文件工具读写资料;所有文件路径使用相对路径。根据当前对话、阶段上下文和已有文档决定下一步行动。修改文件后,简要说明修改内容和相对路径。对不确定内容区分用户确认、Agent 建议和待原型验证事项;不要把建议写成用户已确认的决定。
-优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答,并据此更新相关产物;不要带着这类未决问题提交阶段审批。
+优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答。决定稳定后,再更新受影响的正式产物和必要的过程记录;不要带着这类未决问题提交阶段审批。
+
+分析阶段优先记录当前目标、上层约束、候选方案、取舍、用户已确认或 Agent 暂定的边界,以及必须检查的验收项。除非用户明确要求展开讨论,不要先在回复中逐节起草与正式文档重复的长篇正文;形成结论后直接写入正式产物,再进行一次必要的一致性检查。文件操作前只需说明简短计划、目标文件和主要变化。
正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。
阶段审批是每个阶段的最终检查,表示本阶段产物已经完成,无未决内容,交给用户做最终检阅,不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
+过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
+
阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。除非用户主动质疑或出现新的约束冲突,不要反复要求确认历史暂定决定。
用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json b/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json
index 06c643c6b..c9c1bd6cf 100644
--- a/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json
+++ b/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json
@@ -2,7 +2,7 @@
{"type":"function","function":{"name":"get_workflow_status","description":"读取当前策划工作流状态,只返回阶段列表、当前阶段、已批准阶段和待审批阶段;不推进阶段、不提交审批、不修改文件。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"list_resources","description":"列出固定资源的逻辑目录、资源 ID、标题和简介。资源是只读的随包文档;不要猜测物理路径。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"read_resource","description":"读取一份固定资源文档全文。每次读取一个 resource_id;资源只读。读到未实现占位文档时由你自行判断和处理。","parameters":{"type":"object","properties":{"resource_id":{"type":"string"}},"required":["resource_id"],"additionalProperties":false}}},
- {"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件,优先用于已有文件的小范围修订。先读文件,以唯一且非空的 old_text 精确匹配并替换为 new_text;new_text 为空可删除片段,保留原文并追加可插入。匹配失败不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"],"additionalProperties":false}}},
+ {"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一,匹配失败、重复或范围重叠时不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"delete_path","description":"谨慎使用;永久删除工作区内的文件或目录;目录会连同全部内容递归删除,不备份。先确认目标及删除范围。path 使用相对路径,不能删除工作区根目录,也不能经过链接。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"list_dir","description":"列出工作目录内的文件和目录。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"read_file","description":"读取工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-browser-playtest/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-browser-playtest/SKILL.md
index 577d9e606..fd676d0bc 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-browser-playtest/SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-browser-playtest/SKILL.md
@@ -13,6 +13,7 @@ Use `agc_browser_playtest` from the `agc_tools` MCP server. Do not replace it wi
2. Inspect both desktop and mobile results, including page readiness, visible text, screenshots, console errors, exceptions, failed requests, Canvas probes, blocked actions, and interaction evidence.
3. Compare screenshots with the user's request. Check that the active game fills its intended area, HUD elements do not cover gameplay, controls are visible, and requested platform art appears in the core experience.
4. If evidence exposes a defect, edit the actual game files and call the tool again when that is useful. The client enforces its own execution and resource bounds; do not invent a fixed repair loop in the response.
+ Feed the structured diagnostics, console errors, failed requests, and exception text back to the same LLM repair turn before reporting the playtest as failed. Treat the evidence as debugging input and rerun the affected stage after a real code or project change.
5. Treat browser infrastructure failure, an unloaded page, an unhandled exception, or missing evidence as a failed validation. Do not claim success from a partial result.
6. Use game-specific reasoning for quality. Do not require a fixed board, fixed text, fixed number of slices, or a legacy harness scenario; the tool result is evidence for Codex to interpret.
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-game-production-workflow/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-game-production-workflow/SKILL.md
index f02b45374..1365ebd46 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-game-production-workflow/SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-game-production-workflow/SKILL.md
@@ -19,8 +19,16 @@ Use this Skill as the top-level SOP for a new game or a substantial game brief.
## Stage transitions
-Advance only when the current stage has its output: brief → inventory; inventory → art decision; art decision → usable registered assets or an explicit no-art decision; implementation → source references to those assets; build → playable entry; playtest → evidence; delivery → truthful report. If a tool fails, preserve its error and stop or repair at that stage instead of silently substituting a later-stage placeholder.
+Advance only when the current stage has its output: brief → inventory; inventory → art decision; art decision → usable registered assets or an explicit no-art decision; implementation → source references to those assets; build → playable entry; playtest → evidence; delivery → truthful report. If a tool fails, preserve its error and handle it under "Error handling" instead of silently substituting a later-stage placeholder.
For a small edit to an existing game where the brief and suitable assets are unchanged, use the focused edit path and do not regenerate art. This exception does not apply to a new game or a substantial planning brief.
+## Error handling
+
+When a stage tool, command, or verification fails, retry at most three times before treating that stage as failed. Keep the retries serial and scoped to the same stage and the same input: a retry must not open a parallel path, skip ahead to a later stage, or substitute a placeholder for the missing output.
+
+Every repairable failure must be fed back to the current LLM as the next debugging context before the stage is considered failed. Preserve the redacted tool or command error, the stage, the attempted input, and the evidence already collected; ask the LLM to inspect the current project, make the smallest real repair, and rerun the failed stage. A client-side `isError` tool result or a failed verification is feedback for the LLM, not by itself a terminal user-facing result. Do not silently swallow the error, replace it with a placeholder, or stop after the first failed attempt. Authentication, permission, billing, project identity, corrupted history, transport loss, cancellation, and uncertain paid-operation state remain terminal safety boundaries.
+
+Only after the third attempt also fails, stop and tell the user the failure reason — which stage failed, which tool or command reported the error, what the error says, and what is still missing. A stage whose three attempts never succeeded is not complete, and its missing output cannot be reported as delivered.
+
Read the referenced specialist Skills for their detailed contracts: `agc-project-structure`, `taonier-art-assets`, `agc-web-game-development`, `agc-client-projection`, and `agc-browser-playtest`.
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json
index 72dbdf5fb..f2270d7b8 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
- "version": "2026-08-26.13",
+ "version": "2026-08-26.16",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -22,7 +22,7 @@
"agents/openai.yaml",
"references/workflow-contract.md"
],
- "sha256": "91082fdff4123f1e1fcf930af433cbea51a8c9d26991678b19028b344ea49f39"
+ "sha256": "f25e5bd27e8fc82c61b08dc66366b5b253ee8d16d7fa72dbf2c94d2462f4e7fc"
},
{
"name": "agc-project-structure",
@@ -63,7 +63,7 @@
"agents/openai.yaml",
"references/platform-art-contract.md"
],
- "sha256": "bd1e415aac0cd0f97090296f34c67898dd731d1e177ec91a56027f9b68a88b37"
+ "sha256": "ff3e1645a35fc9bff1ef255aa7bdc2a9729843d68729589b6f2670c84b8130ec"
},
{
"name": "agc-web-game-development",
@@ -98,7 +98,7 @@
"agents/openai.yaml",
"references/browser-evidence-contract.md"
],
- "sha256": "4437cd8a927a1c79a5faf4bcd40e9946676c08a3b460ab171298cabf899f49ad"
+ "sha256": "92ecce42d6589e034d32b75bcd155c1fee34a8c7b843eea5780c0577300ed521"
},
{
"name": "agc-client-projection",
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md
index 8d63beb92..d652b4436 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/SKILL.md
@@ -19,6 +19,12 @@ image, UI design image, or publication material; use `agc_edit_image` for an
edit of an existing registered image; use `taonier_prepare_game_art` only for
the complete game-art package and its canonical slices.
+When `agc_generate_image` is used with `kind="art-spritesheet"`, pass
+`sliceMode="connected-components"` (the default alpha-connectivity splitter)
+or `sliceMode="grid"` with `gridX` and `gridY` (1-32 each). The selected mode is carried
+through the client request and returned result; do not infer it from the number
+of slices.
+
## Authorization boundary
`agc_tools` is an AGC client-owned bridge to the AGC backend. In the normal client build it uses the current client login session and account routes; the user and model never need to provide, configure, paste, create, or rotate an API Key, Token, Cookie, URL, or `.env` value. If the tool returns `401` or `403`, report only that the AGC client login or permission state is unavailable, stop the operation, and do not ask the user for credentials or expose an internal URL.
diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md
index 9a04e1256..bf0b74481 100644
--- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md
+++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/taonier-art-assets/references/platform-art-contract.md
@@ -15,6 +15,7 @@
- On timeout or uncertain delivery, reuse the recorded operation; never create a replacement request.
- `postprocess-failed-source-preserved` means the complete provider source remains usable, but the requested transparent derivative is absent.
- `sliceWarning` means the complete transparent sheet remains usable, but individual slices are absent.
+- For direct `agc_generate_image` spritesheet requests, `sliceMode="connected-components"` selects alpha-connectivity detection and `sliceMode="grid"` uses the caller-provided `gridX` and `gridY` (1-32 each). The client preserves the selected mode and grid dimensions in the request identity and result metadata.
- General and slice warnings can coexist. The tool returns them separately through `warnings` and `sliceWarnings`; callers must preserve every entry and must not downgrade a slice warning into a successful independent-asset claim.
- `assetPaths` contains the complete package paths. `slicePaths` contains only slices that the client downloaded, validated, and registered with their platform source identities.
- `resources` contains only safe registered identity fields: local asset/path/kind/media type, Canvas project/resource/asset/task IDs, and reference resource IDs. It never exposes prompts, models, provider routes, absolute paths, URLs, tokens, cookies, or API keys.
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs
index 17b8a6700..e64b321dd 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs
@@ -16,25 +16,31 @@ mod design_runtime;
mod design_tools;
mod direct_codex_attachments;
mod direct_codex_audit;
-mod direct_codex_references;
+mod direct_codex_user_item;
mod direct_project_history;
mod direct_project_turn_history;
mod direct_runtime;
+mod direct_thread_manager;
mod direct_tool_bridge;
+mod direct_tool_calls;
mod direct_tools_mcp;
+mod direct_turn_stream;
mod generation;
mod interaction;
mod prompt;
mod runtime_actions;
mod runtime_adapter;
mod runtime_driver;
+mod runtime_error;
mod runtime_protocol;
mod runtime_state;
mod runtime_tools;
mod skill_pack;
use codex_app_server::*;
pub(crate) use codex_app_server::{
- direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat,
+ cancel_direct_codex_turn_at,
+ direct_codex_canonical_project_identity_for_commands as direct_codex_canonical_project_identity,
+ direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat, DirectTurnCancelView,
};
use codex_cli::*;
pub(crate) use codex_cli::{
@@ -44,18 +50,22 @@ pub(crate) use codex_provider_proxy::*;
pub(crate) use design_runtime::*;
pub(crate) use direct_codex_attachments::*;
pub(crate) use direct_codex_audit::*;
-pub(crate) use direct_codex_references::*;
+pub(crate) use direct_codex_user_item::*;
pub(crate) use direct_project_history::*;
pub(crate) use direct_project_turn_history::*;
pub(crate) use direct_runtime::*;
+pub(crate) use direct_thread_manager::*;
pub(crate) use direct_tool_bridge::*;
+pub(crate) use direct_tool_calls::*;
pub(crate) use direct_tools_mcp::*;
+pub(crate) use direct_turn_stream::*;
pub(crate) use generation::*;
pub(crate) use interaction::*;
pub(crate) use prompt::*;
pub(crate) use runtime_actions::*;
pub(crate) use runtime_adapter::*;
pub(crate) use runtime_driver::*;
+pub(crate) use runtime_error::*;
pub(crate) use runtime_protocol::*;
pub(crate) use runtime_state::*;
pub(crate) use runtime_tools::*;
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs
new file mode 100644
index 000000000..d903e008b
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_history_wire.rs
@@ -0,0 +1,128 @@
+//! DirectProject 历史注入载荷的单一构造 seam。
+//!
+//! 历史读取与大小前置校验集中在这里;调用方只负责线程生命周期与 RPC 传输。
+
+use super::super::*;
+use super::direct_project_history_injection_oversize_error;
+use serde_json::Value;
+use std::path::Path;
+
+const DIRECT_PROJECT_HISTORY_IMAGE_TOTAL_MAX_BYTES: usize = 8 * 1024 * 1024;
+const DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT: &str =
+ "[历史图片预览已省略:本次恢复图片预算已用尽]";
+
+fn omit_image_block(object: &mut serde_json::Map, text_type: &str) {
+ object.clear();
+ object.insert("type".to_string(), Value::String(text_type.to_string()));
+ object.insert(
+ "text".to_string(),
+ Value::String(DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT.to_string()),
+ );
+}
+
+fn compact_history_images(value: &mut Value, remaining_bytes: &mut usize) {
+ match value {
+ Value::Array(values) => values
+ .iter_mut()
+ .for_each(|value| compact_history_images(value, remaining_bytes)),
+ Value::Object(object) => {
+ let is_image_block = object.get("type").and_then(Value::as_str) == Some("image");
+ if is_image_block {
+ if let Some(data) = object.get("data").and_then(Value::as_str) {
+ if let Some((preview, mime_type)) = crate::agent::compact_mcp_image_data(data) {
+ if preview.len() > *remaining_bytes {
+ omit_image_block(object, "text");
+ } else {
+ *remaining_bytes -= preview.len();
+ object.insert("data".to_string(), Value::String(preview));
+ object.insert(
+ "mimeType".to_string(),
+ Value::String(mime_type.to_string()),
+ );
+ }
+ }
+ }
+ }
+ if object.get("type").and_then(Value::as_str) == Some("input_image") {
+ if let Some(url) = object
+ .get("image_url")
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ {
+ if let Some((header, data)) = url.split_once(",") {
+ if header.ends_with(";base64") {
+ if let Some((preview, mime_type)) =
+ crate::agent::compact_mcp_image_data(data)
+ {
+ if preview.len() > *remaining_bytes {
+ omit_image_block(object, "input_text");
+ } else {
+ *remaining_bytes -= preview.len();
+ object.insert(
+ "image_url".to_string(),
+ Value::String(format!("data:{mime_type};base64,{preview}")),
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ object
+ .values_mut()
+ .for_each(|value| compact_history_images(value, remaining_bytes));
+ }
+ Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
+ }
+}
+
+pub(super) fn build_direct_project_history_injection_params(
+ history_root: &Path,
+ thread_id: &str,
+) -> Result {
+ let canonical_items = read_direct_project_history_items_at(history_root)
+ .map_err(platform_llm::LlmError::InvalidRequest)?;
+ let mut remaining_image_bytes = DIRECT_PROJECT_HISTORY_IMAGE_TOTAL_MAX_BYTES;
+ let items = canonical_items
+ .iter()
+ .map(|item| {
+ let mut projected = direct_codex_user_item_to_response_item(history_root, item)
+ .map_err(platform_llm::LlmError::InvalidRequest)?;
+ compact_history_images(&mut projected, &mut remaining_image_bytes);
+ Ok(projected)
+ })
+ .collect::, _>>()?;
+ let params = serde_json::json!({"threadId": thread_id, "items": items});
+ let payload_bytes = serde_json::to_vec(¶ms)
+ .map(|bytes| bytes.len().saturating_add(1))
+ .unwrap_or(usize::MAX);
+ // 注入前的前置校验:失败关闭并指名 itemId 与字节数,**不截断、不摘要、不改写**。
+ if let Some(error) = direct_project_history_injection_oversize_error(¶ms, payload_bytes) {
+ return Err(platform_llm::LlmError::InvalidRequest(error));
+ }
+ Ok(params)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{compact_history_images, DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT};
+ use serde_json::json;
+
+ #[test]
+ fn history_image_budget_omits_only_wire_preview_when_exhausted() {
+ let mut item = json!({
+ "type": "function_call_output",
+ "output": {"content": [{
+ "type": "image",
+ "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
+ "mimeType": "image/png"
+ }]}
+ });
+ let mut remaining = 1;
+ compact_history_images(&mut item, &mut remaining);
+ let block = &item["output"]["content"][0];
+ assert_eq!(block["type"], "text");
+ assert_eq!(block["text"], DIRECT_PROJECT_HISTORY_IMAGE_OMITTED_TEXT);
+ assert_eq!(remaining, 1);
+ }
+}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs
new file mode 100644
index 000000000..901467454
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs
@@ -0,0 +1,53 @@
+//! DirectProject 线程池身份的 canonical 解析与摘要。
+
+use super::super::*;
+use sha2::{Digest, Sha256};
+
+pub(crate) fn direct_codex_canonical_project_identity(
+ root: &std::path::Path,
+) -> Result<(std::path::PathBuf, String), String> {
+ let (canonical_root, _) = resolve_direct_codex_project_authority(root)?;
+ let manifest = read_manifest(&canonical_root.join(".agent/manifest.json"))
+ .map_err(|error| format!("读取 DirectProject 权威项目身份失败:{error}"))?;
+ let manifest_project_id = manifest.project_id.trim();
+ if manifest_project_id.is_empty() || manifest_project_id.chars().count() > 256 {
+ return Err("DirectProject manifest.projectId 不满足身份边界".to_string());
+ }
+ let path_identity = direct_codex_os_path_identity_bytes(&canonical_root);
+ Ok((
+ canonical_root,
+ direct_codex_project_identity_digest(&path_identity, manifest_project_id.as_bytes()),
+ ))
+}
+
+pub(super) fn direct_codex_os_path_identity_bytes(path: &std::path::Path) -> Vec {
+ #[cfg(unix)]
+ {
+ use std::os::unix::ffi::OsStrExt;
+ return path.as_os_str().as_bytes().to_vec();
+ }
+ #[cfg(windows)]
+ {
+ use std::os::windows::ffi::OsStrExt;
+ let mut bytes = Vec::new();
+ for unit in path.as_os_str().encode_wide() {
+ bytes.extend_from_slice(&unit.to_le_bytes());
+ }
+ return bytes;
+ }
+ #[cfg(not(any(unix, windows)))]
+ path.as_os_str().to_string_lossy().as_bytes().to_vec()
+}
+
+pub(super) fn direct_codex_project_identity_digest(
+ path_identity: &[u8],
+ project_id: &[u8],
+) -> String {
+ let mut digest = Sha256::new();
+ digest.update(b"genarrative-direct-project-identity.v1\0");
+ digest.update((path_identity.len() as u64).to_le_bytes());
+ digest.update(path_identity);
+ digest.update((project_id.len() as u64).to_le_bytes());
+ digest.update(project_id);
+ format!("{:x}", digest.finalize())
+}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs
similarity index 88%
rename from apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs
rename to apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs
index b2aaf7048..25d1591e4 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs
@@ -10,6 +10,12 @@ use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufR
use tokio::sync::{mpsc, oneshot, Mutex, Notify};
use uuid::Uuid;
+mod direct_project_history_wire;
+use direct_project_history_wire::build_direct_project_history_injection_params;
+mod direct_project_identity;
+pub(crate) use direct_project_identity::direct_codex_canonical_project_identity as direct_codex_canonical_project_identity_for_commands;
+use direct_project_identity::*;
+
const GAME_CREATOR_CODEX_APP_SERVER_PROVIDER_ID: &str = "genarrative_agc";
const GAME_CREATOR_CODEX_APP_SERVER_API_KEY_ENV: &str = "GENARRATIVE_AGC_CODEX_API_KEY";
const GAME_CREATOR_CODEX_APP_SERVER_REMOTE_CONTROL_DISABLED_ENV: &str =
@@ -216,6 +222,12 @@ impl CodexTurnStartCancellation {
self.maybe_interrupt();
}
+ /// app-server 连接是否还活着:句柄只剩 Weak 时说明进程已被回收,此时"终止"必须
+ /// 明确报错,而不是静默成功让界面以为回合已经停了。
+ fn app_server_alive(&self) -> bool {
+ self.inner.strong_count() > 0
+ }
+
fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
self.maybe_interrupt();
@@ -271,6 +283,51 @@ fn game_creator_codex_app_server_error_kind(kind: &str) -> platform_llm::LlmErro
))
}
+fn game_creator_codex_app_server_error_kind_with_machine_detail(
+ kind: &str,
+ error: &serde_json::Value,
+) -> platform_llm::LlmError {
+ let mut fields = Vec::new();
+ if let Some(object) = error.as_object() {
+ if let Some(code) = object.get("code").and_then(serde_json::Value::as_str) {
+ if !code.is_empty()
+ && code.len() <= 80
+ && code
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
+ {
+ fields.push(format!("code={code}"));
+ }
+ }
+ let keys = object
+ .keys()
+ .filter(|key| {
+ matches!(
+ key.as_str(),
+ "httpConnectionFailed"
+ | "responseStreamConnectionFailed"
+ | "responseStreamDisconnected"
+ | "responseTooManyFailedAttempts"
+ | "activeTurnNotSteerable"
+ | "codexErrorInfo"
+ )
+ })
+ .cloned()
+ .collect::>();
+ if !keys.is_empty() {
+ fields.push(format!("fields={}", keys.join(",")));
+ }
+ }
+ let suffix = if fields.is_empty() {
+ String::new()
+ } else {
+ format!(" detail={}", fields.join(" "))
+ };
+ platform_llm::LlmError::InvalidRequest(format!(
+ "{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}{kind}{suffix}"
+ ))
+}
+
fn game_creator_codex_app_server_error_http_status(
info: &serde_json::Value,
field: &str,
@@ -425,7 +482,7 @@ fn game_creator_codex_app_server_failed_turn_error(
return game_creator_codex_app_server_error_kind("unauthorized");
}
let Some(info) = error.get("codexErrorInfo").filter(|info| !info.is_null()) else {
- return game_creator_codex_app_server_error_kind("other");
+ return game_creator_codex_app_server_error_kind_with_machine_detail("other", error);
};
if let Some(kind) = info.as_str() {
return match kind {
@@ -449,8 +506,8 @@ fn game_creator_codex_app_server_failed_turn_error(
game_creator_codex_app_server_error_kind("thread-rollback-failed")
}
"sandboxError" => game_creator_codex_app_server_error_kind("sandbox-error"),
- "other" => game_creator_codex_app_server_error_kind("other"),
- _ => game_creator_codex_app_server_error_kind("other"),
+ "other" => game_creator_codex_app_server_error_kind_with_machine_detail("other", error),
+ _ => game_creator_codex_app_server_error_kind_with_machine_detail("other", error),
};
}
for field in [
@@ -466,7 +523,7 @@ fn game_creator_codex_app_server_failed_turn_error(
if info.get("activeTurnNotSteerable").is_some() {
return game_creator_codex_app_server_error_kind("active-turn-not-steerable");
}
- game_creator_codex_app_server_error_kind("other")
+ game_creator_codex_app_server_error_kind_with_machine_detail("other", error)
}
async fn isolate_game_creator_codex_app_server_terminal_unknown(
@@ -513,6 +570,10 @@ enum CodexTurnEvent {
completed: bool,
params: serde_json::Value,
},
+ Request {
+ event_type: &'static str,
+ params: serde_json::Value,
+ },
RawItem(serde_json::Value),
Terminal(serde_json::Value),
TransportClosed(String),
@@ -521,8 +582,21 @@ enum CodexTurnEvent {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum DirectCodexTurnObservation {
AccumulatedText(String),
+ /// 一个 assistant 文本段的当前累计全文。
+ ///
+ /// `item_id` 是一次 assistant 消息的稳定身份:同一个 id 的后续 delta 属于**同一段**,
+ /// id 变了就是新的一段。回合流的"文本段 + 工具"顺序用它来分段,而不是按 delta 分。
+ AgentMessageSegment {
+ item_id: String,
+ accumulated_text: String,
+ completed: bool,
+ },
IntermediateText(String),
+ /// 模型的思考过程(reasoning item 的明文摘要):流式阶段整段替换下发。
+ Reasoning(String),
Activity(&'static str),
+ /// 一条结构化工具调用(`item/started` 与 `item/completed` 各采一次,按 id 幂等)。
+ ToolCall(crate::DirectToolCall),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -667,6 +741,25 @@ fn direct_codex_safe_activity_for_item_value(item: &serde_json::Value) -> &'stat
direct_codex_safe_activity_for_item(item_type)
}
+/// Project an app-server item into the small public payload carried by the
+/// DirectProject event queue. Full item contents are persisted in JSONL and
+/// must not be forwarded through the runtime event stream.
+fn direct_thread_item_started_payload(item: &serde_json::Value) -> serde_json::Value {
+ serde_json::json!({
+ "itemType": item
+ .get("type")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or("unknown"),
+ })
+}
+
+fn direct_thread_item_id(item: &serde_json::Value) -> Option {
+ item.get("id")
+ .and_then(serde_json::Value::as_str)
+ .filter(|value| !value.is_empty())
+ .map(str::to_string)
+}
+
fn direct_codex_command_is_game_verification(command: &str) -> bool {
let command = command.to_ascii_lowercase();
command.contains("game.static_smoke")
@@ -788,6 +881,37 @@ fn direct_codex_mcp_tool_intermediate_text(item: &serde_json::Value) -> String {
/// (with the concrete command/tool/path) while tools run; it does not push
/// plan/reasoning text deltas. Showing what the agent is actually doing is
/// the only reliable way to make the execution phase feel alive.
+/// 从 reasoning item 里抽明文思考文本:优先 `summary[].text`,其次 `content[].text`。
+///
+/// Codex 的 reasoning item 形如
+/// `{ "type": "reasoning", "summary": [...], "content": [{ "text": "..." }], "encrypted_content": ... }`,
+/// 没有 `role` 字段;明文(至少 content/summary 之一)存在时我们才展示,拿不到就返回 None。
+fn direct_codex_item_reasoning_text(item: &serde_json::Value) -> Option {
+ if item.get("type").and_then(serde_json::Value::as_str) != Some("reasoning") {
+ return None;
+ }
+ let collect = |key: &str| -> Option {
+ let parts = item
+ .get(key)?
+ .as_array()?
+ .iter()
+ .filter_map(|entry| {
+ entry
+ .get("text")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|text| !text.is_empty())
+ })
+ .collect::>();
+ if parts.is_empty() {
+ None
+ } else {
+ Some(parts.join("\n\n"))
+ }
+ };
+ collect("summary").or_else(|| collect("content"))
+}
+
fn direct_codex_item_intermediate_text(item: &serde_json::Value) -> Option {
const MAX_ITEM_TEXT_CHARS: usize = 240;
let item_type = item
@@ -840,7 +964,7 @@ fn direct_codex_safe_activity_for_notification(method: &str) -> Option<&'static
| "item/reasoning/summaryTextDelta"
| "item/reasoning/summaryPartAdded"
| "item/reasoning/textDelta" => Some("preparing"),
- "item/mcpToolCall/progress" | "serverRequest/resolved" => Some("controlled-tool"),
+ "item/mcpToolCall/progress" => Some("controlled-tool"),
"item/fileChange/outputDelta" | "item/fileChange/patchUpdated" => Some("file-write"),
"command/exec/outputDelta"
| "process/outputDelta"
@@ -850,6 +974,23 @@ fn direct_codex_safe_activity_for_notification(method: &str) -> Option<&'static
}
}
+fn direct_codex_request_event_type(method: &str) -> Option<&'static str> {
+ match method {
+ "item/fileChange/requestApproval"
+ | "item/commandExecution/requestApproval"
+ | "item/permissions/requestApproval" => Some("approval.requested"),
+ "item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => Some("ask.requested"),
+ _ => None,
+ }
+}
+
+fn direct_codex_resolution_event_type(method: &str) -> Option<&'static str> {
+ match method {
+ "serverRequest/resolved" => Some("request.resolved"),
+ _ => None,
+ }
+}
+
fn direct_codex_intermediate_text_for_notification(
method: &str,
params: &serde_json::Value,
@@ -2620,6 +2761,7 @@ impl CodexAppServerConnection {
request,
None,
None,
+ None,
on_agent_message_delta,
direct_observer,
audit,
@@ -2634,6 +2776,7 @@ impl CodexAppServerConnection {
request: LlmRunRequest,
direct_history_root: Option<&std::path::Path>,
direct_client_turn_id: Option<&str>,
+ direct_user_item: Option<&serde_json::Value>,
mut on_agent_message_delta: Option<&mut (dyn FnMut(&platform_llm::LlmStreamDelta) + Send)>,
mut direct_observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
mut audit: Option<&mut DirectCodexTurnAudit>,
@@ -2641,6 +2784,12 @@ impl CodexAppServerConnection {
let _turn_guard = self.inner.turn_gate.lock().await;
let mut request = request;
let history_root = direct_history_root.unwrap_or(&self.inner.workspace_path);
+ // 工具调用卡片的 turnId 用 AGC 客户端回合 id(与实时事件、落盘条目同一口径),
+ // 不用 Codex app-server 自己的 turnId——前端要按它把卡片挂回对应的那一轮。
+ let direct_tool_call_turn_id: Option = direct_client_turn_id
+ .map(str::trim)
+ .filter(|turn_id| !turn_id.is_empty())
+ .map(str::to_string);
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
let current_prompt = direct_codex_current_user_prompt(&request).trim();
if current_prompt.is_empty() {
@@ -2649,12 +2798,19 @@ impl CodexAppServerConnection {
));
}
if let Some(client_turn_id) = direct_client_turn_id {
- let user_item = direct_project_local_message_item(
- "user",
- current_prompt,
- Some(&format!("direct-codex:{client_turn_id}:user")),
- )
- .map_err(platform_llm::LlmError::InvalidRequest)?;
+ let user_item = match direct_user_item {
+ Some(item) => {
+ direct_codex_user_item_to_response_item(history_root, item)
+ .map_err(platform_llm::LlmError::InvalidRequest)?;
+ item.clone()
+ }
+ None => direct_project_local_message_item(
+ "user",
+ current_prompt,
+ Some(&format!("direct-codex:{client_turn_id}:user")),
+ )
+ .map_err(platform_llm::LlmError::InvalidRequest)?,
+ };
append_direct_project_user_message_at(history_root, &user_item)
.map_err(platform_llm::LlmError::InvalidRequest)?;
}
@@ -2664,24 +2820,14 @@ impl CodexAppServerConnection {
let thread_id = thread_lease.thread_id.clone();
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
if thread_created {
- let items = match read_direct_project_history_items_at(history_root) {
- Ok(items) => items,
- Err(error) => {
- self.release_thread(snapshot, &thread_id).await;
- return Err(platform_llm::LlmError::InvalidRequest(error));
- }
- };
- let params = serde_json::json!({"threadId": thread_id.clone(), "items": items});
- let payload_bytes = serde_json::to_vec(¶ms)
- .map(|bytes| bytes.len().saturating_add(1))
- .unwrap_or(usize::MAX);
- // 注入前的前置校验:失败关闭并指名 itemId 与字节数,**不截断、不摘要、不改写**。
- if let Some(error) =
- direct_project_history_injection_oversize_error(¶ms, payload_bytes)
- {
- self.release_thread(snapshot, &thread_id).await;
- return Err(platform_llm::LlmError::InvalidRequest(error));
- }
+ let params =
+ match build_direct_project_history_injection_params(history_root, &thread_id) {
+ Ok(params) => params,
+ Err(error) => {
+ self.release_thread(snapshot, &thread_id).await;
+ return Err(error);
+ }
+ };
if let Err(error) = self.request("thread/inject_items", params).await {
self.release_thread(snapshot, &thread_id).await;
return Err(platform_llm::LlmError::Transport(error));
@@ -2731,6 +2877,17 @@ impl CodexAppServerConnection {
}
let turn_start_cancellation =
Arc::new(CodexTurnStartCancellation::new(&self.inner, &thread_id));
+ // Direct 回合登记为"可终止":终止命令只作用在这一轮上,回合结束时自动注销。
+ let _active_turn_guard = direct_tool_call_turn_id
+ .as_deref()
+ .filter(|_| self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject)
+ .map(|turn_id| {
+ register_active_direct_codex_turn(
+ direct_codex_active_turn_key(history_root),
+ turn_id,
+ Arc::clone(&turn_start_cancellation),
+ )
+ });
let mut turn_start_guard = CodexTurnStartGuard {
cancellation: Arc::clone(&turn_start_cancellation),
armed: true,
@@ -2767,6 +2924,21 @@ impl CodexAppServerConnection {
}
};
turn_start_guard.armed = false;
+ let direct_thread_id = history_root.to_string_lossy().into_owned();
+ if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: "turn.started".to_string(),
+ turn_id: turn_id.clone(),
+ item_id: None,
+ payload: serde_json::json!({
+ "threadId": thread_id,
+ "turnId": turn_id,
+ }),
+ },
+ );
+ }
let mut receiver = self.register_turn(&turn_id).await;
let mut direct_project_history = DirectProjectHistoryAccumulator::default();
let mut guard = CodexTurnGuard {
@@ -2818,17 +2990,40 @@ impl CodexAppServerConnection {
Some(CodexTurnEvent::AgentMessageDelta { item_id, delta }) => {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
direct_project_history.observe_delta(&item_id, &delta);
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: "item.delta".to_string(),
+ turn_id: turn_id.clone(),
+ item_id: Some(item_id.clone()),
+ payload: serde_json::json!({ "delta": delta.clone() }),
+ },
+ );
}
streamed_text.push_str(&delta);
if let Some(observer) = direct_observer.as_deref_mut() {
observer(DirectCodexTurnObservation::AccumulatedText(
streamed_text.clone(),
));
+ // 同一个 assistant item 的当前累计全文:回合流按 item 分段,
+ // 段内只追加、段间才换行,不能拿"整轮累计"当一段。
+ let segment_text = direct_project_history
+ .accumulated_text_for(&item_id)
+ .unwrap_or_else(|| delta.clone());
+ if !segment_text.trim().is_empty() {
+ observer(DirectCodexTurnObservation::AgentMessageSegment {
+ item_id: item_id.clone(),
+ accumulated_text: segment_text,
+ completed: false,
+ });
+ }
}
if let Some(callback) = on_agent_message_delta.as_deref_mut() {
callback(&platform_llm::LlmStreamDelta {
accumulated_text: streamed_text.clone(),
delta_text: delta,
+ accumulated_reasoning: String::new(),
+ reasoning_delta: String::new(),
finish_reason: None,
});
}
@@ -2858,6 +3053,37 @@ impl CodexAppServerConnection {
})?
.map_err(platform_llm::LlmError::InvalidRequest)?;
direct_project_history.complete_item(&item);
+ let item_id = direct_thread_item_id(&item);
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: "item.completed".to_string(),
+ turn_id: turn_id.clone(),
+ item_id,
+ payload: serde_json::json!({}),
+ },
+ );
+ }
+ }
+ Some(CodexTurnEvent::Request { event_type, params }) => {
+ if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
+ let request_id = params
+ .get("requestId")
+ .and_then(serde_json::Value::as_str)
+ .or_else(|| params.get("id").and_then(serde_json::Value::as_str))
+ .filter(|value| !value.is_empty())
+ .map(str::to_string);
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: event_type.to_string(),
+ turn_id: turn_id.clone(),
+ item_id: None,
+ payload: request_id
+ .map(|id| serde_json::json!({ "requestId": id }))
+ .unwrap_or_else(|| serde_json::json!({})),
+ },
+ );
}
}
Some(CodexTurnEvent::Activity(activity)) => {
@@ -2880,6 +3106,10 @@ impl CodexAppServerConnection {
// 让执行期间聊天窗口显示“正在做什么”,而不是只
// 有活动状态来回跳动。completed 事件不再重复。
if !completed {
+ if let Some(reasoning) = direct_codex_item_reasoning_text(item)
+ {
+ observer(DirectCodexTurnObservation::Reasoning(reasoning));
+ }
if let Some(text) = direct_codex_item_intermediate_text(item) {
observer(DirectCodexTurnObservation::IntermediateText(
text,
@@ -2895,6 +3125,24 @@ impl CodexAppServerConnection {
completed,
¶ms,
);
+ // 工具调用卡片:item/started 与 item/completed 各采一次,
+ // 由下游按 id 幂等 upsert 成同一条。采集失败(拿不到 id /
+ // 非工具类 item)就静默跳过,不影响这一轮的其它投影。
+ if let Some(turn_id) = direct_tool_call_turn_id.as_deref() {
+ if let Some(tool_call) = direct_tool_call_from_item(
+ history_root,
+ item,
+ turn_id,
+ completed,
+ direct_tool_call_now_ms(),
+ ) {
+ if let Some(observer) = direct_observer.as_deref_mut() {
+ observer(DirectCodexTurnObservation::ToolCall(
+ tool_call,
+ ));
+ }
+ }
+ }
if completed {
if let Some(audit) = audit.as_mut() {
audit.observe_item(¶ms);
@@ -2902,6 +3150,27 @@ impl CodexAppServerConnection {
}
}
if item_type == "agentMessage" {
+ // 某些 app-server 实现会在工具开始后停止发送 agentMessage delta,
+ // 但会在 item/completed 携带完整文本。把这份最终快照补进回合流,
+ // 让流中的文本段不会停在工具前的短前缀。
+ if completed {
+ if let (Some(item_id), Some(text)) = (
+ item.get("id").and_then(serde_json::Value::as_str),
+ item.get("text")
+ .and_then(serde_json::Value::as_str)
+ .filter(|value| !value.trim().is_empty()),
+ ) {
+ if let Some(observer) = direct_observer.as_deref_mut() {
+ observer(
+ DirectCodexTurnObservation::AgentMessageSegment {
+ item_id: item_id.to_string(),
+ accumulated_text: text.to_string(),
+ completed: true,
+ },
+ );
+ }
+ }
+ }
if let Some(text) = item
.get("text")
.and_then(serde_json::Value::as_str)
@@ -2920,27 +3189,70 @@ impl CodexAppServerConnection {
self.inner.workspace_mode.passive_item_boundary_name(),
)));
}
+ if !completed
+ && self.inner.workspace_mode
+ == CodexAppServerWorkspaceMode::DirectProject
+ {
+ let item_id = direct_thread_item_id(item);
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: "item.started".to_string(),
+ turn_id: turn_id.clone(),
+ item_id,
+ payload: direct_thread_item_started_payload(item),
+ },
+ );
+ }
}
}
Some(CodexTurnEvent::Terminal(params)) => {
let turn = params.get("turn").unwrap_or(¶ms);
- if final_text.is_none() {
- final_text = turn
- .get("items")
- .and_then(serde_json::Value::as_array)
- .and_then(|items| {
- items.iter().rev().find_map(|item| {
- (item.get("type")?.as_str()? == "agentMessage")
- .then(|| item.get("text")?.as_str().map(str::to_string))
- .flatten()
- })
- });
+ if let Some(items) = turn.get("items").and_then(serde_json::Value::as_array)
+ {
+ for item in items {
+ if item.get("type").and_then(serde_json::Value::as_str)
+ != Some("agentMessage")
+ {
+ continue;
+ }
+ if let Some(text) = item
+ .get("text")
+ .and_then(serde_json::Value::as_str)
+ .filter(|text| !text.trim().is_empty())
+ {
+ final_text = Some(text.to_string());
+ if let (Some(item_id), Some(observer)) = (
+ item.get("id").and_then(serde_json::Value::as_str),
+ direct_observer.as_deref_mut(),
+ ) {
+ observer(DirectCodexTurnObservation::AgentMessageSegment {
+ item_id: item_id.to_string(),
+ accumulated_text: text.to_string(),
+ completed: true,
+ });
+ }
+ }
+ }
}
- match turn
+ let status = turn
.get("status")
.and_then(serde_json::Value::as_str)
- .unwrap_or_default()
+ .unwrap_or_default();
+ if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject
+ && matches!(status, "completed" | "interrupted" | "failed")
{
+ append_direct_thread_event(
+ &direct_thread_id,
+ DirectThreadRawEventDraft {
+ event_type: "turn.completed".to_string(),
+ turn_id: turn_id.clone(),
+ item_id: None,
+ payload: serde_json::json!({ "status": status }),
+ },
+ );
+ }
+ match status {
"completed" => {
return final_text
.filter(|text| !text.trim().is_empty())
@@ -3020,6 +3332,223 @@ impl Drop for CodexTurnStartGuard {
}
}
+/// Direct 回合中断表:与具体取消句柄解耦的最小实现,"选哪一轮 / 注销哪一轮"可单测。
+struct DirectCodexActiveTurnTable {
+ entries: HashMap,
+}
+
+impl DirectCodexActiveTurnTable {
+ fn new() -> Self {
+ Self {
+ entries: HashMap::new(),
+ }
+ }
+
+ fn register(&mut self, key: std::path::PathBuf, client_turn_id: &str, value: T) {
+ self.entries
+ .insert(key, (client_turn_id.to_string(), value));
+ }
+
+ /// 只有当前登记项仍是本回合的句柄时才注销,避免旧回合的收尾清掉后来注册的回合。
+ fn unregister(&mut self, key: &Path, is_same: impl Fn(&T) -> bool) {
+ if self
+ .entries
+ .get(key)
+ .is_some_and(|(_, value)| is_same(value))
+ {
+ self.entries.remove(key);
+ }
+ }
+
+ /// 选中要终止的回合:没有活动回合、或前端给的 clientTurnId 与活动回合不一致时都返回
+ /// 可读原因,绝不误伤另一个回合。
+ fn select(&self, key: &Path, client_turn_id: Option<&str>) -> Result<&(String, T), String> {
+ let active = self
+ .entries
+ .get(key)
+ .ok_or_else(|| "当前项目没有正在运行的陶泥儿回合,无法终止".to_string())?;
+ if let Some(expected) = client_turn_id
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ {
+ if active.0 != expected {
+ return Err(DIRECT_CODEX_ANOTHER_TURN_RUNNING_MESSAGE.to_string());
+ }
+ }
+ Ok(active)
+ }
+
+ /// 当前登记在这一轮上的 clientTurnId;没有任何登记时返回 `None`。
+ fn registered_client_turn_id(&self, key: &Path) -> Option<&str> {
+ self.entries
+ .get(key)
+ .map(|(client_turn_id, _)| client_turn_id.as_str())
+ }
+}
+
+/// "正在跑的是另一轮"的统一文案:`select` 与"终止"兜底路径共用,保证两处拒绝语义一致。
+const DIRECT_CODEX_ANOTHER_TURN_RUNNING_MESSAGE: &str = "正在运行的是另一个陶泥儿回合,已拒绝终止";
+
+/// 正在运行的 Direct 回合中断句柄,按项目根(canonical,去掉 Windows `\\?\` 前缀)索引。
+///
+/// `CodexTurnStartCancellation` 本身已经能在 turn/start 响应到达**前后**发出
+/// `turn/interrupt`;这里只是把它留一个 Tauri 命令取得到的引用,回合结束后由
+/// [`DirectCodexActiveTurnGuard`] 移除。只做新增:不改既有事件、命令语义。
+static GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS: OnceLock<
+ std::sync::Mutex>>,
+> = OnceLock::new();
+
+fn direct_codex_active_turns(
+) -> &'static std::sync::Mutex>> {
+ GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS
+ .get_or_init(|| std::sync::Mutex::new(DirectCodexActiveTurnTable::new()))
+}
+
+/// 注册键:与 Direct 回合用的 `codex_root` 同一形态(canonical 且去掉 `\\?\` 前缀),
+/// 这样前端传进来的项目路径与注册时的路径一定落到同一个键上。
+fn direct_codex_active_turn_key(root: &Path) -> std::path::PathBuf {
+ let canonical = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
+ match canonical
+ .to_str()
+ .and_then(|value| value.strip_prefix("\\\\?\\"))
+ {
+ Some(stripped) => std::path::PathBuf::from(stripped),
+ None => canonical,
+ }
+}
+
+struct DirectCodexActiveTurnGuard {
+ key: std::path::PathBuf,
+ cancellation: Arc,
+}
+
+impl Drop for DirectCodexActiveTurnGuard {
+ fn drop(&mut self) {
+ let Some(active_turns) = GAME_CREATOR_DIRECT_CODEX_ACTIVE_TURNS.get() else {
+ return;
+ };
+ let Ok(mut entries) = active_turns.lock() else {
+ return;
+ };
+ let cancellation = Arc::clone(&self.cancellation);
+ entries.unregister(&self.key, |current| Arc::ptr_eq(current, &cancellation));
+ }
+}
+
+/// 把一个 Direct 回合登记为"可终止",返回的 guard 在回合结束时注销它。
+fn register_active_direct_codex_turn(
+ key: std::path::PathBuf,
+ client_turn_id: &str,
+ cancellation: Arc,
+) -> DirectCodexActiveTurnGuard {
+ if let Ok(mut entries) = direct_codex_active_turns().lock() {
+ entries.register(key.clone(), client_turn_id, Arc::clone(&cancellation));
+ }
+ DirectCodexActiveTurnGuard { key, cancellation }
+}
+
+/// 已向正在跑的回合发出中断:界面等这一轮自己的收尾复位。
+pub(crate) const DIRECT_TURN_CANCEL_OUTCOME_INTERRUPTED: &str = "interrupted";
+/// 这一轮已经没有人替它收尾,本地守卫已被兜底释放:界面必须自己复位。
+pub(crate) const DIRECT_TURN_CANCEL_OUTCOME_RELEASED: &str = "released";
+
+/// `cancel_direct_codex_turn` 的返回值:界面据此决定是自己复位,还是等回合自己收尾。
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct DirectTurnCancelView {
+ /// [`DIRECT_TURN_CANCEL_OUTCOME_INTERRUPTED`] 或
+ /// [`DIRECT_TURN_CANCEL_OUTCOME_RELEASED`]。
+ pub(crate) outcome: String,
+ /// 给用户看的可读结果。
+ pub(crate) message: String,
+ /// 被终止 / 被释放的 clientTurnId。
+ pub(crate) client_turn_id: String,
+}
+
+/// "终止"这一步要作用在哪:发中断,还是走残留守卫兜底释放。
+enum DirectCodexTurnCancelTarget {
+ /// app-server 侧还有活句柄:正常发 `turn/interrupt`。
+ Interrupt(Arc),
+ /// app-server 侧已经拿不到可中断的活句柄;带上是哪种情况。
+ Stale(DirectTaonierStaleGuardReason),
+}
+
+/// 终止当前项目正在运行的 Direct 回合。
+///
+/// 正常路径:只向正在跑的 Codex app-server 回合发 `turn/interrupt`(app-server 随后回
+/// `turn/completed status=interrupted`,正在 await 的那个回合命令会带着可读原因返回),
+/// 不动任何既有事件或命令语义。
+///
+/// 兜底路径:app-server 侧已经拿不到可中断的活句柄时,说明这一轮不会再有人替它收尾。
+/// 只发中断会让本地守卫(`DirectTaonierActiveInvocationGuard`)永远留在进程内,用户此后
+/// 每条消息都会被"已有另一条回合正在运行"拒绝——这正是"重进会话被堵死"的死锁形态。
+/// 这时显式释放这条守卫并把可读原因返回给界面。释放条件见
+/// [`release_stale_direct_taonier_active_invocation`] 的注释;"正在跑的是另一轮"仍然
+/// 保持原拒绝语义,什么都不释放。
+pub(crate) fn cancel_direct_codex_turn_at(
+ root: &Path,
+ client_turn_id: Option<&str>,
+) -> Result {
+ let key = direct_codex_active_turn_key(root);
+ let expected = client_turn_id
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+ {
+ let entries = direct_codex_active_turns()
+ .lock()
+ .map_err(|_| "Direct 回合中断表已损坏,无法终止".to_string())?;
+ if let (Some(registered), Some(expected)) =
+ (entries.registered_client_turn_id(&key), expected)
+ {
+ if registered != expected {
+ return Err(DIRECT_CODEX_ANOTHER_TURN_RUNNING_MESSAGE.to_string());
+ }
+ }
+ }
+ let target = {
+ let entries = direct_codex_active_turns()
+ .lock()
+ .map_err(|_| "Direct 回合中断表已损坏,无法终止".to_string())?;
+ match entries.select(&key, client_turn_id) {
+ Ok((_, cancellation)) if cancellation.app_server_alive() => {
+ DirectCodexTurnCancelTarget::Interrupt(Arc::clone(cancellation))
+ }
+ Ok(_) => {
+ DirectCodexTurnCancelTarget::Stale(DirectTaonierStaleGuardReason::ExecutorExited)
+ }
+ Err(_) => DirectCodexTurnCancelTarget::Stale(
+ DirectTaonierStaleGuardReason::NeverReachedExecutor,
+ ),
+ }
+ };
+ match target {
+ DirectCodexTurnCancelTarget::Interrupt(cancellation) => {
+ cancellation.cancel();
+ Ok(DirectTurnCancelView {
+ outcome: DIRECT_TURN_CANCEL_OUTCOME_INTERRUPTED.to_string(),
+ message: "已向正在运行的回合发出终止".to_string(),
+ client_turn_id: client_turn_id
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .unwrap_or_default()
+ .to_string(),
+ })
+ }
+ DirectCodexTurnCancelTarget::Stale(reason) => {
+ let released =
+ release_stale_direct_taonier_active_invocation(root, client_turn_id, reason)?;
+ Ok(DirectTurnCancelView {
+ outcome: DIRECT_TURN_CANCEL_OUTCOME_RELEASED.to_string(),
+ message: format!(
+ "{},已释放这一轮的占用,可以直接重新发送消息",
+ reason.message()
+ ),
+ client_turn_id: released,
+ })
+ }
+ }
+}
+
struct CodexThreadLease {
connection: CodexAppServerConnection,
key: CodexNodeThreadKey,
@@ -3133,6 +3662,7 @@ fn parse_game_creator_codex_app_server_text(
} else {
String::new()
},
+ reasoning: String::new(),
finish_reason: Some("stop".to_string()),
response_id: Some(thread_id.to_string()),
usage: None,
@@ -3375,7 +3905,9 @@ async fn read_game_creator_codex_app_server_stdout(
| "item/completed"
| "rawResponseItem/completed"
| "turn/completed"
- ) && safe_activity.is_none()
+ ) && direct_codex_request_event_type(method).is_none()
+ && direct_codex_resolution_event_type(method).is_none()
+ && safe_activity.is_none()
&& intermediate_text.is_none()
{
continue;
@@ -3412,7 +3944,9 @@ async fn read_game_creator_codex_app_server_stdout(
continue;
}
}
- let event = if let Some(activity) = safe_activity {
+ let event = if let Some(event_type) = direct_codex_resolution_event_type(method) {
+ CodexTurnEvent::Request { event_type, params }
+ } else if let Some(activity) = safe_activity {
// Preparing notifications may carry private plan/reasoning text;
// expose only the safe activity category. Other categories may
// retain their bounded, redacted intermediate text below.
@@ -3461,6 +3995,13 @@ async fn read_game_creator_codex_app_server_stdout(
.cloned()
.unwrap_or(serde_json::Value::Null),
),
+ method if direct_codex_request_event_type(method).is_some() => {
+ CodexTurnEvent::Request {
+ event_type: direct_codex_request_event_type(method)
+ .expect("request event type checked above"),
+ params,
+ }
+ }
_ => CodexTurnEvent::Terminal(params),
}
};
@@ -3709,6 +4250,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at(
None,
None,
None,
+ None,
)
.await
}
@@ -3726,56 +4268,11 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_observer(
None,
Some(observer),
None,
+ None,
)
.await
}
-fn direct_codex_canonical_project_identity(
- root: &std::path::Path,
-) -> Result<(std::path::PathBuf, String), String> {
- let (canonical_root, _) = resolve_direct_codex_project_authority(root)?;
- let manifest = read_manifest(&canonical_root.join(".agent/manifest.json"))
- .map_err(|error| format!("读取 DirectProject 权威项目身份失败:{error}"))?;
- let manifest_project_id = manifest.project_id.trim();
- if manifest_project_id.is_empty() || manifest_project_id.chars().count() > 256 {
- return Err("DirectProject manifest.projectId 不满足身份边界".to_string());
- }
- let path_identity = direct_codex_os_path_identity_bytes(&canonical_root);
- Ok((
- canonical_root,
- direct_codex_project_identity_digest(&path_identity, manifest_project_id.as_bytes()),
- ))
-}
-
-fn direct_codex_os_path_identity_bytes(path: &std::path::Path) -> Vec {
- #[cfg(unix)]
- {
- use std::os::unix::ffi::OsStrExt;
- return path.as_os_str().as_bytes().to_vec();
- }
- #[cfg(windows)]
- {
- use std::os::windows::ffi::OsStrExt;
- let mut bytes = Vec::new();
- for unit in path.as_os_str().encode_wide() {
- bytes.extend_from_slice(&unit.to_le_bytes());
- }
- return bytes;
- }
- #[cfg(not(any(unix, windows)))]
- path.as_os_str().to_string_lossy().as_bytes().to_vec()
-}
-
-fn direct_codex_project_identity_digest(path_identity: &[u8], project_id: &[u8]) -> String {
- let mut digest = Sha256::new();
- digest.update(b"genarrative-direct-project-identity.v1\0");
- digest.update((path_identity.len() as u64).to_le_bytes());
- digest.update(path_identity);
- digest.update((project_id.len() as u64).to_le_bytes());
- digest.update(project_id);
- format!("{:x}", digest.finalize())
-}
-
pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
root: &std::path::Path,
system_prompt: String,
@@ -3783,6 +4280,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
client_turn_id: Option<&str>,
observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
audit: Option<&mut DirectCodexTurnAudit>,
+ direct_user_item: Option,
) -> Result {
// Resolve project authority before deriving the pool/thread identity. A
// caller may hold a stable symlink path whose target changes between
@@ -3846,6 +4344,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
request,
Some(&codex_root),
effective_client_turn_id,
+ direct_user_item.as_ref(),
None,
observer,
audit,
@@ -3955,6 +4454,68 @@ pub(crate) fn build_direct_codex_history_prompt(
mod tests {
use super::*;
+ /// 终止只作用在"当前项目正在跑的那一轮"上:没有活动回合 / clientTurnId 不匹配都要
+ /// 返回可读原因,不能误伤别人;注销也只注销本回合自己的句柄。
+ #[test]
+ fn direct_codex_active_turn_table_selects_only_the_running_turn() {
+ let mut table: DirectCodexActiveTurnTable = DirectCodexActiveTurnTable::new();
+ let key = std::path::PathBuf::from("C:/projects/direct-turn-demo");
+ assert_eq!(
+ table.select(&key, None).expect_err("no active turn"),
+ "当前项目没有正在运行的陶泥儿回合,无法终止"
+ );
+
+ table.register(key.clone(), "turn-a", 1);
+ assert_eq!(table.select(&key, None).expect("active turn").0, "turn-a");
+ assert_eq!(table.select(&key, Some("turn-a")).expect("same turn").1, 1);
+ assert_eq!(
+ table
+ .select(&key, Some("turn-b"))
+ .expect_err("another running turn"),
+ "正在运行的是另一个陶泥儿回合,已拒绝终止"
+ );
+
+ // 句柄已被后来的回合替换:旧回合收尾不得注销新回合。
+ table.register(key.clone(), "turn-b", 2);
+ table.unregister(&key, |value| *value == 1);
+ assert_eq!(table.select(&key, None).expect("newer turn").0, "turn-b");
+ table.unregister(&key, |value| *value == 2);
+ assert!(table.select(&key, None).is_err());
+ }
+
+ /// 注册键:前端传的项目路径与回合注册时的路径必须归一化成同一个键(Windows 上
+ /// `canonicalize` 会带 `\\?\` 前缀,去掉后两边才相等)。
+ #[test]
+ fn direct_codex_active_turn_key_normalizes_windows_prefix() {
+ let root = tempfile::tempdir().expect("temp dir");
+ let canonical = std::fs::canonicalize(root.path()).expect("canonical root");
+ let expected = canonical
+ .to_str()
+ .and_then(|value| value.strip_prefix("\\\\?\\"))
+ .map(std::path::PathBuf::from)
+ .unwrap_or(canonical);
+ let key = direct_codex_active_turn_key(root.path());
+ assert_eq!(key, expected);
+ // 归一化后的键不再带 Windows 扩展长度前缀:前端传进来的普通路径才能命中同一个键。
+ assert!(!key.to_string_lossy().starts_with("\\\\?\\"));
+ }
+
+ #[test]
+ fn direct_thread_item_projection_drops_full_app_server_payload() {
+ let item = serde_json::json!({
+ "id": "item-1",
+ "type": "mcpToolCall",
+ "tool": "agc_write_file",
+ "arguments": { "path": "game/index.html", "token": "secret" },
+ "result": { "content": "large output" }
+ });
+ assert_eq!(direct_thread_item_id(&item).as_deref(), Some("item-1"));
+ assert_eq!(
+ direct_thread_item_started_payload(&item),
+ serde_json::json!({ "itemType": "mcpToolCall" })
+ );
+ }
+
#[test]
fn direct_item_activities_are_closed_safe_categories() {
let allowed = [
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs
index 8af1c9d2d..863b8cdad 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs
@@ -6,8 +6,9 @@ use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
-const GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH: &str = "codex/win-x64/bin/codex.exe";
-const GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH: &str = "codex/win-x64/manifest.json";
+const GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH: &str = "coding-agent/win-x64/bin/codex.exe";
+const GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH: &str =
+ "coding-agent/win-x64/manifest.json";
const GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES: [&str; 6] = [
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
@@ -589,6 +590,7 @@ fn parse_game_creator_codex_cli_response(
} else {
String::new()
},
+ reasoning: String::new(),
finish_reason: Some("stop".to_string()),
response_id,
usage,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs
index 3cd44b4bd..ebe6152b8 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs
@@ -49,6 +49,18 @@ pub(crate) struct DesignView {
messages: Vec,
running: bool,
can_retry: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ reasoning_text: Option,
+ reasoning_entries: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct DesignReasoningEntry {
+ id: String,
+ text: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ message_id: Option,
}
#[derive(Clone, Debug, Serialize)]
@@ -65,6 +77,7 @@ pub(crate) struct DesignEvent {
}
fn design_view(session: &DesignSession, running: bool) -> DesignView {
+ let reasoning_entries = persisted_design_reasoning_entries(session);
DesignView {
session: DesignSessionSummary {
session_id: session.session_id.clone(),
@@ -82,9 +95,136 @@ fn design_view(session: &DesignSession, running: bool) -> DesignView {
&& session.turn.as_ref().is_some_and(|turn| turn.pending)
&& session.pending_approval.is_none()
&& session.pending_clarification.is_none(),
+ reasoning_text: reasoning_entries.last().map(|entry| entry.text.clone()),
+ reasoning_entries,
}
}
+fn reasoning_text_from_history_item(item: &Value) -> Option {
+ if item.get("type").and_then(Value::as_str) != Some("reasoning") {
+ return None;
+ }
+ let mut text = String::new();
+ if let Some(summary) = item.get("summary").and_then(Value::as_array) {
+ for part in summary {
+ if let Some(value) = part.get("text").and_then(Value::as_str) {
+ text.push_str(value.trim());
+ }
+ }
+ }
+ if let Some(content) = item.get("content").and_then(Value::as_array) {
+ for part in content {
+ let part_type = part.get("type").and_then(Value::as_str).unwrap_or_default();
+ if matches!(
+ part_type,
+ "reasoning" | "reasoning_content" | "reasoning_text" | "analysis" | "thinking"
+ ) {
+ if let Some(value) = part.get("text").and_then(Value::as_str) {
+ text.push_str(value.trim());
+ }
+ }
+ }
+ }
+ (!text.trim().is_empty()).then_some(text)
+}
+
+fn persisted_design_reasoning_entries(session: &DesignSession) -> Vec {
+ // Responses history contains tool-only provider responses. Their reasoning is
+ // followed by function calls and only the next provider response may contain
+ // visible assistant text, so pairing on the next `message` item makes the
+ // earlier reasoning look like an orphan and moves it to the bottom of the UI.
+ // Both persisted streams retain user-turn boundaries; pair reasoning and
+ // visible assistant messages by their response order within each turn.
+ let mut assistant_groups: Vec> = vec![Vec::new()];
+ for message in &session.messages {
+ if message.role == "user" {
+ assistant_groups.push(Vec::new());
+ } else if message.role == "assistant" {
+ assistant_groups
+ .last_mut()
+ .expect("assistant group always exists")
+ .push(message.id.clone());
+ }
+ }
+ let mut entries = Vec::new();
+ let mut group_index = 0;
+ let mut assistant_index = 0;
+ let mut sequence = 0_u64;
+ let mut current_reasoning: Vec = Vec::new();
+ let mut pending_reasoning: Vec = Vec::new();
+ let mut saw_response_output = false;
+
+ for item in &session.history {
+ if item.get("role").and_then(Value::as_str) == Some("user") {
+ if !pending_reasoning.is_empty() || !current_reasoning.is_empty() {
+ pending_reasoning.append(&mut current_reasoning);
+ // A user item closes the previous turn. Resolve its reasoning
+ // against that turn's last assistant message before moving to
+ // the next group; otherwise it is incorrectly attached to the
+ // next turn and rendered at the bottom as an orphan.
+ let assistant_id = assistant_groups
+ .get(group_index)
+ .and_then(|ids| ids.last())
+ .cloned();
+ for mut entry in pending_reasoning.drain(..) {
+ entry.message_id = assistant_id.clone();
+ entries.push(entry);
+ }
+ }
+ group_index += 1;
+ assistant_index = 0;
+ saw_response_output = false;
+ continue;
+ }
+ if item.get("type").and_then(Value::as_str) == Some("reasoning") {
+ if saw_response_output {
+ pending_reasoning.append(&mut current_reasoning);
+ saw_response_output = false;
+ }
+ if let Some(text) = reasoning_text_from_history_item(item) {
+ sequence += 1;
+ current_reasoning.push(DesignReasoningEntry {
+ id: item
+ .get("id")
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ .unwrap_or_else(|| format!("reasoning-{sequence}")),
+ text,
+ message_id: None,
+ });
+ }
+ continue;
+ }
+ if item.get("role").and_then(Value::as_str) == Some("assistant")
+ || item.get("type").and_then(Value::as_str) == Some("message")
+ {
+ pending_reasoning.extend(current_reasoning.drain(..));
+ let assistant_id = assistant_groups
+ .get(group_index)
+ .and_then(|ids| ids.get(assistant_index))
+ .cloned();
+ assistant_index += 1;
+ for mut entry in pending_reasoning.drain(..) {
+ entry.message_id = assistant_id.clone();
+ entries.push(entry);
+ }
+ saw_response_output = false;
+ } else if item.get("type").is_some() {
+ saw_response_output = true;
+ }
+ }
+ pending_reasoning.append(&mut current_reasoning);
+ let fallback_id = assistant_groups
+ .get(group_index)
+ .and_then(|ids| ids.last())
+ .cloned();
+ for mut entry in pending_reasoning {
+ entry.message_id = fallback_id.clone();
+ entries.push(entry);
+ }
+ entries
+}
+
fn design_event(
root: &Path,
turn_id: &str,
@@ -104,6 +244,17 @@ fn design_event(
}
}
+fn design_reasoning_event(
+ root: &Path,
+ turn_id: &str,
+ id: Option<&str>,
+ reasoning: String,
+) -> DesignEvent {
+ let mut event = design_event(root, turn_id, "reasoning", id, None, None);
+ event.reasoning_text = Some(reasoning);
+ event
+}
+
fn design_project_id(root: &Path) -> Result {
validate_project_root(root)?;
Ok(read_existing_manifest_for_project(root)?.project_id)
@@ -462,15 +613,12 @@ fn build_design_request(
.with_tool_choice(platform_llm::LlmToolChoice::Auto)
.with_web_search(false);
apply_game_creator_llm_reasoning_effort(request, llm)
+ .map(|request| request.with_reasoning_capture(true))
}
// 调试队列只接收副本,写盘慢或失败时丢弃,不参与会话恢复。
fn design_debug(root: &Path, kind: &str, data: Value) {
- if std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG")
- .ok()
- .as_deref()
- != Some("1")
- {
+ if !design_debug_enabled() {
return;
}
type Entry = (PathBuf, Value);
@@ -548,8 +696,15 @@ async fn request_design_provider(
Some(String::new()),
None,
));
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ String::new(),
+ ));
let result = if llm.stream {
let mut stream_sequence = 0_u64;
+ let mut emitted_reasoning = String::new();
client
.stream_run(request.clone(), |delta| {
stream_sequence = stream_sequence.saturating_add(1);
@@ -565,18 +720,33 @@ async fn request_design_provider(
"model": llm.model,
"deltaChars": delta.delta_text.chars().count(),
"accumulatedChars": delta.accumulated_text.chars().count(),
+ "reasoningDeltaChars": delta.reasoning_delta.chars().count(),
+ "reasoningAccumulatedChars": delta.accumulated_reasoning.chars().count(),
"deltaText": delta.delta_text,
"finishReason": delta.finish_reason,
}),
);
- emit(design_event(
- root,
- &turn_id,
- "text",
- Some(&message_id),
- Some(delta.accumulated_text.clone()),
- None,
- ));
+ if !delta.delta_text.is_empty() || delta.finish_reason.is_some() {
+ emit(design_event(
+ root,
+ &turn_id,
+ "text",
+ Some(&message_id),
+ Some(delta.accumulated_text.clone()),
+ None,
+ ));
+ }
+ if !delta.reasoning_delta.is_empty()
+ || delta.accumulated_reasoning != emitted_reasoning
+ {
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ delta.accumulated_reasoning.clone(),
+ ));
+ emitted_reasoning = delta.accumulated_reasoning.clone();
+ }
})
.await
} else {
@@ -584,6 +754,14 @@ async fn request_design_provider(
};
match result {
Ok(response) => {
+ if !response.reasoning.is_empty() {
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ response.reasoning.clone(),
+ ));
+ }
design_debug(
root,
"response",
@@ -606,6 +784,12 @@ async fn request_design_provider(
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
.is_none()
{
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ String::new(),
+ ));
return Err(detail);
}
tokio::time::sleep(Duration::from_millis(
@@ -654,8 +838,24 @@ async fn request_scripted_design_provider(
Some(String::new()),
None,
));
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ String::new(),
+ ));
match fake_provider::take() {
- Some(Ok(response)) => return Ok(response),
+ Some(Ok(response)) => {
+ if !response.reasoning.is_empty() {
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ response.reasoning.clone(),
+ ));
+ }
+ return Ok(response);
+ }
Some(Err(error)) => {
let detail = redact_agent_runtime_error(
root,
@@ -666,10 +866,24 @@ async fn request_scripted_design_provider(
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
.is_none()
{
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ String::new(),
+ ));
return Err(detail);
}
}
- None => return Err("假 Provider 脚本耗尽".into()),
+ None => {
+ emit(design_reasoning_event(
+ root,
+ &turn_id,
+ Some(&message_id),
+ String::new(),
+ ));
+ return Err("假 Provider 脚本耗尽".into());
+ }
}
}
unreachable!()
@@ -927,6 +1141,18 @@ fn resolve_design_runtime_mode(root: &Path) -> Result