Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7edc9d0ee | |||
| f60771dd00 | |||
| f7cccd8592 | |||
| 5ee2d940c1 | |||
| c75a7725ce | |||
| 3a95715990 | |||
| ed86963de2 | |||
| 65e73eee02 | |||
| 6068eeda83 | |||
| 0e244c64ea | |||
| ad7c059603 | |||
| 55d2a393d1 | |||
| e7f66ce950 | |||
| 35dcfea3ef | |||
| cea8bf77fa | |||
| 0136bfe564 | |||
| 22785b4d7b | |||
| a86a2ee4d2 | |||
| b22eae2726 | |||
| 35d0db6377 | |||
| f087ba3e2b | |||
| 7fc1b841be | |||
| a576fdcc37 | |||
| 4dcf38715b | |||
| d1f9f4e1ef | |||
| 5836aaec26 | |||
| 18d60feb04 | |||
| 4e1316e272 | |||
| 302b4addae |
@@ -232,37 +232,6 @@ jobs:
|
||||
done
|
||||
done
|
||||
|
||||
- name: Prepare standalone Rust crate dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
|
||||
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
|
||||
# 而 check:native-shells 会经 agent-runtime-*:check 用 `cargo test --manifest-path`
|
||||
# 单独跑这两个 crate。不在这里预热的话,这两条测试会在测试阶段自己
|
||||
# `Updating crates.io index`,crates.io 一抖动整条 native shell 作业就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path "${manifest_path}"; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
done
|
||||
|
||||
- name: Run native shell gates
|
||||
run: npm run check:native-shells
|
||||
|
||||
|
||||
@@ -16,5 +16,8 @@
|
||||
"maxRetries": 2,
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {}
|
||||
"agentLlm": {},
|
||||
"planning": {
|
||||
"capabilityEnabled": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"config": "node scripts/game-creator-config-wizard.mjs",
|
||||
"test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open",
|
||||
"test:chat:manual": "node scripts/agent-swarm-test-chat.mjs",
|
||||
"test:plan": "node scripts/agent-swarm-test-chat.mjs --plan --task \"我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。\"",
|
||||
"test:plan:manual": "node scripts/agent-swarm-test-chat.mjs --plan",
|
||||
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
|
||||
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
|
||||
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
|
||||
|
||||
@@ -36,6 +36,8 @@ export const ungeneratedGameEntryMarker =
|
||||
'还没有生成游戏。回到聊天输入创意并确认生成后';
|
||||
export const defaultRealSwarmTestTask =
|
||||
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
|
||||
export const defaultRealSwarmPlanTask =
|
||||
'我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
|
||||
export const swarmTurnReportPrefix = '[turn.report] ';
|
||||
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
|
||||
|
||||
@@ -138,10 +140,15 @@ export const usage = `用法:
|
||||
--keep-project 保留自动创建的一次性项目
|
||||
--no-open 手工模式启动预览但不自动打开浏览器
|
||||
--task <需求> 通过 manual 入口非交互提交自定义需求
|
||||
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时
|
||||
--plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
|
||||
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
|
||||
--dry-run 只检查目录发现和项目准备,不启动 LLM
|
||||
-h, --help 显示帮助
|
||||
`;
|
||||
|
||||
环境变量:
|
||||
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approve;revise/reject 必须
|
||||
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
|
||||
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
|
||||
|
||||
function readOptionValue(args, index, option) {
|
||||
const value = args[index + 1]?.trim();
|
||||
@@ -170,6 +177,7 @@ export function parseSwarmTestArguments(args) {
|
||||
keepProject: false,
|
||||
openBrowser: true,
|
||||
task: null,
|
||||
plan: false,
|
||||
timeoutMinutes: null,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
@@ -194,6 +202,8 @@ export function parseSwarmTestArguments(args) {
|
||||
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
|
||||
options.task = task;
|
||||
index += 1;
|
||||
} else if (argument === '--plan') {
|
||||
options.plan = true;
|
||||
} else if (argument === '--timeout-minutes') {
|
||||
if (options.timeoutMinutes !== null) {
|
||||
throw new Error('--timeout-minutes 只能指定一次');
|
||||
@@ -212,11 +222,16 @@ export function parseSwarmTestArguments(args) {
|
||||
}
|
||||
|
||||
export function shouldStartPersistentPreview(options) {
|
||||
return !options.task;
|
||||
// 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
|
||||
return !options.task && !options.plan;
|
||||
}
|
||||
|
||||
export function resolveSwarmTestTimeoutMs(options) {
|
||||
const minutes = options.timeoutMinutes ?? (options.task ? 50 : null);
|
||||
// 立项策划的设计目标是五分钟出方案,给一分钟余量;再久就是卡住了,早失败
|
||||
// 比让 harness 空等更有用。做游戏那条链路的 50 分钟不变。
|
||||
const planMinutes = options.plan ? 6 : null;
|
||||
const minutes =
|
||||
options.timeoutMinutes ?? planMinutes ?? (options.task ? 50 : null);
|
||||
return minutes === null ? null : minutes * 60_000;
|
||||
}
|
||||
|
||||
@@ -371,15 +386,7 @@ async function runMissingConfigWizard(setActiveChild, explicitConfigDir) {
|
||||
}
|
||||
}
|
||||
|
||||
async function secureWindowsPrivateRuntimePath(
|
||||
targetPath,
|
||||
options,
|
||||
secureWindowsPath = null,
|
||||
) {
|
||||
if (secureWindowsPath) {
|
||||
await secureWindowsPath(targetPath, options);
|
||||
return;
|
||||
}
|
||||
async function secureWindowsPrivateRuntimePath(targetPath, options) {
|
||||
const { secureWindowsGameCreatorPathForCurrentUser } = await import(
|
||||
'./game-creator-config-wizard.mjs'
|
||||
);
|
||||
@@ -391,7 +398,6 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
runtimeConfigDir,
|
||||
fileName,
|
||||
required,
|
||||
secureWindowsPath = null,
|
||||
) {
|
||||
const sourcePath = path.join(sourceConfigDir, fileName);
|
||||
const sourceMetadata = await lstat(sourcePath).catch((error) => {
|
||||
@@ -411,11 +417,9 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
const sourceBytes = await readFile(sourcePath);
|
||||
const destinationFile = await open(destinationPath, 'wx', 0o600);
|
||||
try {
|
||||
await secureWindowsPrivateRuntimePath(
|
||||
destinationPath,
|
||||
{ isDirectory: false },
|
||||
secureWindowsPath,
|
||||
);
|
||||
await secureWindowsPrivateRuntimePath(destinationPath, {
|
||||
isDirectory: false,
|
||||
});
|
||||
await destinationFile.writeFile(sourceBytes);
|
||||
await destinationFile.sync();
|
||||
} finally {
|
||||
@@ -448,11 +452,7 @@ async function copyPrivateRuntimeConfigEntry(
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function prepareSwarmTestRuntimeConfig(
|
||||
sourceConfigDir,
|
||||
tempRoot,
|
||||
{ secureWindowsPath = null } = {},
|
||||
) {
|
||||
export async function prepareSwarmTestRuntimeConfig(sourceConfigDir, tempRoot) {
|
||||
if (!path.isAbsolute(sourceConfigDir)) {
|
||||
throw new Error('配置来源目录必须是绝对路径');
|
||||
}
|
||||
@@ -472,11 +472,9 @@ export async function prepareSwarmTestRuntimeConfig(
|
||||
);
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await secureWindowsPrivateRuntimePath(
|
||||
runtimeConfigDir,
|
||||
{ isDirectory: true },
|
||||
secureWindowsPath,
|
||||
);
|
||||
await secureWindowsPrivateRuntimePath(runtimeConfigDir, {
|
||||
isDirectory: true,
|
||||
});
|
||||
} else {
|
||||
await chmod(runtimeConfigDir, 0o700);
|
||||
}
|
||||
@@ -494,14 +492,12 @@ export async function prepareSwarmTestRuntimeConfig(
|
||||
runtimeConfigDir,
|
||||
configFileName,
|
||||
true,
|
||||
secureWindowsPath,
|
||||
);
|
||||
await copyPrivateRuntimeConfigEntry(
|
||||
canonicalSourceConfigDir,
|
||||
runtimeConfigDir,
|
||||
localConfigFileName,
|
||||
false,
|
||||
secureWindowsPath,
|
||||
);
|
||||
return {
|
||||
path: await realpath(runtimeConfigDir),
|
||||
@@ -983,12 +979,25 @@ export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
|
||||
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
|
||||
}
|
||||
|
||||
// GDD 审批位不能等 CLI 退出之后再处理:Run 停在这里时状态是 waiting-for-user-input,
|
||||
// 而 swarm CLI 恰好把这个状态算作「本轮还在跑」,turn 永远不 settle,CLI 也就永远
|
||||
// 不退出。所以审批必须在 CLI 还活着的时候并发做完,让 Run 自己继续跑到收束。
|
||||
// 这一句是 PlanGddCompletionBlockerKind::AwaitingApprovalDecision 专有的投影文案,
|
||||
// 另外三个 blocked 子状态都不会打出它;即便认错了,真正的判据也是随后那次
|
||||
// --plan-gdd-status,没有待决定审批时不会有任何写入。
|
||||
const planGddApprovalWaitPattern = /等待 Fast GDD 审批决定/u;
|
||||
|
||||
export function swarmOutputAwaitsPlanGddApproval(line) {
|
||||
return planGddApprovalWaitPattern.test(line);
|
||||
}
|
||||
|
||||
async function runTaskCargo(
|
||||
cliArguments,
|
||||
task,
|
||||
setActiveChild,
|
||||
timeoutMs,
|
||||
autoPilot = false,
|
||||
onPlanGddApprovalWait = null,
|
||||
) {
|
||||
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
@@ -1000,6 +1009,25 @@ async function runTaskCargo(
|
||||
let taskSubmitted = false;
|
||||
let promptsSeen = 0;
|
||||
let sittingAtPrompt = false;
|
||||
let planGddApproval = null;
|
||||
let planGddApprovalError = null;
|
||||
let planGddApprovalStarted = false;
|
||||
let planGddApprovalPromise = null;
|
||||
const startPlanGddApproval = () => {
|
||||
planGddApprovalStarted = true;
|
||||
console.log(
|
||||
`[自动审批] 检测到 Fast GDD 审批位,正在提交 ${resolvePlanGddAutoDecision().action}`,
|
||||
);
|
||||
planGddApprovalPromise = onPlanGddApprovalWait()
|
||||
.then((value) => {
|
||||
planGddApproval = value;
|
||||
})
|
||||
.catch((error) => {
|
||||
planGddApprovalError = error;
|
||||
// 审批没成的话 Run 会一直停在等待位,干等到超时只会把真正的原因埋掉。
|
||||
void terminateChildTree(child).catch(() => {});
|
||||
});
|
||||
};
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
process.stdout.write(chunk);
|
||||
@@ -1012,6 +1040,13 @@ async function runTaskCargo(
|
||||
reportLines.push(normalizedLine);
|
||||
settled = true;
|
||||
}
|
||||
if (
|
||||
onPlanGddApprovalWait &&
|
||||
!planGddApprovalStarted &&
|
||||
swarmOutputAwaitsPlanGddApproval(normalizedLine)
|
||||
) {
|
||||
startPlanGddApproval();
|
||||
}
|
||||
}
|
||||
if (!autoPilot || child.stdin.writableEnded) return;
|
||||
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
|
||||
@@ -1050,9 +1085,12 @@ async function runTaskCargo(
|
||||
if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) {
|
||||
reportLines.push(normalizedPendingLine);
|
||||
}
|
||||
await planGddApprovalPromise;
|
||||
if (planGddApprovalError) throw planGddApprovalError;
|
||||
return {
|
||||
...result,
|
||||
turnReportOutput: reportLines.join('\n'),
|
||||
planGddApproval,
|
||||
};
|
||||
} finally {
|
||||
setActiveChild(null);
|
||||
@@ -1742,6 +1780,196 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
|
||||
return inspection;
|
||||
}
|
||||
|
||||
// 这四条路径的权威定义都在 Rust 侧 `planning_storage.rs`(`PLAN_SESSION_PATH`、
|
||||
// `PLAN_GDD_INDEX_PATH`、`PLAN_STORAGE_ROOT`、`PLAN_FAST_GDD_PATH`)。跨语言没有共享
|
||||
// 常量的通道,改路径时要连同 `GddApprovalCard.tsx` 一起动。
|
||||
export const planningOutputPaths = [
|
||||
'.agent/planning/session.json',
|
||||
'.agent/planning/index.json',
|
||||
'.agent/planning/pending.json',
|
||||
'game/fast_gdd.md',
|
||||
];
|
||||
|
||||
export async function inspectPlanningOutputs(projectPath) {
|
||||
const outputs = [];
|
||||
for (const relativePath of planningOutputPaths) {
|
||||
const absolutePath = path.join(projectPath, ...relativePath.split('/'));
|
||||
const metadata = await lstat(absolutePath).catch((error) => {
|
||||
if (error?.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
outputs.push({
|
||||
path: relativePath,
|
||||
exists: Boolean(metadata?.isFile()),
|
||||
bytes: metadata?.isFile() ? metadata.size : 0,
|
||||
});
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
async function reportPlanningOutputs(projectPath) {
|
||||
const outputs = await inspectPlanningOutputs(projectPath);
|
||||
console.log('\n立项策划产物:');
|
||||
for (const output of outputs) {
|
||||
console.log(
|
||||
output.exists
|
||||
? ` [有] ${output.path}(${output.bytes} 字节)`
|
||||
: ` [无] ${output.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const planGddApprovalTimeoutMs = 60_000;
|
||||
export const planGddStatusOutputPrefix = 'planGddStateJson=';
|
||||
export const planGddDecisionOutputPrefix = 'planGddDecisionJson=';
|
||||
|
||||
function parsePrefixedJsonLine(output, prefix, label) {
|
||||
const line = output
|
||||
.split('\n')
|
||||
.map((value) => (value.endsWith('\r') ? value.slice(0, -1) : value))
|
||||
.find((value) => value.startsWith(prefix));
|
||||
if (!line) throw new Error(`${label}缺少 ${prefix} 输出`);
|
||||
try {
|
||||
return JSON.parse(line.slice(prefix.length));
|
||||
} catch (error) {
|
||||
throw new Error(`解析${label}失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePlanGddStatusOutput(output) {
|
||||
return parsePrefixedJsonLine(
|
||||
output,
|
||||
planGddStatusOutputPrefix,
|
||||
'Fast GDD 审批状态',
|
||||
);
|
||||
}
|
||||
|
||||
export function parsePlanGddDecisionOutput(output) {
|
||||
return parsePrefixedJsonLine(
|
||||
output,
|
||||
planGddDecisionOutputPrefix,
|
||||
'Fast GDD 审批回执',
|
||||
);
|
||||
}
|
||||
|
||||
// 审批卡是这条链路唯一的人类判据,所以自动应答默认只投 approve,且只在投影确实有
|
||||
// 一张待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把
|
||||
// 判据换成噪声——所以那两条分支只在跑的人自己用 AGC_PLAN_GDD_COMMENT 给出意见时
|
||||
// 才走。手工调 --plan-gdd-decide 也能达到同样效果,但那要求 plan 根 run 仍然活着,
|
||||
// 而它恰好是本进程持有的 CLI 子进程。
|
||||
export function planGddAutoApprovalIsPending(state) {
|
||||
return Boolean(state?.pendingApproval);
|
||||
}
|
||||
|
||||
export function resolvePlanGddAutoDecision(env = process.env) {
|
||||
const action = (env.AGC_PLAN_GDD_DECISION ?? 'approve').trim();
|
||||
if (!['approve', 'revise', 'reject'].includes(action)) {
|
||||
throw new Error('AGC_PLAN_GDD_DECISION 只能是 approve / revise / reject');
|
||||
}
|
||||
const comment = (env.AGC_PLAN_GDD_COMMENT ?? '').trim();
|
||||
if (action === 'approve') return { action, comment: null };
|
||||
if (!comment) {
|
||||
throw new Error(
|
||||
`${action} 必须同时设 AGC_PLAN_GDD_COMMENT 提供真实修改意见`,
|
||||
);
|
||||
}
|
||||
return { action, comment };
|
||||
}
|
||||
|
||||
async function settlePlanGddApproval(
|
||||
projectPath,
|
||||
runtimeConfigPath,
|
||||
setActiveChild,
|
||||
) {
|
||||
const readStatus = async () => {
|
||||
const result = await runCapturedCargo(
|
||||
['--config-dir', runtimeConfigPath, '--plan-gdd-status', projectPath],
|
||||
setActiveChild,
|
||||
{
|
||||
timeoutMs: planGddApprovalTimeoutMs,
|
||||
label: 'Fast GDD 审批状态查询',
|
||||
},
|
||||
);
|
||||
if (result.code !== 0 || result.signal) {
|
||||
throw new Error(
|
||||
`读取 Fast GDD 审批状态失败:${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return parsePlanGddStatusOutput(result.stdout);
|
||||
};
|
||||
|
||||
const before = await readStatus();
|
||||
if (!planGddAutoApprovalIsPending(before)) {
|
||||
return { decided: false, state: before };
|
||||
}
|
||||
const { action, comment } = resolvePlanGddAutoDecision();
|
||||
const decision = await runCapturedCargo(
|
||||
[
|
||||
'--config-dir',
|
||||
runtimeConfigPath,
|
||||
'--plan-gdd-decide',
|
||||
projectPath,
|
||||
action,
|
||||
...(comment === null ? [] : ['--stdin']),
|
||||
],
|
||||
setActiveChild,
|
||||
{
|
||||
timeoutMs: planGddApprovalTimeoutMs,
|
||||
label: 'Fast GDD 审批决定',
|
||||
stdin: comment,
|
||||
},
|
||||
);
|
||||
if (decision.code !== 0 || decision.signal) {
|
||||
throw new Error(
|
||||
`提交 Fast GDD 审批决定失败:${decision.stderr.trim() || decision.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
const receipt = parsePlanGddDecisionOutput(decision.stdout);
|
||||
// 回执落盘和唤醒后台任务是两件事:decide 命令把唤醒失败降级成 recoveryPending,
|
||||
// 于是审批已经生效、Run 却仍停在 waiting-for-user-input。实测就是这样——只有
|
||||
// 补一次 --agent-resume 才会重新起 turn。这是仓库自己给这个状态定义的恢复动作。
|
||||
let recovered = false;
|
||||
if (receipt.recoveryPending) {
|
||||
const resume = await runCapturedCargo(
|
||||
['--config-dir', runtimeConfigPath, '--agent-resume', projectPath],
|
||||
setActiveChild,
|
||||
{
|
||||
timeoutMs: planGddApprovalTimeoutMs,
|
||||
label: 'Fast GDD 审批后恢复后台任务',
|
||||
},
|
||||
);
|
||||
if (resume.code !== 0 || resume.signal) {
|
||||
throw new Error(
|
||||
`审批已提交但恢复后台任务失败:${resume.stderr.trim() || resume.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
recovered = true;
|
||||
}
|
||||
return { decided: true, receipt, recovered, state: await readStatus() };
|
||||
}
|
||||
|
||||
async function reportPlanGddApproval(approval) {
|
||||
const { state } = approval;
|
||||
console.log('\nFast GDD 审批:');
|
||||
if (!approval.decided) {
|
||||
console.log(` [无待决定审批] 当前投影状态=${state.state}`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
` [已决定 ${approval.receipt.decisionRef.action}] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
|
||||
);
|
||||
if (approval.recovered) {
|
||||
console.log(
|
||||
' [已恢复] 审批回执的 recoveryPending 由一次 --agent-resume 收口',
|
||||
);
|
||||
}
|
||||
if (state.session) {
|
||||
console.log(
|
||||
` 澄清轮次=${state.session.clarificationRound} 返工深度=${state.session.repairDepth} phase=${state.session.phase}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasConfiguredEditorApiKey(configDir) {
|
||||
let configured = false;
|
||||
for (const fileName of [configFileName, localConfigFileName]) {
|
||||
@@ -1894,7 +2122,13 @@ export async function runSwarmTestChat(options) {
|
||||
const setActiveChild = (child) => {
|
||||
activeChild = child;
|
||||
};
|
||||
// GDD 审批要和 swarm CLI 并发跑,两者不能共用 activeChild 这一个槽位:审批子进程
|
||||
// 结束时的 setActiveChild(null) 会把 CLI 从槽里抹掉,Ctrl-C 就杀不到它了。
|
||||
const concurrentChildren = new Set();
|
||||
const setConcurrentChild = (child) => {
|
||||
if (child) concurrentChildren.add(child);
|
||||
else concurrentChildren.clear();
|
||||
};
|
||||
const stopRequested = () => receivedSignal !== null;
|
||||
const handleSignal = (signal) => {
|
||||
const repeatedSignal = receivedSignal !== null;
|
||||
@@ -1972,10 +2206,11 @@ export async function runSwarmTestChat(options) {
|
||||
);
|
||||
}
|
||||
console.log('LLM 配置已就绪。');
|
||||
const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
|
||||
console.log(
|
||||
options.task
|
||||
? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n'
|
||||
: '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n',
|
||||
? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
|
||||
: `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
|
||||
);
|
||||
|
||||
phase = 'chat';
|
||||
@@ -1985,7 +2220,8 @@ export async function runSwarmTestChat(options) {
|
||||
runtimeConfig.path,
|
||||
'--swarm-chat',
|
||||
'--init',
|
||||
'--autonomous-game-build',
|
||||
// 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
|
||||
options.plan ? '--plan' : '--autonomous-game-build',
|
||||
project.path,
|
||||
];
|
||||
let chat;
|
||||
@@ -1998,6 +2234,15 @@ export async function runSwarmTestChat(options) {
|
||||
timeoutDeadline === null
|
||||
? null
|
||||
: Math.max(1, timeoutDeadline - Date.now()),
|
||||
options.plan,
|
||||
options.plan
|
||||
? () =>
|
||||
settlePlanGddApproval(
|
||||
project.path,
|
||||
runtimeConfig.path,
|
||||
setConcurrentChild,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: await runInteractiveCargo(chatArguments, setActiveChild);
|
||||
} catch (error) {
|
||||
@@ -2013,6 +2258,32 @@ export async function runSwarmTestChat(options) {
|
||||
if (options.task) {
|
||||
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput);
|
||||
}
|
||||
if (options.plan) {
|
||||
// 立项策划不出游戏产物,正式验收在 GDD 审批卡上;这里只报告落盘情况,
|
||||
// 是否收束已经由 CLI 的退出码判过了。
|
||||
// 自动任务档的审批已经在 CLI 运行期间并发做完了;手工档(人自己敲 Ctrl+D
|
||||
// 退出)没有那次触发,退出后补一次,没有待决定审批时它是只读的。
|
||||
phase = 'plan-approval';
|
||||
const approval =
|
||||
chat.planGddApproval ??
|
||||
(await settlePlanGddApproval(
|
||||
project.path,
|
||||
runtimeConfig.path,
|
||||
setConcurrentChild,
|
||||
));
|
||||
if (receivedSignal) break session;
|
||||
phase = 'plan-report';
|
||||
await reportPlanGddApproval(approval);
|
||||
await reportPlanningOutputs(project.path);
|
||||
phase = 'complete';
|
||||
console.log(
|
||||
approval.decided
|
||||
? '\n立项策划链路已收束:Fast GDD 已批准,策划产物见上方清单。'
|
||||
: '\n立项策划链路已收束:Run 正常结束但没有待决定审批,策划产物见上方清单。',
|
||||
);
|
||||
break session;
|
||||
}
|
||||
phase = 'artifact-validation';
|
||||
const requireEditorImages = await hasConfiguredEditorApiKey(
|
||||
runtimeConfig.path,
|
||||
);
|
||||
|
||||
@@ -114,7 +114,6 @@ const allowedUncalledTauriCommands = [
|
||||
'open_game_creator_launcher_window',
|
||||
'open_game_creator_workspace_window',
|
||||
'read_direct_project_conversation',
|
||||
'reset_design_agent_session',
|
||||
'stop_local_game_preview_if_matches',
|
||||
'start_game_creator_external_mcp',
|
||||
'stop_game_creator_external_mcp',
|
||||
@@ -1286,9 +1285,6 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
||||
throw new Error('AI game creator shell identifier drifted');
|
||||
}
|
||||
|
||||
const expectedBundledDesignAgentResources = {
|
||||
'design-agent': 'design-agent',
|
||||
};
|
||||
const expectedBundledCodexResources = {
|
||||
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
||||
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
|
||||
@@ -1304,33 +1300,16 @@ const expectedBundledCodexResources = {
|
||||
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
|
||||
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
|
||||
};
|
||||
assert.deepEqual(
|
||||
tauriConfig.bundle?.resources,
|
||||
expectedBundledDesignAgentResources,
|
||||
'AI game creator shell base Tauri config must bundle the design-agent resource pack',
|
||||
);
|
||||
for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
|
||||
if (String(key).includes('codex')) {
|
||||
throw new Error(
|
||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||
);
|
||||
}
|
||||
if (tauriConfig.bundle?.resources !== undefined) {
|
||||
throw new Error(
|
||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
windowsTauriConfig.bundle?.resources,
|
||||
expectedBundledCodexResources,
|
||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
|
||||
);
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
windowsTauriConfig.bundle?.resources ?? {},
|
||||
'design-agent',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'design-agent resource pack must not be mixed into the Windows Codex sidecar bundle',
|
||||
);
|
||||
}
|
||||
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
||||
throw new Error(
|
||||
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
|
||||
@@ -1726,16 +1705,10 @@ if (
|
||||
runtimeConfigSetupStart === -1 ||
|
||||
runtimeConfigSetupEnd === -1 ||
|
||||
!runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') ||
|
||||
!runtimeConfigSetupSource.includes('setup_log.fail(') ||
|
||||
!runtimeConfigSetupSource.includes('append_bounded_diagnostic_line(') ||
|
||||
!runtimeConfigSetupSource.includes(
|
||||
'startup.appdata.configure.failed details={details}',
|
||||
) ||
|
||||
!tauriHandlerSource.includes('impl StartupLogSlot {') ||
|
||||
!tauriHandlerSource.includes('append_bounded_diagnostic_line(&path, line)') ||
|
||||
!tauriHandlerSource.includes(
|
||||
'self.append(line);\n show_startup_error_dialog(self.path().as_deref());',
|
||||
) ||
|
||||
!tauriHandlerSource.includes('early_startup_log_path(')
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures',
|
||||
|
||||
@@ -722,7 +722,7 @@ function runAgent() {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on('error', reject);
|
||||
child.on('close', (code, signal) => {
|
||||
child.on('close', (code) => {
|
||||
const output = `${stdout}${stderr}`;
|
||||
if (previewReadError) {
|
||||
reject(previewReadError);
|
||||
@@ -740,24 +740,13 @@ function runAgent() {
|
||||
previewDom,
|
||||
});
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`agent run failed: exitCode=${code}, signal=${signal ?? 'none'}\n` +
|
||||
`stderr tail (last 8000 characters):\n${stderr.slice(-8000)}\n` +
|
||||
`stdout tail (last 4000 characters):\n${stdout.slice(-4000)}`,
|
||||
),
|
||||
);
|
||||
reject(new Error(output || `agent run exited with ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function seedLocalAsset() {
|
||||
await fs.mkdir(path.join(projectRoot, 'game'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, 'game/index.html'),
|
||||
'<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><body>还没有生成游戏</body></html>',
|
||||
);
|
||||
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
|
||||
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
|
||||
await seedConversationContext();
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
normalizeWindowsPath,
|
||||
parseWindowsProcessSnapshot,
|
||||
stopWindowsProcessTree,
|
||||
stopWindowsWorktreeProcesses,
|
||||
} from '../../../scripts/dev-windows-process.mjs';
|
||||
import {
|
||||
agcVitePortEnvKey,
|
||||
readAgcDevEndpoint,
|
||||
@@ -21,10 +15,6 @@ import {
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const repoRoot = resolve(appRoot, '../..');
|
||||
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
|
||||
const apiServerExePath = resolve(
|
||||
repoRoot,
|
||||
'server-rs/target/debug/api-server.exe',
|
||||
);
|
||||
const defaultApiTarget =
|
||||
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
|
||||
const backendDatabase = 'genarrative-game-creator-dev';
|
||||
@@ -170,184 +160,19 @@ function readBackendServiceFailure(
|
||||
return null;
|
||||
}
|
||||
|
||||
function urlPort(url) {
|
||||
try {
|
||||
const port = Number(new URL(url).port);
|
||||
return Number.isInteger(port) && port > 0 ? port : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如缺少
|
||||
// Get-NetTCPConnection),此时调用方必须退化为旧行为,不能让本地启动直接失败。
|
||||
function readWindowsPortOwnerIdentities(
|
||||
ports,
|
||||
{ spawnImpl = spawnSync, env = process.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 result = spawnImpl(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: { ...env, GENARRATIVE_QUERY_PORTS: uniquePorts.join(',') },
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
if (result?.error || result?.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
function isWorktreeApiServerOwner(
|
||||
owner,
|
||||
{ expectedExePath = apiServerExePath } = {},
|
||||
) {
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
const expected = normalizeWindowsPath(expectedExePath);
|
||||
const actual = normalizeWindowsPath(owner.executablePath);
|
||||
return Boolean(expected) && actual === expected;
|
||||
}
|
||||
|
||||
function isWorktreeSpacetimeOwner(
|
||||
owner,
|
||||
{ expectedDataDir = backendSpacetimeDataDir } = {},
|
||||
) {
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
const expected = normalizeWindowsPath(expectedDataDir);
|
||||
if (!expected) {
|
||||
return false;
|
||||
}
|
||||
const name = String(owner.name ?? '').toLowerCase();
|
||||
if (!name.startsWith('spacetime')) {
|
||||
return false;
|
||||
}
|
||||
return normalizeWindowsPath(owner.commandLine).includes(expected);
|
||||
}
|
||||
|
||||
// 端口健康不代表后端属于当前工作树:上个工作树 Ctrl+C 残留的 api-server 仍会
|
||||
// 应答 /healthz。复用前必须证明端口上的进程就是本工作树的可执行文件与数据目录。
|
||||
function verifyAgcBackendOwnership({
|
||||
apiUrl,
|
||||
spacetimeUrl,
|
||||
bgfilterWorkerUrl,
|
||||
platform = process.platform,
|
||||
expectedExePath = apiServerExePath,
|
||||
expectedDataDir = backendSpacetimeDataDir,
|
||||
readPortOwners = readWindowsPortOwnerIdentities,
|
||||
} = {}) {
|
||||
if (platform !== 'win32') {
|
||||
return { ok: true, reason: 'platform-unsupported', owners: new Map() };
|
||||
}
|
||||
|
||||
const ports = [
|
||||
urlPort(apiUrl),
|
||||
urlPort(bgfilterWorkerUrl),
|
||||
urlPort(spacetimeUrl),
|
||||
];
|
||||
const owners = readPortOwners(ports);
|
||||
if (!owners) {
|
||||
return { ok: true, reason: 'owner-probe-unavailable', owners: new Map() };
|
||||
}
|
||||
|
||||
const apiOwner = owners.get(urlPort(apiUrl));
|
||||
if (!isWorktreeApiServerOwner(apiOwner, { expectedExePath })) {
|
||||
return { ok: false, reason: 'api-server-owner-mismatch', owners, apiOwner };
|
||||
}
|
||||
|
||||
const workerOwner = owners.get(urlPort(bgfilterWorkerUrl));
|
||||
if (!isWorktreeApiServerOwner(workerOwner, { expectedExePath })) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'bgfilter-worker-owner-mismatch',
|
||||
owners,
|
||||
workerOwner,
|
||||
};
|
||||
}
|
||||
|
||||
const spacetimeOwner = owners.get(urlPort(spacetimeUrl));
|
||||
if (!isWorktreeSpacetimeOwner(spacetimeOwner, { expectedDataDir })) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'spacetime-owner-mismatch',
|
||||
owners,
|
||||
spacetimeOwner,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, reason: 'owned', owners };
|
||||
}
|
||||
|
||||
function formatOwnerLabel(owner) {
|
||||
if (!owner) {
|
||||
return '未知进程';
|
||||
}
|
||||
const pid = Number(owner.processId);
|
||||
const label = owner.executablePath || owner.commandLine || owner.name || '';
|
||||
return `${Number.isInteger(pid) ? `pid=${pid} ` : ''}${String(label).trim()}`.trim();
|
||||
}
|
||||
|
||||
async function isBackendReady({
|
||||
state = readJson(devStackStatePath),
|
||||
isReady = isHttpReady,
|
||||
verifyOwnership = verifyAgcBackendOwnership,
|
||||
onOwnershipRejected = null,
|
||||
} = {}) {
|
||||
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
|
||||
resolveBackendTargetsFromState(state, {
|
||||
requireAgcBackend: true,
|
||||
});
|
||||
if (!hasMatchingBackend || !apiUrl || !spacetimeUrl || !bgfilterWorkerUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ownership = await verifyOwnership({
|
||||
apiUrl,
|
||||
spacetimeUrl,
|
||||
bgfilterWorkerUrl,
|
||||
});
|
||||
if (!ownership?.ok) {
|
||||
onOwnershipRejected?.(ownership);
|
||||
return false;
|
||||
}
|
||||
if (ownership.reason === 'owner-probe-unavailable') {
|
||||
console.warn(
|
||||
'[ai-game-creator-shell] 无法读取端口监听进程归属,本次按旧行为复用配套后端。',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
hasMatchingBackend &&
|
||||
Boolean(apiUrl) &&
|
||||
Boolean(spacetimeUrl) &&
|
||||
Boolean(bgfilterWorkerUrl) &&
|
||||
(await isReady(`${apiUrl}/healthz`)) &&
|
||||
(await isReady(`${spacetimeUrl}/v1/ping`)) &&
|
||||
(await isReady(`${bgfilterWorkerUrl}/readyz`))
|
||||
@@ -660,18 +485,14 @@ async function terminateChildTree(
|
||||
return { stopped: true, forced: false };
|
||||
}
|
||||
const result = await taskkillImpl(child.pid);
|
||||
const taskkillStopped =
|
||||
!result?.timedOut &&
|
||||
!result?.error &&
|
||||
[0, 128].includes(result?.code ?? 0);
|
||||
if (taskkillStopped) {
|
||||
return { stopped: true, forced: true, result };
|
||||
}
|
||||
|
||||
// 包装层(cmd.exe / npm.cmd)先被 Ctrl+C 杀掉时 taskkill 拿不到活着的 PID,
|
||||
// 这里继续按记录下来的根 PID 遍历,尽量收掉更深的后端进程。
|
||||
const treeStopped = stopWindowsProcessTree(child.pid);
|
||||
return { stopped: treeStopped.length > 0, forced: true, result };
|
||||
return {
|
||||
stopped:
|
||||
!result?.timedOut &&
|
||||
!result?.error &&
|
||||
[0, 128].includes(result?.code ?? 0),
|
||||
forced: true,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
const processGroupId = childLifecycles.get(child)?.processGroupId;
|
||||
@@ -721,29 +542,15 @@ async function waitForBackendReady(
|
||||
backendChild,
|
||||
timeoutMs = 600_000,
|
||||
{
|
||||
checkBackendReady = (onOwnershipRejected) =>
|
||||
isBackendReady({ onOwnershipRejected }),
|
||||
checkBackendReady = isBackendReady,
|
||||
readState = () => readJson(devStackStatePath),
|
||||
resolveTargets = readBackendTargets,
|
||||
} = {},
|
||||
) {
|
||||
const initialStateUpdatedAt = readState()?.updatedAt ?? '';
|
||||
const startedAt = Date.now();
|
||||
let lastOwnershipReason = '';
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (
|
||||
await checkBackendReady((ownership) => {
|
||||
if (ownership.reason === lastOwnershipReason) {
|
||||
return;
|
||||
}
|
||||
lastOwnershipReason = ownership.reason;
|
||||
// 本次自己拉起的后端如果归属校验一直不通过,必须把原因打出来,
|
||||
// 否则只会表现为等待 600 秒后超时。
|
||||
console.warn(
|
||||
`[ai-game-creator-shell] 等待配套后端就绪时归属校验未通过(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)})。`,
|
||||
);
|
||||
})
|
||||
) {
|
||||
if (await checkBackendReady()) {
|
||||
return resolveTargets();
|
||||
}
|
||||
const state = readState();
|
||||
@@ -769,14 +576,7 @@ async function waitForBackendReady(
|
||||
|
||||
async function ensureBackend({
|
||||
onBackendChild = () => {},
|
||||
checkBackendReady = () =>
|
||||
isBackendReady({
|
||||
onOwnershipRejected(ownership) {
|
||||
console.warn(
|
||||
`[ai-game-creator-shell] 端口上的配套后端不属于当前工作树(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)}),改为启动本工作树自己的后端。`,
|
||||
);
|
||||
},
|
||||
}),
|
||||
checkBackendReady = isBackendReady,
|
||||
resolveTargets = readBackendTargets,
|
||||
spawnBackend = () =>
|
||||
spawnChild(
|
||||
@@ -856,33 +656,15 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
|
||||
|
||||
async function main() {
|
||||
let backendChild = null;
|
||||
let startedBackend = false;
|
||||
let viteChild = null;
|
||||
let shutdownSignal = '';
|
||||
const signalHandlers = new Map();
|
||||
|
||||
// 只有本次会话真正拉起过配套后端时才做兜底清扫:复用别人后端时不能连带
|
||||
// 杀掉对方的进程。dev.mjs 的清理依赖它的 shell 包装层仍然活着,而 Ctrl+C
|
||||
// 往往先杀掉包装层,所以这里必须按本工作树 api-server.exe 的身份再收一次。
|
||||
const sweepStartedBackend = () => {
|
||||
if (!startedBackend || process.platform !== 'win32') {
|
||||
return;
|
||||
}
|
||||
const stopped = stopWindowsWorktreeProcesses({ apiServerExePath });
|
||||
if (stopped.length > 0) {
|
||||
console.log(
|
||||
`[ai-game-creator-shell] 已清理残留后端进程: ${stopped.join(', ')}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||
const handler = () => {
|
||||
shutdownSignal = signal;
|
||||
stopChild(viteChild, signal);
|
||||
stopChild(backendChild, signal);
|
||||
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
|
||||
sweepStartedBackend();
|
||||
};
|
||||
signalHandlers.set(signal, handler);
|
||||
process.on(signal, handler);
|
||||
@@ -901,7 +683,6 @@ async function main() {
|
||||
},
|
||||
});
|
||||
backendChild = backend.backendChild;
|
||||
startedBackend = Boolean(backendChild);
|
||||
if (shutdownSignal) {
|
||||
throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`);
|
||||
}
|
||||
@@ -935,7 +716,6 @@ async function main() {
|
||||
terminateChildTree(viteChild),
|
||||
terminateChildTree(backendChild),
|
||||
]);
|
||||
sweepStartedBackend();
|
||||
for (const [signal, handler] of signalHandlers) {
|
||||
process.off(signal, handler);
|
||||
}
|
||||
@@ -952,25 +732,20 @@ function isDirectModuleExecution() {
|
||||
export {
|
||||
ensureBackend,
|
||||
formatChildFailure,
|
||||
formatOwnerLabel,
|
||||
isAiGameCreatorServer,
|
||||
isBackendReady,
|
||||
isDirectModuleExecution,
|
||||
isProcessGroupAlive,
|
||||
isWorktreeApiServerOwner,
|
||||
isWorktreeSpacetimeOwner,
|
||||
preflightExistingVite,
|
||||
readBackendServiceFailure,
|
||||
readChildFailure,
|
||||
readExistingViteServer,
|
||||
readLinuxProcessGroupAlive,
|
||||
readWindowsPortOwnerIdentities,
|
||||
resolveBackendTargetsFromState,
|
||||
runWindowsTaskkill,
|
||||
spawnChild,
|
||||
stopChild,
|
||||
terminateChildTree,
|
||||
verifyAgcBackendOwnership,
|
||||
waitForBackendReady,
|
||||
waitForChildTermination,
|
||||
};
|
||||
|
||||
@@ -20,10 +20,6 @@ import {
|
||||
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';
|
||||
|
||||
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
const args = [...argv];
|
||||
@@ -112,11 +108,7 @@ async function runTauriDev(
|
||||
if (!prepared || shutdownSignal) return 1;
|
||||
const tauriArguments = buildTauriArguments(argv, endpoint.url);
|
||||
child = spawnCli(tauriArguments, {
|
||||
env: {
|
||||
...withAgcDevEndpointEnv(endpoint),
|
||||
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
|
||||
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
|
||||
},
|
||||
env: withAgcDevEndpointEnv(endpoint),
|
||||
});
|
||||
const childResult = waitForCli(child);
|
||||
const outcome = await Promise.race([
|
||||
@@ -165,13 +157,7 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
|
||||
const frontend = spawnChild(
|
||||
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
||||
['run', 'agc:serve'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...withAgcDevEndpointEnv(endpoint),
|
||||
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
|
||||
},
|
||||
},
|
||||
{ cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
|
||||
);
|
||||
onChild(frontend);
|
||||
console.log(
|
||||
|
||||
@@ -74,14 +74,6 @@ codegen-units = 256
|
||||
lto = "off"
|
||||
incremental = true
|
||||
|
||||
# Runner 启动阶段会对当前 Debug 可执行文件计算 SHA-256。仅优化密码学依赖,
|
||||
# 保持业务代码的 Debug 编译速度,同时避免整份 Debug 构建因未优化 hash 热点而阻塞启动。
|
||||
[profile.dev.package.sha2]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.digest]
|
||||
opt-level = 3
|
||||
|
||||
[profile.test]
|
||||
opt-level = 0
|
||||
debug = 1
|
||||
|
||||
@@ -43,6 +43,7 @@ struct PromptCompositions {
|
||||
/// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、
|
||||
/// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段
|
||||
/// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。
|
||||
supervisor_plan: Vec<String>,
|
||||
supervisor_chat: SupervisorChatComposition,
|
||||
}
|
||||
|
||||
@@ -98,6 +99,10 @@ struct ProviderFragments {
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AgentCatalog {
|
||||
supervisor: AgentGroup,
|
||||
/// 立项策划子 Agent。与 `supervisor` 平级、**不进 `groups`**:`specialist_nodes`
|
||||
/// 只从 `groups[].roles[]` 派生,因此它不参与 `build.rs` 与种子 DAG 的一致性
|
||||
/// 校验,「做游戏」的 16 任务 DAG 一行不动。详见技术方案第 3.1 节。
|
||||
planning: AgentGroup,
|
||||
groups: Vec<AgentGroup>,
|
||||
}
|
||||
|
||||
@@ -226,6 +231,12 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
§ions,
|
||||
&["$base", "$visualContract"],
|
||||
)?;
|
||||
validate_composition(
|
||||
"supervisorPlan",
|
||||
&manifest.compositions.supervisor_plan,
|
||||
§ions,
|
||||
&["$header"],
|
||||
)?;
|
||||
validate_section_reference(
|
||||
&manifest.compositions.supervisor_chat.identity,
|
||||
§ions,
|
||||
@@ -288,6 +299,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
.supervisor
|
||||
.roles
|
||||
.iter()
|
||||
.chain(manifest.agent_catalog.planning.roles.iter())
|
||||
.chain(
|
||||
manifest
|
||||
.agent_catalog
|
||||
@@ -336,6 +348,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
.runtime
|
||||
.iter()
|
||||
.chain(manifest.compositions.supervisor.iter())
|
||||
.chain(manifest.compositions.supervisor_plan.iter())
|
||||
.filter(|item| !item.starts_with('$'))
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
@@ -408,6 +421,14 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
{
|
||||
register("composition supervisor", section);
|
||||
}
|
||||
for section in manifest
|
||||
.compositions
|
||||
.supervisor_plan
|
||||
.iter()
|
||||
.filter(|section| !section.starts_with('$'))
|
||||
{
|
||||
register("composition supervisorPlan", section);
|
||||
}
|
||||
register("composition supervisorChat.identity", identity);
|
||||
register(
|
||||
"composition supervisorChat.finalReply",
|
||||
@@ -436,9 +457,19 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
"composition supervisor",
|
||||
"composition supervisorChat.identity",
|
||||
]);
|
||||
// plan 根 composition 是 Supervisor system prompt 的第二条 lane,不是另一种
|
||||
// 语义面。它按设计复用 runtime lane 的 `isolatedAgentContract`(`agent.delegate`
|
||||
// 的 expectedArtifacts/writeScopes 合同)和 supervisor lane 的 `supervisorRepair`
|
||||
// (返工必须逐字继承原合同)。除这两个方向外,跨所有者复用仍然是错误。
|
||||
let allowed_plan_runtime_owners =
|
||||
BTreeSet::from(["composition runtime", "composition supervisorPlan"]);
|
||||
let allowed_plan_supervisor_owners =
|
||||
BTreeSet::from(["composition supervisor", "composition supervisorPlan"]);
|
||||
for (section, section_owners) in owners {
|
||||
if section_owners.len() > 1
|
||||
&& !(section == identity && section_owners == allowed_identity_owners)
|
||||
&& section_owners != allowed_plan_runtime_owners
|
||||
&& section_owners != allowed_plan_supervisor_owners
|
||||
{
|
||||
return Err(format!(
|
||||
"Prompt section 跨语义所有者复用:{section} -> {section_owners:?}"
|
||||
@@ -675,11 +706,17 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
if catalog.supervisor.roles.len() != 1 {
|
||||
return Err("agentCatalog.supervisor 必须且只能包含一个 role".to_string());
|
||||
}
|
||||
if catalog.planning.roles.len() != 1 {
|
||||
return Err("agentCatalog.planning 必须且只能包含一个 role".to_string());
|
||||
}
|
||||
if catalog.groups.is_empty() {
|
||||
return Err("agentCatalog.groups 不能为空".to_string());
|
||||
}
|
||||
let mut group_brief_names = BTreeSet::new();
|
||||
for group in std::iter::once(&catalog.supervisor).chain(catalog.groups.iter()) {
|
||||
for group in std::iter::once(&catalog.supervisor)
|
||||
.chain(std::iter::once(&catalog.planning))
|
||||
.chain(catalog.groups.iter())
|
||||
{
|
||||
if !group_brief_names.insert(group.brief_path_name.as_str()) {
|
||||
return Err(format!(
|
||||
"agent group briefPathName 重复:{}",
|
||||
@@ -687,7 +724,10 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut generated_names = BTreeSet::from(["PROJECT_SUPERVISOR".to_string()]);
|
||||
let mut generated_names = BTreeSet::from([
|
||||
"PROJECT_SUPERVISOR".to_string(),
|
||||
"PROJECT_PLANNING".to_string(),
|
||||
]);
|
||||
for group in &catalog.groups {
|
||||
let generated = rust_identifier(&group.id);
|
||||
if !generated
|
||||
@@ -713,6 +753,12 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
|
||||
&mut task_ids,
|
||||
&mut tool_ids,
|
||||
)?;
|
||||
validate_agent_group(
|
||||
&catalog.planning,
|
||||
&mut group_ids,
|
||||
&mut task_ids,
|
||||
&mut tool_ids,
|
||||
)?;
|
||||
for group in &catalog.groups {
|
||||
validate_agent_group(group, &mut group_ids, &mut task_ids, &mut tool_ids)?;
|
||||
}
|
||||
@@ -874,6 +920,10 @@ fn render_rust(manifest: &PromptBundleManifest, sections: &BTreeMap<String, Stri
|
||||
"RUNTIME_PROMPT_SUPERVISOR_COMPOSITION",
|
||||
&manifest.compositions.supervisor,
|
||||
));
|
||||
output.push_str(&render_string_slice_const(
|
||||
"RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION",
|
||||
&manifest.compositions.supervisor_plan,
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n",
|
||||
rust_literal(&manifest.compositions.supervisor_chat.identity),
|
||||
@@ -962,6 +1012,26 @@ fn render_agent_catalog(catalog: &AgentCatalog) -> String {
|
||||
"static PROJECT_SUPERVISOR_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
|
||||
render_group_value(&catalog.supervisor, "&PROJECT_SUPERVISOR_AGENT_ROLES")
|
||||
));
|
||||
let planning_role = &catalog.planning.roles[0];
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_AGENT_ID: &str = {};\n",
|
||||
rust_literal(&planning_role.task_id)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_MEMORY_PATH: &str = {};\n",
|
||||
rust_literal(&format!(
|
||||
"memory/agents/{}",
|
||||
catalog.planning.brief_path_name
|
||||
))
|
||||
));
|
||||
output.push_str(&render_role_array(
|
||||
"PROJECT_PLANNING_AGENT_ROLES",
|
||||
&catalog.planning.roles,
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"static PROJECT_PLANNING_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
|
||||
render_group_value(&catalog.planning, "&PROJECT_PLANNING_AGENT_ROLES")
|
||||
));
|
||||
for group in &catalog.groups {
|
||||
let roles_name = format!("{}_AGENT_ROLES", rust_identifier(&group.id));
|
||||
output.push_str(&render_role_array(&roles_name, &group.roles));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main",
|
||||
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
|
||||
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件。",
|
||||
"windows": ["client"],
|
||||
"permissions": [
|
||||
"clipboard-manager:allow-read-image",
|
||||
@@ -21,7 +21,6 @@
|
||||
]
|
||||
},
|
||||
"opener:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save"
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:系统架构。明确系统清单、职责边界、依赖和数据归属。
|
||||
@@ -1,6 +0,0 @@
|
||||
共享过程文件(如需维护,请使用这些相对路径):
|
||||
- project/analysis.md
|
||||
- project/决策台账.md
|
||||
- project/dialog.md
|
||||
不要把正式产物写在工作区根目录,也不要等审批失败后再迁移。
|
||||
阶段审批工具:当你判断本阶段必需产物已完成时,必须提交阶段审批。用户批准后 Runtime 自动进入下一阶段;你不能自行切换阶段。
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:概念设计。明确游戏是什么、不是什么,并形成概念设计产物。
|
||||
@@ -1,4 +0,0 @@
|
||||
顾问阶段不需要继续自主推动项目或主动安排下一步;遵照用户的具体指示行动。
|
||||
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
|
||||
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定;不要替用户做决定。
|
||||
顾问阶段没有下一层,也不需要提交阶段审批。
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:项目顾问。五个策划阶段已经完成,后续由用户指示驱动协作。
|
||||
@@ -1,44 +0,0 @@
|
||||
概念阶段定稿时,还必须创建或更新 `project/速览卡.md`。Runtime 只检查该文件是否存在,不检查内容。请使用下面的固定结构,不要加入审批操作说明或独立的决定状态段落:
|
||||
|
||||
# 速览卡:《游戏名》
|
||||
|
||||
## 1. 游戏名称
|
||||
|
||||
## 2. 游戏分类
|
||||
|
||||
## 3. 美术风格
|
||||
- 视觉类型:
|
||||
- 风格关键词:
|
||||
- 色彩与氛围:
|
||||
- MVP 美术边界:
|
||||
|
||||
## 4. 一句话描述
|
||||
|
||||
## 5. 游戏支柱
|
||||
| 支柱 | 玩家感受 | 实现机制 |
|
||||
|---|---|---|
|
||||
|
||||
## 6. 核心循环
|
||||
|
||||
## 7. 目标用户
|
||||
- 核心用户:
|
||||
- 游戏偏好:
|
||||
- 单次游玩时长:
|
||||
- 参考游戏与参考点:
|
||||
|
||||
## 8. 平台事实
|
||||
|
||||
## 9. 最小 MVP 系统
|
||||
| 系统 | 最小功能 | 为什么必须有 | 验证方法 |
|
||||
|---|---|---|---|
|
||||
|
||||
## 10. 给创作者的关键提示
|
||||
- 先做:
|
||||
- 暂时不做:
|
||||
- 这样验证:
|
||||
- 达标再扩展:
|
||||
|
||||
### 待原型验证项
|
||||
- 问题:
|
||||
- 原型:
|
||||
- 观察:
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:系统文档。逐个完成已确定系统的内部规则、接口和验证标准。
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:技术文档。完成数据与配表、技术实现、美术圣经和总册。
|
||||
@@ -1 +0,0 @@
|
||||
当前阶段:顶层设计。明确玩家持续游玩的循环、资源流、节奏和系统范围。
|
||||
File diff suppressed because one or more lines are too long
@@ -1,384 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"resources": [
|
||||
{
|
||||
"path": "skills/concept.md",
|
||||
"id": "skills.concept",
|
||||
"summary": "概念阶段写作规则。",
|
||||
"category": "skills",
|
||||
"title": "概念设计分册",
|
||||
"inject_phases": [
|
||||
"concept"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "skills/top_design.md",
|
||||
"id": "skills.top_design",
|
||||
"summary": "顶层设计阶段写作规则。",
|
||||
"category": "skills",
|
||||
"title": "顶层设计分册",
|
||||
"inject_phases": [
|
||||
"top_design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "skills/architecture.md",
|
||||
"id": "skills.architecture",
|
||||
"summary": "系统架构阶段写作规则。",
|
||||
"category": "skills",
|
||||
"title": "系统架构分册",
|
||||
"inject_phases": [
|
||||
"architecture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "skills/systems.md",
|
||||
"id": "skills.systems",
|
||||
"summary": "系统文档阶段写作规则。",
|
||||
"category": "skills",
|
||||
"title": "系统文档分册",
|
||||
"inject_phases": [
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "skills/tdd.md",
|
||||
"id": "skills.tdd",
|
||||
"summary": "技术文档阶段写作规则。",
|
||||
"category": "skills",
|
||||
"title": "技术文档分册",
|
||||
"inject_phases": [
|
||||
"tdd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "templates.analysis",
|
||||
"category": "templates",
|
||||
"title": "analysis",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/analysis.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.architecture",
|
||||
"category": "templates",
|
||||
"title": "architecture",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/architecture.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.concept_design",
|
||||
"category": "templates",
|
||||
"title": "concept-design",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/concept-design.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.stardew_analysis",
|
||||
"category": "templates",
|
||||
"title": "stardew-analysis",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/stardew-analysis.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.tdd_art_bible",
|
||||
"category": "templates",
|
||||
"title": "tdd-art-bible",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/tdd-art-bible.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.tdd_data",
|
||||
"category": "templates",
|
||||
"title": "tdd-data",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/tdd-data.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.tdd_master",
|
||||
"category": "templates",
|
||||
"title": "tdd-master",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/tdd-master.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.tdd_tech",
|
||||
"category": "templates",
|
||||
"title": "tdd-tech",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/tdd-tech.md"
|
||||
},
|
||||
{
|
||||
"id": "templates.top_design",
|
||||
"category": "templates",
|
||||
"title": "top-design",
|
||||
"summary": "策划文档结构模板。",
|
||||
"path": "templates/top-design.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.decision_log",
|
||||
"category": "exemplars",
|
||||
"title": "decision-log",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/decision-log.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.fast_gdd",
|
||||
"category": "exemplars",
|
||||
"title": "fast-gdd",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/fast-gdd.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.overview_card",
|
||||
"category": "exemplars",
|
||||
"title": "overview-card",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/overview-card.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_architecture",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-architecture",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-architecture.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_concept",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-concept",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-concept.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_s06_combat",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-s06-combat",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-s06-combat.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_tdd_art_bible",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-tdd-art-bible",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-tdd-art-bible.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_tdd_data",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-tdd-data",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-tdd-data.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_tdd_master",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-tdd-master",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-tdd-master.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_tdd_tech",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-tdd-tech",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-tdd-tech.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.stardew_top_design",
|
||||
"category": "exemplars",
|
||||
"title": "stardew-top-design",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/stardew-top-design.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.tdd_art_bible_SKILL",
|
||||
"category": "exemplars",
|
||||
"title": "tdd-art-bible-SKILL",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/tdd-art-bible-SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.tdd_data_SKILL",
|
||||
"category": "exemplars",
|
||||
"title": "tdd-data-SKILL",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/tdd-data-SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "exemplars.tdd_tech_SKILL",
|
||||
"category": "exemplars",
|
||||
"title": "tdd-tech-SKILL",
|
||||
"summary": "策划文档范例或需求附件。",
|
||||
"path": "exemplars/tdd-tech-SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.核心玩法编排.skill",
|
||||
"category": "system_types",
|
||||
"title": "01_核心玩法编排 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/01_核心玩法编排/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.核心玩法编排.template",
|
||||
"category": "system_types",
|
||||
"title": "01_核心玩法编排 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/01_核心玩法编排/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.时间与日程.skill",
|
||||
"category": "system_types",
|
||||
"title": "02_时间与日程 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/02_时间与日程/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.时间与日程.template",
|
||||
"category": "system_types",
|
||||
"title": "02_时间与日程 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/02_时间与日程/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.生产种植经营.skill",
|
||||
"category": "system_types",
|
||||
"title": "03_生产种植经营 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/03_生产种植经营/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.生产种植经营.template",
|
||||
"category": "system_types",
|
||||
"title": "03_生产种植经营 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/03_生产种植经营/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.地图与探索.skill",
|
||||
"category": "system_types",
|
||||
"title": "04_地图与探索 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/04_地图与探索/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.地图与探索.template",
|
||||
"category": "system_types",
|
||||
"title": "04_地图与探索 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/04_地图与探索/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.采集与支线活动.skill",
|
||||
"category": "system_types",
|
||||
"title": "05_采集与支线活动 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/05_采集与支线活动/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.采集与支线活动.template",
|
||||
"category": "system_types",
|
||||
"title": "05_采集与支线活动 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/05_采集与支线活动/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.战斗与敌人.skill",
|
||||
"category": "system_types",
|
||||
"title": "06_战斗与敌人 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/06_战斗与敌人/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.战斗与敌人.template",
|
||||
"category": "system_types",
|
||||
"title": "06_战斗与敌人 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/06_战斗与敌人/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.物品背包与制作.skill",
|
||||
"category": "system_types",
|
||||
"title": "07_物品背包与制作 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/07_物品背包与制作/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.物品背包与制作.template",
|
||||
"category": "system_types",
|
||||
"title": "07_物品背包与制作 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/07_物品背包与制作/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.成长与技能.skill",
|
||||
"category": "system_types",
|
||||
"title": "08_成长与技能 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/08_成长与技能/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.成长与技能.template",
|
||||
"category": "system_types",
|
||||
"title": "08_成长与技能 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/08_成长与技能/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.NPC关系与任务.skill",
|
||||
"category": "system_types",
|
||||
"title": "09_NPC关系与任务 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/09_NPC关系与任务/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.NPC关系与任务.template",
|
||||
"category": "system_types",
|
||||
"title": "09_NPC关系与任务 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/09_NPC关系与任务/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.经济与商店.skill",
|
||||
"category": "system_types",
|
||||
"title": "10_经济与商店 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/10_经济与商店/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.经济与商店.template",
|
||||
"category": "system_types",
|
||||
"title": "10_经济与商店 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/10_经济与商店/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.事件与节日.skill",
|
||||
"category": "system_types",
|
||||
"title": "11_事件与节日 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/11_事件与节日/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.事件与节日.template",
|
||||
"category": "system_types",
|
||||
"title": "11_事件与节日 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/11_事件与节日/模板.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.UI与文本呈现.skill",
|
||||
"category": "system_types",
|
||||
"title": "12_UI与文本呈现 skill",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/12_UI与文本呈现/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "system_types.UI与文本呈现.template",
|
||||
"category": "system_types",
|
||||
"title": "12_UI与文本呈现 template",
|
||||
"summary": "系统类型写法规则或模板。",
|
||||
"path": "modules/system-types/12_UI与文本呈现/模板.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# 决策台账:《星露谷物语》金样项目
|
||||
|
||||
版本:v3 | 规则:台账放活队列——design 只放结论、分析只放论证、决定与开放问题住这里。编号连续不复用;被推翻的行标 overturned 挂新行,不删行。
|
||||
状态六态:`confirmed`(用户亲口/亲选)/ `auto_decided`(技术类代决,必带理由+推翻条件,用户一键可翻)/ `default_pending`(默认建议兜底,用户未点头)/ `prototype_pending`(待原型验证)/ `pending_user`(等用户拍板)/ `overturned`(被推翻,挂旧行编号)。
|
||||
|
||||
> 编号口径:D-01~D-13 与 exemplars/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
|
||||
|
||||
## 当前待办(活队列)
|
||||
|
||||
### 等用户拍板(pending_user)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据 | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-14 | 体力与战斗共享单池 | TDD | user | 风险资源统一制造取舍(概念张力一);**暂按共享实现,改单拆只需改 S02 成本入口** | 战斗参与率实测过低(玩家回避矿井) | pending_user(暂按共享实现) |
|
||||
| D-15 | 背包格子制 vs 重量制 | TDD | user | 格子制直觉、重量制焦虑感与 T5"休闲不打卡"冲突;暂按格子制实现、存档预留 capacity_type 字段 | 格子管理成为主要负面反馈 | pending_user(B 级阻断存档结构,暂按格子制) |
|
||||
|
||||
### 待原型验证(prototype_pending)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据 | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-13b | 战斗判定窗口手感(前摇帧数/无敌帧 450ms 基准) | 系统 | user | 数值可定、手感不可纸面验证 | 原型显示节奏拖慢/玩家困惑 | prototype_pending(规则本体见 D-13 confirmed) |
|
||||
|
||||
### 默认建议兜底(default_pending)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据 | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-19 | 天气权重表具体数值(晴/雨/风暴按季节) | TDD | agent | 概念层只定"雨免浇水"定性;数值推内容期填 | 前 5 日出现连续 3 日雨/全无雨 | default_pending(默认值已进数据表,带 designer_note) |
|
||||
|
||||
## 已采用决定
|
||||
|
||||
### 用户确认(confirmed)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据 | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-01 | 定调:牧场物语系参照、治愈慢节奏 | 概念 | user | 用户原始需求 | — | confirmed |
|
||||
| D-02 | 单人体验,无多人 | 概念 | user | 概念层非目标 | — | confirmed |
|
||||
| D-03 | 战斗保持伴生风险,不做装备驱动主轴 | 概念 | user | 概念期问题一 | 矿井流失率过半且归因战斗 | confirmed |
|
||||
| D-07 | 日目标自设,季节与社区提供低频牵引 | 顶层 | user | 顶层期问题一 | 新手周流失归因无方向 | confirmed |
|
||||
| D-11 | 采集/钓鱼/战斗统一"活动结果"接口 | 架构 | user | 架构期问题一 | 第三活动类型出现结构性差异 | confirmed |
|
||||
| D-13 | 战斗采用节奏/指令判定 | 系统 | user | S06 问题一 | 原型显示节奏拖慢/玩家困惑 | confirmed(手感部分拆 D-13b prototype_pending) |
|
||||
|
||||
### 技术代决(auto_decided——带理由与推翻条件,用户一键可翻)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据(理由) | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-04 | 时间片制:700ms=10 游戏分钟 | 概念 | agent | 原作实证节拍;一天≈14 分钟真实时间贴合 T5"休闲" | 内测一天体感过短/过长 | auto_decided |
|
||||
| D-05 | 分区域切换(区域独立场景,非连续地图) | 概念 | agent | 概念层"不是什么:无边界开放世界";区域小网络全部步行可达 | 场景切换成为移动负担反馈 | auto_decided |
|
||||
| D-06 | 28 日/季、四季/年 | 顶层 | agent | 季节窗口制造"本季计划"节奏(支柱二) | 换季频率在测试中被无视 | auto_decided |
|
||||
| D-09 | 商店营业时段走条件表 | 架构 | agent | 与配方/区域解锁共用 check(condition_id) 单一入口 | 条件表规模膨胀难维护 | auto_decided |
|
||||
| D-10 | 出货箱日终统一结算 | 架构 | agent | 收入集中进日终面板,强化"一天一结算"叙事;商店现卖保留即时通道 | 玩家普遍绕开出货箱 | auto_decided |
|
||||
| D-12 | 工具升级期间该工具不可用 | 系统 | agent | 升级=时间成本换效率(顶层张力二);备用旧工具暂不做(开放问题) | 升级期挫败感集中爆发 | auto_decided |
|
||||
| D-16 | 矿井逐层生成本期不做(P2) | TDD | agent | GDD 已标"不做无限地牢";首期按布局池 8~12 模板拼装 | 内测要求深度爬塔玩法 | auto_decided |
|
||||
| D-17 | 换装首期 5 层(基础体/裤/衣/发型/饰件),非 19 层 | TDD | agent | 外观自定义非首期卖点;层结构预留到 19 层 | 外观系统成核心诉求 | auto_decided |
|
||||
| D-18 | 作物品质三档:普通/银/金 | TDD | agent | 经济分层需要(即时变现 vs 等待升值的取舍) | 银金档无人区分、一律普通出售 | auto_decided |
|
||||
|
||||
### 已推翻(overturned——旧行保留,挂新行)
|
||||
|
||||
| 编号 | 决定 | 层 | 谁 | 依据 | 推翻条件 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D-08 | 作物品质两档:普通/银 | TDD | agent | 早期小权衡:两档最简 | — | **overturned → D-18**(经济分层不足,改三档;铱档留 P1) |
|
||||
|
||||
## 队列纪律(给 agent 的使用说明)
|
||||
|
||||
- 新决定入队:拿下一号(当前最大 D-19,下一号 D-20);就现代决可登记不开条目,但状态必须写 auto_decided 并带理由+推翻条件。
|
||||
- 用户翻案:旧行标 overturned 挂新行,受影响文档节重写(本台账只记录,不代改)。
|
||||
- 概念层变更定稿后:速览卡"决定状态与原型验证项"字段随本文件最新版同步(prompt 级纪律)。
|
||||
@@ -1,124 +0,0 @@
|
||||
# 脉冲荒原:节点争夺战
|
||||
|
||||
> Fast GDD v1 · 状态:ready\_for\_approval
|
||||
> gddId:`gdd-a5309290-edd4-4232-9c7c-d99024e0fa42`
|
||||
> fingerprint:`sha256-serde-json-v2:1d34b0cbee5f623a224102e4d832ad6ecc10ffbf27b876d9bc1e24a5483f06a3`
|
||||
> approvalRequestId:`gdd-approval-6f5de673-553d-499f-8ca1-8c32f4841bc0`
|
||||
|
||||
## 决定状态
|
||||
|
||||
- **初始需求**(confirmed,user\_freeform,第 0 轮):用户提出制作原创、可落地的2D坦克大战MVP。
|
||||
- **坦克对战的胜负目标**(confirmed,user\_option,第 1 轮):采用区域争夺作为每局主要胜负目标。
|
||||
- **区域争夺的占领方式**(confirmed,user\_option,第 2 轮):采用少量多节点轮换占领,迫使双方在路线间转移争夺。
|
||||
- **轮换节点的结束规则**(confirmed,user\_option,第 3 轮):控制当前节点持续得分,任一方先达到目标分即获胜。
|
||||
- **目标用户**(default\_pending,default,第 0 轮):默认面向喜欢短局、直接操控、位置博弈和可重复挑战的休闲动作玩家。
|
||||
- **美术方向**(default\_pending,default,第 0 轮):默认采用风格化、轮廓清晰的原创战场与占位资产,优先验证可读性。
|
||||
- **成长范围**(default\_pending,default,第 0 轮):默认设置一条轻量成长线,仅提供少量战后选择,不影响单局闭环。
|
||||
- **MVP内容边界**(default\_pending,default,第 0 轮):默认首个版本只做单人对抗一名基础AI、单张地图和一套坦克配置。
|
||||
|
||||
## 一句话描述
|
||||
|
||||
驾驶原创装甲穿越掩体,在轮换能量节点间交火夺分,先达目标分者赢得一局紧凑的2D坦克区域争夺战。
|
||||
|
||||
## 游戏分类与美术
|
||||
|
||||
- 主类型:2D坦克区域争夺
|
||||
- 融合类型:无
|
||||
- 视觉类型:风格化俯视2D
|
||||
- 关键词:轮廓清晰、几何掩体、弹道高亮、原创装甲
|
||||
- 色彩氛围:冷青灰战场衬托橙蓝阵营高亮;节点被占领时产生清晰的环形脉冲,命中、受击和得分使用短促高对比反馈。
|
||||
- MVP 美术边界:MVP 使用可复用的几何占位资产:一张俯视战场、两种原创装甲外形、节点与掩体模块;先保证阵营、弹道、占领状态在桌面和移动视口均清楚。
|
||||
|
||||
## 游戏支柱
|
||||
|
||||
### 移动夺势
|
||||
|
||||
- 玩家感受:始终知道下一处冲突在哪里,移动本身就是争夺优势而非赶路。
|
||||
- 机制:节点位置与活跃状态持续改变,玩家必须在占领、转移、驻守和反攻之间做出即时选择。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 掩体交火
|
||||
|
||||
- 玩家感受:每次探头、绕侧和开火都带来可读的风险回报。
|
||||
- 机制:几何掩体阻挡弹道,玩家通过角度、射击窗口和绕行路线逼退对手,再进入节点控分。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 清晰逆转
|
||||
|
||||
- 玩家感受:局势紧张但不迷惑,玩家理解自己为何领先或落后并愿意再开一局。
|
||||
- 机制:目标分、活跃节点和控制状态持续可视化;落后方可通过夺回当前节点改变局势。
|
||||
- 决定状态:confirmed
|
||||
|
||||
## 核心循环
|
||||
|
||||
1. 进入战场并观察当前活跃节点与敌方位置
|
||||
2. 驾驶“棱虎机”在几何掩体间移动,瞄准并发射脉冲炮
|
||||
3. 抵达活跃节点并在敌方干扰下完成占领或夺回
|
||||
4. 控制节点持续积累“脉冲分”,节点轮换后转移路线
|
||||
5. 先达到目标分的一方获胜,结算后可立即重开
|
||||
|
||||
## 目标用户
|
||||
|
||||
- 核心用户:偏好短局即时动作、方向操控、躲避射击和争夺空间的休闲玩家;可接受单人对抗基础AI。
|
||||
- 偏好:喜欢规则一眼可懂、操作反馈直接、每局约十几分钟内完成,并能通过走位和时机而非复杂配装取胜。
|
||||
- 单局时长:默认每局约10~20分钟;首个可玩闭环优先做到一局可完整开始、争夺、决胜和重开。
|
||||
- 参考游戏:无
|
||||
|
||||
## Runtime 平台事实
|
||||
|
||||
- Runtime:self-contained-web
|
||||
- 视口:desktop / mobile
|
||||
- 输入:keyboard / touch
|
||||
- 预览:local-http
|
||||
|
||||
## MVP 系统
|
||||
|
||||
### 坦克操控与射击
|
||||
|
||||
- 最小功能:提供八方向移动、旋转瞄准、单一脉冲炮射击、受击反馈与短暂失活重生,支持键盘和触控操作。
|
||||
- 必要原因:直接构成坦克对战手感,也是区域争夺发生的主要冲突手段。
|
||||
- 验证方式:试玩者能在一分钟内完成移动、瞄准、射击和躲入掩体,并能用射击驱离节点附近敌人。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 轮换节点占领
|
||||
|
||||
- 最小功能:以少量节点组成单张战场,仅一个节点在任一时刻活跃;进入后按占优方推进占领,活跃节点轮换并提供清晰状态提示。
|
||||
- 必要原因:落实用户确认的多节点轮换占领,并制造移动、停留和反攻决策。
|
||||
- 验证方式:玩家无需额外说明即可找到活跃节点,读懂中立、己方和敌方控制状态,并在轮换后改变路线。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 目标分胜负
|
||||
|
||||
- 最小功能:活跃节点由控制方持续获得脉冲分;任一方先达到目标分即胜,显示双方分数、当前节点和胜负结算。
|
||||
- 必要原因:把占领行为闭合成清晰、可验证的单局目标。
|
||||
- 验证方式:试玩者能预测哪方领先、理解如何逆转,并在达到目标分时明确知道对局结束。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 俯视战场结构
|
||||
|
||||
- 最小功能:布置可绕行的几何掩体、节点路径与出生区域;掩体阻挡弹道并形成接近、驻守和侧袭路线。
|
||||
- 必要原因:让位置博弈支撑区域争夺,避免节点规则沦为单纯站桩计分。
|
||||
- 验证方式:观察玩家是否主动利用掩体接近节点或规避火力,而非只在开阔地互射。
|
||||
- 决定状态:confirmed
|
||||
|
||||
### 基础对手AI
|
||||
|
||||
- 最小功能:提供一名基础AI对手:追踪活跃节点、靠近争夺、在射程内攻击并在失活后返回战场。
|
||||
- 必要原因:在无多人条件下完成完整的对抗闭环并验证核心玩法。
|
||||
- 验证方式:连续试玩中,AI应持续争夺节点并制造可理解的反攻机会,不出现长时间卡住或无目标游走。
|
||||
- 决定状态:default\_pending
|
||||
|
||||
## 制作边界
|
||||
|
||||
- 多人联机与服务器
|
||||
- 商城、赛季和复杂社交
|
||||
- 开放世界与完整剧情
|
||||
- 多地图、多武器树与复杂成长
|
||||
- 可编辑关卡和排行榜
|
||||
|
||||
## 创作者提示
|
||||
|
||||
- 先做:先做单张紧凑俯视战场、玩家坦克、基础AI对手、移动瞄准射击、掩体碰撞、节点轮换、占领计分和胜负结算,确保一局可从开始玩到结束。
|
||||
- 暂缓:暂缓多人联机、商城、服务器、开放世界、赛季、复杂社交、完整剧情、多武器树和多地图内容。
|
||||
- 如何验证:用可操作原型观察玩家是否在首局主动驶向活跃节点、利用掩体交火、理解分数变化并完成一局;记录误读占领状态、无目标游走和胜负不明的情况。
|
||||
- 何时扩展:仅当试玩者能无讲解理解活跃节点、占领状态和领先来源,并主动移动反攻时,再增加第二种坦克特性、第二张地图或轻量成长选择。
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
# 速览卡:《星露谷物语》金样项目
|
||||
|
||||
> 12 字段版本;渲染器照旧出卡走审批。字段来源见每节尾注(概念层/定调记录/台账投影)。概念层变更定稿后本卡必须同步更新(prompt 级纪律)。
|
||||
|
||||
## 1. 游戏名称
|
||||
《星露谷物语》(金样项目沿用案例名;新项目由概念层第 1 节定名)←概念层§1
|
||||
|
||||
## 2. 一句话描述
|
||||
继承一座荒废农场的乡村生活 RPG:安排每天的时间与体力,种田、探索、交朋友,把日子过成自己想要的样子。(←概念层§1 一句话概念,45~90 字)
|
||||
|
||||
## 3. 游戏分类
|
||||
乡村生活模拟 RPG(经营+探索+社交;参照系牧场物语)←概念层§1
|
||||
|
||||
## 4. 美术风格(四件)←定调记录+概念层§4
|
||||
- 视觉类型:手绘感像素风、俯视 45° 视角。
|
||||
- 风格关键词:温暖、田园、四季分明、生活感。
|
||||
- 色彩与氛围:暖土绿基底+季节信号色整体切换;治愈不压抑;无锐利科技感、无阴暗元素。
|
||||
- MVP 美术边界:首期 3 区域 tileset、8 位 NPC(行走+立绘)、约 120 物品图标、玩家换装 5 层;不做 19 层全量换装与全区域。
|
||||
|
||||
## 5. 游戏支柱(3 条)←设计锚点提炼
|
||||
| 支柱 | 玩家感受 | 实现机制 |
|
||||
|---|---|---|
|
||||
| 自己的节奏 | "今天想干嘛就干嘛,明天一切更顺手。" | 自由日程+时间体力预算;无失败结局 |
|
||||
| 今天的选择让明天更从容 | "升级工具、攒钱扩建是有意义的。" | 长期投资线:工具升级/技能/设施 |
|
||||
| 社区让独居变成归属 | "镇上的人在等我。" | NPC 关系/任务/社区修复目标 |
|
||||
|
||||
## 6. 核心循环(5 步)←锚点循环位展开
|
||||
安排一天的时间与体力 → 农/采/钓/矿/战/社交任选组合 → 获得资源·金钱·经验·关系 → 投资工具·设施·种子·物品 → 解锁更高效或更丰富的活动。
|
||||
|
||||
## 7. 目标用户 ←概念层§5
|
||||
牧场物语系慢节奏成长玩家+动森式"无压力日常"需求;单人、可反复、每次一至数个游戏日;不要求预先掌握复杂数值。
|
||||
|
||||
## 8. 平台事实 ←运行时注入,禁改
|
||||
自包含 Web · 双视口(桌面/移动)· 键鼠/触屏双输入 · 本地 HTTP 预览(金样项目按 HTML 演示路线锁定)。
|
||||
|
||||
## 9. MVP 系统(5 个)←概念层"最小闭环粗清单"
|
||||
| 系统 | 最小功能 | 为什么必须有 | 验证方法 |
|
||||
|---|---|---|---|
|
||||
| 时间与日程 | 时钟/日终结算/季节天气 | 全局节拍器 | 一个游戏日全流程可完成并结算 |
|
||||
| 体力与状态 | 单池体力/昏倒惩罚 | 一切取舍的成本源 | 玩家主动在体力耗尽前收手 |
|
||||
| 农场经营 | 锄种浇收+加工队列 | 核心产出与规划场 | "买种→收获→出售"闭环成立 |
|
||||
| 物品与制作 | item_id/背包/配方解锁 | 资源身份与转化 | 拾取/堆叠/制作全链无回翻 GDD |
|
||||
| 经济与商店 | 基准价+价差+出货箱 | 投资回报换算 | 第 4 日现金流回正(前五日验算) |
|
||||
|
||||
## 10. 制作边界 ←概念层"不是什么"表
|
||||
不做:硬核生存(无饥饿/债务/死亡惩罚);效率至上的工厂经营;以战斗为核心的动作游戏;剧情驱动的任务链主线;多人竞争;无边界开放世界(区域小网络全步行可达)。
|
||||
|
||||
## 11. 创作者提示(先做与验证)←概念层"先做与验证"节
|
||||
- 先做:第 1 日循环(买种→播种→浇灌→收获→出售→日终结算)+一个可进入的矿井遭遇。
|
||||
- 暂不做:装备刷取、随机构筑、复杂剧情、节日全量、联机。
|
||||
- 这样验证:测试者玩完第 1 日后是否主动说"再玩一天";能否说出"明天要先做什么"。
|
||||
- 达标再扩展:玩家能自述明日计划后,才加社交深度与矿井分层。
|
||||
|
||||
## 12. 决定状态与原型验证项 ←台账投影(decision-log.md v3)
|
||||
- 已确认(confirmed):定调 D-01 / 单人 D-02 / 战斗伴生 D-03 / 日目标自设 D-07 / 活动统一接口 D-11 / 战斗节奏判定 D-13。
|
||||
- 技术代决(auto_decided,可一键翻案):D-04 时间片 / D-05 分区域切换 / D-06 28 日季 / D-09 营业条件 / D-10 出货箱日终 / D-12 工具占用 / D-16 矿井生成 P2 / D-17 换装 5 层 / D-18 品质三档(推翻 D-08 两档)。
|
||||
- 等拍板(pending_user):D-14 体力战斗是否共享单池(暂按共享实现);D-15 背包格子/重量(暂按格子制,B 级阻断存档结构)。
|
||||
- 待原型(prototype_pending):D-13b 战斗判定窗口手感(前摇帧数/450ms 无敌帧基准)。
|
||||
- 默认兜底(default_pending):D-19 天气权重数值(默认已入表,带 designer_note)。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user