Compare commits

..

29 Commits

Author SHA1 Message Date
k88936 f7edc9d0ee 区分 Raw 图片 PNG 资源限制错误
Project CI / Frontend tests (pull_request) Successful in 4m43s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Native shell tests (pull_request) Failing after 5m14s
将有效但超出解码限制的 PNG 映射为明确的 400 提示

补充超大合法 PNG 的回归测试
2026-09-09 11:04:44 +08:00
k88936 f60771dd00 提前检查 Raw 图片请求预算
在构造 multipart 表单前校验请求 deadline

避免预算耗尽时复制输入图片并缩短耗时统计范围
2026-09-09 10:58:48 +08:00
k88936 f7cccd8592 补充 Raw 图片客户端错误上下文
为 HTTP 客户端构造失败统一保留操作上下文

补充配置错误文案回归测试
2026-09-09 10:57:04 +08:00
k88936 5ee2d940c1 修正 Raw 图片尺寸校验错误语义
尺寸校验失败改用请求参数无效语义

补充错误文案回归测试
2026-09-09 10:55:36 +08:00
k88936 c75a7725ce Merge remote-tracking branch 'origin/master' into feat/expose-image2edit
Project CI / Repository checks (pull_request) Successful in 2m51s
Project CI / Native shell tests (pull_request) Failing after 4m9s
Project CI / Backend tests (pull_request) Failing after 4m58s
Project CI / Frontend tests (pull_request) Successful in 3m46s
2026-09-09 10:42:22 +08:00
k88936 3a95715990 同步 Raw 图片编辑技术方案
Project CI / Native shell tests (pull_request) Failing after 6m36s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 4m22s
记录解码资源上限和阻塞线程策略
补充输出格式回退与成功追踪约定
2026-09-08 23:41:27 +08:00
k88936 ed86963de2 修正 Raw 图片追踪结果类型
明确输出数据集合类型
保持 API 编译检查通过
2026-09-08 23:38:25 +08:00
k88936 65e73eee02 补充 Raw 图片生成成功追踪
记录成功任务与输出数量
完善上游响应和失败阶段日志
2026-09-08 23:25:47 +08:00
k88936 6068eeda83 补充 Raw 图片编辑请求日志
记录上游响应状态与耗时
记录传输失败阶段和结构化上下文
2026-09-08 23:22:56 +08:00
k88936 0e244c64ea 统一 GPT Image 2 尺寸常量
集中维护像素边界和对齐规则
供生成与编辑路径共同引用
2026-09-08 22:11:35 +08:00
k88936 ad7c059603 遵守 Raw 图片请求截止时间
发送前裁剪有效超时预算
预算耗尽时提前返回结构化错误
2026-09-08 21:54:22 +08:00
k88936 55d2a393d1 完善 Raw 图片尺寸错误类型
为公开错误实现 Error trait
支持通用错误链处理
2026-09-08 21:52:15 +08:00
k88936 e7f66ce950 修正 Raw 图片传输失败阶段标记
区分请求发送和响应读取失败
让错误详情准确反映故障阶段
2026-09-08 20:03:13 +08:00
k88936 35dcfea3ef 修正 Raw 图片输出格式回退
忽略空值后再使用载荷级格式
补充空字符串和 null 回归测试
2026-09-08 19:56:41 +08:00
k88936 cea8bf77fa 减少 Raw 图片编辑请求拷贝
移动 multipart 选项字段而非克隆
降低掩码和字符串的峰值内存
2026-09-08 19:52:13 +08:00
k88936 0136bfe564 移出 Raw 图片同步解码
将请求预校验放入阻塞线程
避免占用 Tokio 异步工作线程
2026-09-08 19:48:58 +08:00
k88936 22785b4d7b 限制 Raw 图片解码资源
为 PNG 解码设置边长和分配上限
避免压缩图片膨胀导致内存耗尽
2026-09-08 19:45:02 +08:00
k88936 a86a2ee4d2 简化 Raw 图片编辑响应类型
移除无必要的 JSON Value 转换
保持响应结构由类型系统校验
2026-09-08 19:32:55 +08:00
k88936 b22eae2726 收紧 Raw 图片编辑输入契约
Project CI / Frontend tests (pull_request) Successful in 3m51s
Project CI / Native shell tests (pull_request) Successful in 20m20s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Backend tests (pull_request) Failing after 14s
新增 3840 边长、16 像素对齐、3:1 比例和总像素范围校验

要求 image 与 mask 为可完整解码的 PNG 并在扣费前拒绝非法输入

将 Raw 路由 JSON body limit 提升至 64 MiB

同步 Raw 图片编辑技术方案和边界测试
2026-09-08 18:18:58 +08:00
k88936 35d0db6377 修复 Raw 图片失败审计与响应格式
Project CI / Frontend tests (pull_request) Successful in 6m7s
Project CI / Native shell tests (pull_request) Successful in 19m41s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 9s
为 Raw provider 失败分支补齐结构化 failure audit 并接入 api-server 记录链

使用 VectorEngine 响应 output_format 生成返回图片 MIME 与扩展名

补充响应格式和审计字段定向测试并同步技术方案
2026-09-08 16:19:29 +08:00
k88936 f087ba3e2b 隔离 Raw 图片代理实现
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 36m40s
Project CI / Native shell tests (pull_request) Successful in 54m1s
移除对现有图片编辑 client 与 curl transport 的复用

将 Raw GPT Image 2 请求、multipart 构造和响应解析收敛到独立文件

保持现有 editor 图片链路代码不变
2026-09-08 13:54:31 +08:00
k88936 7fc1b841be 收口 Raw 图片 base64 校验错误
客户端仅返回稳定的 base64 格式错误提示

补充测试避免泄露解码器内部诊断细节
2026-09-08 12:30:55 +08:00
k88936 a576fdcc37 移除未使用的 multipart 请求包装函数
无选项测试直接调用统一传输入口

避免生产构建保留未使用函数告警
2026-09-08 12:29:59 +08:00
k88936 4dcf38715b 补充图片编辑选项 multipart 测试
覆盖质量、背景、输出格式与 mask 文件字段

断言文件名、MIME 类型和字节内容均正确发送
2026-09-08 12:21:11 +08:00
k88936 d1f9f4e1ef 减少图片编辑 multipart 字节复制
将已克隆的选项按值传入阻塞传输函数

构造 mask 与参考图表单时直接移动字节缓冲
2026-09-08 12:16:00 +08:00
k88936 5836aaec26 简化图片编辑 multipart 请求分支
统一通过带选项的 multipart 传输函数发送请求

移除冗余包装函数调用与未使用导入
2026-09-08 12:10:56 +08:00
k88936 18d60feb04 补充 Raw 图片参数预检查
在计费前校验质量、背景和输出格式

规范化可选参数并保持单图请求契约
2026-09-08 10:40:09 +08:00
k88936 4e1316e272 实现 Raw GPT Image 2 图片编辑代理
新增受 Bearer 保护的 /api/raw/v1/images/edit JSON 路由

按单图 image 与可选 mask 转发 GPT Image 2 参数

固定 model 与 n 并返回仅含 data[].b64_json 的响应

复用钱包计费与退款边界并补充单元测试
2026-09-08 10:36:48 +08:00
k88936 302b4addae 补充 Raw GPT Image 2 代理方案
Project CI / Native shell tests (pull_request) Successful in 55m40s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 23m43s
明确 JSON 请求与 data 数组响应

记录预检查和钱包事务边界

拆分 api-server 与 platform-image 文件职责
2026-09-07 16:33:34 +08:00
284 changed files with 37015 additions and 25402 deletions
-31
View File
@@ -232,37 +232,6 @@ jobs:
done done
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 - name: Run native shell gates
run: npm run check:native-shells run: npm run check:native-shells
@@ -16,5 +16,8 @@
"maxRetries": 2, "maxRetries": 2,
"retryBackoffMs": 500 "retryBackoffMs": 500
}, },
"agentLlm": {} "agentLlm": {},
"planning": {
"capabilityEnabled": true
}
} }
+2
View File
@@ -19,6 +19,8 @@
"config": "node scripts/game-creator-config-wizard.mjs", "config": "node scripts/game-creator-config-wizard.mjs",
"test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open", "test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open",
"test:chat:manual": "node scripts/agent-swarm-test-chat.mjs", "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": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs", "agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs", "agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
@@ -36,6 +36,8 @@ export const ungeneratedGameEntryMarker =
'还没有生成游戏。回到聊天输入创意并确认生成后'; '还没有生成游戏。回到聊天输入创意并确认生成后';
export const defaultRealSwarmTestTask = export const defaultRealSwarmTestTask =
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。'; '制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
export const defaultRealSwarmPlanTask =
'我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
export const swarmTurnReportPrefix = '[turn.report] '; export const swarmTurnReportPrefix = '[turn.report] ';
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1'; export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
@@ -138,10 +140,15 @@ export const usage = `用法:
--keep-project 保留自动创建的一次性项目 --keep-project 保留自动创建的一次性项目
--no-open 手工模式启动预览但不自动打开浏览器 --no-open 手工模式启动预览但不自动打开浏览器
--task <需求> 通过 manual 入口非交互提交自定义需求 --task <需求> 通过 manual 入口非交互提交自定义需求
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时 --plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM --dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助 -h, --help 显示帮助
`;
环境变量:
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approverevise/reject 必须
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
function readOptionValue(args, index, option) { function readOptionValue(args, index, option) {
const value = args[index + 1]?.trim(); const value = args[index + 1]?.trim();
@@ -170,6 +177,7 @@ export function parseSwarmTestArguments(args) {
keepProject: false, keepProject: false,
openBrowser: true, openBrowser: true,
task: null, task: null,
plan: false,
timeoutMinutes: null, timeoutMinutes: null,
dryRun: false, dryRun: false,
help: false, help: false,
@@ -194,6 +202,8 @@ export function parseSwarmTestArguments(args) {
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符'); if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
options.task = task; options.task = task;
index += 1; index += 1;
} else if (argument === '--plan') {
options.plan = true;
} else if (argument === '--timeout-minutes') { } else if (argument === '--timeout-minutes') {
if (options.timeoutMinutes !== null) { if (options.timeoutMinutes !== null) {
throw new Error('--timeout-minutes 只能指定一次'); throw new Error('--timeout-minutes 只能指定一次');
@@ -212,11 +222,16 @@ export function parseSwarmTestArguments(args) {
} }
export function shouldStartPersistentPreview(options) { export function shouldStartPersistentPreview(options) {
return !options.task; // 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
return !options.task && !options.plan;
} }
export function resolveSwarmTestTimeoutMs(options) { 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; return minutes === null ? null : minutes * 60_000;
} }
@@ -964,12 +979,25 @@ export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1; return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
} }
// GDD 审批位不能等 CLI 退出之后再处理:Run 停在这里时状态是 waiting-for-user-input
// 而 swarm CLI 恰好把这个状态算作「本轮还在跑」,turn 永远不 settleCLI 也就永远
// 不退出。所以审批必须在 CLI 还活着的时候并发做完,让 Run 自己继续跑到收束。
// 这一句是 PlanGddCompletionBlockerKind::AwaitingApprovalDecision 专有的投影文案,
// 另外三个 blocked 子状态都不会打出它;即便认错了,真正的判据也是随后那次
// --plan-gdd-status,没有待决定审批时不会有任何写入。
const planGddApprovalWaitPattern = /等待 Fast GDD 审批决定/u;
export function swarmOutputAwaitsPlanGddApproval(line) {
return planGddApprovalWaitPattern.test(line);
}
async function runTaskCargo( async function runTaskCargo(
cliArguments, cliArguments,
task, task,
setActiveChild, setActiveChild,
timeoutMs, timeoutMs,
autoPilot = false, autoPilot = false,
onPlanGddApprovalWait = null,
) { ) {
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
stdio: ['pipe', 'pipe', 'inherit'], stdio: ['pipe', 'pipe', 'inherit'],
@@ -981,6 +1009,25 @@ async function runTaskCargo(
let taskSubmitted = false; let taskSubmitted = false;
let promptsSeen = 0; let promptsSeen = 0;
let sittingAtPrompt = false; 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.setEncoding('utf8');
child.stdout.on('data', (chunk) => { child.stdout.on('data', (chunk) => {
process.stdout.write(chunk); process.stdout.write(chunk);
@@ -993,6 +1040,13 @@ async function runTaskCargo(
reportLines.push(normalizedLine); reportLines.push(normalizedLine);
settled = true; settled = true;
} }
if (
onPlanGddApprovalWait &&
!planGddApprovalStarted &&
swarmOutputAwaitsPlanGddApproval(normalizedLine)
) {
startPlanGddApproval();
}
} }
if (!autoPilot || child.stdin.writableEnded) return; if (!autoPilot || child.stdin.writableEnded) return;
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine); const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
@@ -1031,9 +1085,12 @@ async function runTaskCargo(
if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) { if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) {
reportLines.push(normalizedPendingLine); reportLines.push(normalizedPendingLine);
} }
await planGddApprovalPromise;
if (planGddApprovalError) throw planGddApprovalError;
return { return {
...result, ...result,
turnReportOutput: reportLines.join('\n'), turnReportOutput: reportLines.join('\n'),
planGddApproval,
}; };
} finally { } finally {
setActiveChild(null); setActiveChild(null);
@@ -1723,6 +1780,196 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
return inspection; 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) { export async function hasConfiguredEditorApiKey(configDir) {
let configured = false; let configured = false;
for (const fileName of [configFileName, localConfigFileName]) { for (const fileName of [configFileName, localConfigFileName]) {
@@ -1875,7 +2122,13 @@ export async function runSwarmTestChat(options) {
const setActiveChild = (child) => { const setActiveChild = (child) => {
activeChild = child; activeChild = child;
}; };
// GDD 审批要和 swarm CLI 并发跑,两者不能共用 activeChild 这一个槽位:审批子进程
// 结束时的 setActiveChild(null) 会把 CLI 从槽里抹掉,Ctrl-C 就杀不到它了。
const concurrentChildren = new Set(); const concurrentChildren = new Set();
const setConcurrentChild = (child) => {
if (child) concurrentChildren.add(child);
else concurrentChildren.clear();
};
const stopRequested = () => receivedSignal !== null; const stopRequested = () => receivedSignal !== null;
const handleSignal = (signal) => { const handleSignal = (signal) => {
const repeatedSignal = receivedSignal !== null; const repeatedSignal = receivedSignal !== null;
@@ -1953,10 +2206,11 @@ export async function runSwarmTestChat(options) {
); );
} }
console.log('LLM 配置已就绪。'); console.log('LLM 配置已就绪。');
const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
console.log( console.log(
options.task options.task
? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n' ? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
: '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n', : `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
); );
phase = 'chat'; phase = 'chat';
@@ -1966,7 +2220,8 @@ export async function runSwarmTestChat(options) {
runtimeConfig.path, runtimeConfig.path,
'--swarm-chat', '--swarm-chat',
'--init', '--init',
'--autonomous-game-build', // 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
options.plan ? '--plan' : '--autonomous-game-build',
project.path, project.path,
]; ];
let chat; let chat;
@@ -1979,6 +2234,15 @@ export async function runSwarmTestChat(options) {
timeoutDeadline === null timeoutDeadline === null
? null ? null
: Math.max(1, timeoutDeadline - Date.now()), : Math.max(1, timeoutDeadline - Date.now()),
options.plan,
options.plan
? () =>
settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
)
: null,
) )
: await runInteractiveCargo(chatArguments, setActiveChild); : await runInteractiveCargo(chatArguments, setActiveChild);
} catch (error) { } catch (error) {
@@ -1994,6 +2258,32 @@ export async function runSwarmTestChat(options) {
if (options.task) { if (options.task) {
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput); 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( const requireEditorImages = await hasConfiguredEditorApiKey(
runtimeConfig.path, runtimeConfig.path,
); );
@@ -114,7 +114,6 @@ const allowedUncalledTauriCommands = [
'open_game_creator_launcher_window', 'open_game_creator_launcher_window',
'open_game_creator_workspace_window', 'open_game_creator_workspace_window',
'read_direct_project_conversation', 'read_direct_project_conversation',
'reset_design_agent_session',
'stop_local_game_preview_if_matches', 'stop_local_game_preview_if_matches',
'start_game_creator_external_mcp', 'start_game_creator_external_mcp',
'stop_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'); throw new Error('AI game creator shell identifier drifted');
} }
const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
};
const expectedBundledCodexResources = { const expectedBundledCodexResources = {
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe', 'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe': 'resources/codex/win-x64/bin/codex-code-mode-host.exe':
@@ -1304,33 +1300,16 @@ const expectedBundledCodexResources = {
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md', 'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json', 'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
}; };
assert.deepEqual( if (tauriConfig.bundle?.resources !== undefined) {
tauriConfig.bundle?.resources, throw new Error(
expectedBundledDesignAgentResources, 'AI game creator shell base Tauri config must not require Windows-only Codex resources',
'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',
);
}
} }
assert.deepEqual( assert.deepEqual(
windowsTauriConfig.bundle?.resources, windowsTauriConfig.bundle?.resources,
expectedBundledCodexResources, expectedBundledCodexResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set', '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) { if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
throw new Error( throw new Error(
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory', 'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
@@ -1726,16 +1705,10 @@ if (
runtimeConfigSetupStart === -1 || runtimeConfigSetupStart === -1 ||
runtimeConfigSetupEnd === -1 || runtimeConfigSetupEnd === -1 ||
!runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') || !runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') ||
!runtimeConfigSetupSource.includes('setup_log.fail(') || !runtimeConfigSetupSource.includes('append_bounded_diagnostic_line(') ||
!runtimeConfigSetupSource.includes( !runtimeConfigSetupSource.includes(
'startup.appdata.configure.failed details={details}', '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( throw new Error(
'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures', 'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures',
@@ -722,7 +722,7 @@ function runAgent() {
stderr += chunk.toString(); stderr += chunk.toString();
}); });
child.on('error', reject); child.on('error', reject);
child.on('close', (code, signal) => { child.on('close', (code) => {
const output = `${stdout}${stderr}`; const output = `${stdout}${stderr}`;
if (previewReadError) { if (previewReadError) {
reject(previewReadError); reject(previewReadError);
@@ -740,24 +740,13 @@ function runAgent() {
previewDom, previewDom,
}); });
} else { } else {
reject( reject(new Error(output || `agent run exited with ${code}`));
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)}`,
),
);
} }
}); });
}); });
} }
async function seedLocalAsset() { 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, 'assets/uploads'), { recursive: true });
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true }); await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
await seedConversationContext(); 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 { existsSync, readdirSync, readFileSync } from 'node:fs';
import http from 'node:http'; import http from 'node:http';
import net from 'node:net'; import net from 'node:net';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import {
normalizeWindowsPath,
parseWindowsProcessSnapshot,
stopWindowsProcessTree,
stopWindowsWorktreeProcesses,
} from '../../../scripts/dev-windows-process.mjs';
import { import {
agcVitePortEnvKey, agcVitePortEnvKey,
readAgcDevEndpoint, readAgcDevEndpoint,
@@ -21,10 +15,6 @@ import {
const appRoot = fileURLToPath(new URL('..', import.meta.url)); const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..'); const repoRoot = resolve(appRoot, '../..');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json'); const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const apiServerExePath = resolve(
repoRoot,
'server-rs/target/debug/api-server.exe',
);
const defaultApiTarget = const defaultApiTarget =
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082'; process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
const backendDatabase = 'genarrative-game-creator-dev'; const backendDatabase = 'genarrative-game-creator-dev';
@@ -170,184 +160,19 @@ function readBackendServiceFailure(
return null; 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({ async function isBackendReady({
state = readJson(devStackStatePath), state = readJson(devStackStatePath),
isReady = isHttpReady, isReady = isHttpReady,
verifyOwnership = verifyAgcBackendOwnership,
onOwnershipRejected = null,
} = {}) { } = {}) {
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } = const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
resolveBackendTargetsFromState(state, { resolveBackendTargetsFromState(state, {
requireAgcBackend: true, 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 ( return (
hasMatchingBackend &&
Boolean(apiUrl) &&
Boolean(spacetimeUrl) &&
Boolean(bgfilterWorkerUrl) &&
(await isReady(`${apiUrl}/healthz`)) && (await isReady(`${apiUrl}/healthz`)) &&
(await isReady(`${spacetimeUrl}/v1/ping`)) && (await isReady(`${spacetimeUrl}/v1/ping`)) &&
(await isReady(`${bgfilterWorkerUrl}/readyz`)) (await isReady(`${bgfilterWorkerUrl}/readyz`))
@@ -660,18 +485,14 @@ async function terminateChildTree(
return { stopped: true, forced: false }; return { stopped: true, forced: false };
} }
const result = await taskkillImpl(child.pid); const result = await taskkillImpl(child.pid);
const taskkillStopped = return {
!result?.timedOut && stopped:
!result?.error && !result?.timedOut &&
[0, 128].includes(result?.code ?? 0); !result?.error &&
if (taskkillStopped) { [0, 128].includes(result?.code ?? 0),
return { stopped: true, forced: true, result }; 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 };
} }
const processGroupId = childLifecycles.get(child)?.processGroupId; const processGroupId = childLifecycles.get(child)?.processGroupId;
@@ -721,29 +542,15 @@ async function waitForBackendReady(
backendChild, backendChild,
timeoutMs = 600_000, timeoutMs = 600_000,
{ {
checkBackendReady = (onOwnershipRejected) => checkBackendReady = isBackendReady,
isBackendReady({ onOwnershipRejected }),
readState = () => readJson(devStackStatePath), readState = () => readJson(devStackStatePath),
resolveTargets = readBackendTargets, resolveTargets = readBackendTargets,
} = {}, } = {},
) { ) {
const initialStateUpdatedAt = readState()?.updatedAt ?? ''; const initialStateUpdatedAt = readState()?.updatedAt ?? '';
const startedAt = Date.now(); const startedAt = Date.now();
let lastOwnershipReason = '';
while (Date.now() - startedAt < timeoutMs) { while (Date.now() - startedAt < timeoutMs) {
if ( if (await checkBackendReady()) {
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)})。`,
);
})
) {
return resolveTargets(); return resolveTargets();
} }
const state = readState(); const state = readState();
@@ -769,14 +576,7 @@ async function waitForBackendReady(
async function ensureBackend({ async function ensureBackend({
onBackendChild = () => {}, onBackendChild = () => {},
checkBackendReady = () => checkBackendReady = isBackendReady,
isBackendReady({
onOwnershipRejected(ownership) {
console.warn(
`[ai-game-creator-shell] 端口上的配套后端不属于当前工作树(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)}),改为启动本工作树自己的后端。`,
);
},
}),
resolveTargets = readBackendTargets, resolveTargets = readBackendTargets,
spawnBackend = () => spawnBackend = () =>
spawnChild( spawnChild(
@@ -856,33 +656,15 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
async function main() { async function main() {
let backendChild = null; let backendChild = null;
let startedBackend = false;
let viteChild = null; let viteChild = null;
let shutdownSignal = ''; let shutdownSignal = '';
const signalHandlers = new Map(); 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']) { for (const signal of ['SIGINT', 'SIGTERM']) {
const handler = () => { const handler = () => {
shutdownSignal = signal; shutdownSignal = signal;
stopChild(viteChild, signal); stopChild(viteChild, signal);
stopChild(backendChild, signal); stopChild(backendChild, signal);
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
sweepStartedBackend();
}; };
signalHandlers.set(signal, handler); signalHandlers.set(signal, handler);
process.on(signal, handler); process.on(signal, handler);
@@ -901,7 +683,6 @@ async function main() {
}, },
}); });
backendChild = backend.backendChild; backendChild = backend.backendChild;
startedBackend = Boolean(backendChild);
if (shutdownSignal) { if (shutdownSignal) {
throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`); throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`);
} }
@@ -935,7 +716,6 @@ async function main() {
terminateChildTree(viteChild), terminateChildTree(viteChild),
terminateChildTree(backendChild), terminateChildTree(backendChild),
]); ]);
sweepStartedBackend();
for (const [signal, handler] of signalHandlers) { for (const [signal, handler] of signalHandlers) {
process.off(signal, handler); process.off(signal, handler);
} }
@@ -952,25 +732,20 @@ function isDirectModuleExecution() {
export { export {
ensureBackend, ensureBackend,
formatChildFailure, formatChildFailure,
formatOwnerLabel,
isAiGameCreatorServer, isAiGameCreatorServer,
isBackendReady, isBackendReady,
isDirectModuleExecution, isDirectModuleExecution,
isProcessGroupAlive, isProcessGroupAlive,
isWorktreeApiServerOwner,
isWorktreeSpacetimeOwner,
preflightExistingVite, preflightExistingVite,
readBackendServiceFailure, readBackendServiceFailure,
readChildFailure, readChildFailure,
readExistingViteServer, readExistingViteServer,
readLinuxProcessGroupAlive, readLinuxProcessGroupAlive,
readWindowsPortOwnerIdentities,
resolveBackendTargetsFromState, resolveBackendTargetsFromState,
runWindowsTaskkill, runWindowsTaskkill,
spawnChild, spawnChild,
stopChild, stopChild,
terminateChildTree, terminateChildTree,
verifyAgcBackendOwnership,
waitForBackendReady, waitForBackendReady,
waitForChildTermination, waitForChildTermination,
}; };
@@ -20,10 +20,6 @@ import {
const appRoot = fileURLToPath(new URL('..', import.meta.url)); const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..'); const repoRoot = resolve(appRoot, '../..');
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js'); const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
const AGC_DESIGN_DEBUG_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) { function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
const args = [...argv]; const args = [...argv];
@@ -112,11 +108,7 @@ async function runTauriDev(
if (!prepared || shutdownSignal) return 1; if (!prepared || shutdownSignal) return 1;
const tauriArguments = buildTauriArguments(argv, endpoint.url); const tauriArguments = buildTauriArguments(argv, endpoint.url);
child = spawnCli(tauriArguments, { child = spawnCli(tauriArguments, {
env: { env: withAgcDevEndpointEnv(endpoint),
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
}); });
const childResult = waitForCli(child); const childResult = waitForCli(child);
const outcome = await Promise.race([ const outcome = await Promise.race([
@@ -165,13 +157,7 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
const frontend = spawnChild( const frontend = spawnChild(
process.platform === 'win32' ? 'npm.cmd' : 'npm', process.platform === 'win32' ? 'npm.cmd' : 'npm',
['run', 'agc:serve'], ['run', 'agc:serve'],
{ { cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
cwd: repoRoot,
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
},
); );
onChild(frontend); onChild(frontend);
console.log( console.log(
@@ -74,14 +74,6 @@ codegen-units = 256
lto = "off" lto = "off"
incremental = true 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] [profile.test]
opt-level = 0 opt-level = 0
debug = 1 debug = 1
@@ -43,6 +43,7 @@ struct PromptCompositions {
/// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、 /// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、
/// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段 /// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段
/// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。 /// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。
supervisor_plan: Vec<String>,
supervisor_chat: SupervisorChatComposition, supervisor_chat: SupervisorChatComposition,
} }
@@ -98,6 +99,10 @@ struct ProviderFragments {
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AgentCatalog { struct AgentCatalog {
supervisor: AgentGroup, supervisor: AgentGroup,
/// 立项策划子 Agent。与 `supervisor` 平级、**不进 `groups`**`specialist_nodes`
/// 只从 `groups[].roles[]` 派生,因此它不参与 `build.rs` 与种子 DAG 的一致性
/// 校验,「做游戏」的 16 任务 DAG 一行不动。详见技术方案第 3.1 节。
planning: AgentGroup,
groups: Vec<AgentGroup>, groups: Vec<AgentGroup>,
} }
@@ -226,6 +231,12 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
&sections, &sections,
&["$base", "$visualContract"], &["$base", "$visualContract"],
)?; )?;
validate_composition(
"supervisorPlan",
&manifest.compositions.supervisor_plan,
&sections,
&["$header"],
)?;
validate_section_reference( validate_section_reference(
&manifest.compositions.supervisor_chat.identity, &manifest.compositions.supervisor_chat.identity,
&sections, &sections,
@@ -288,6 +299,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
.supervisor .supervisor
.roles .roles
.iter() .iter()
.chain(manifest.agent_catalog.planning.roles.iter())
.chain( .chain(
manifest manifest
.agent_catalog .agent_catalog
@@ -336,6 +348,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
.runtime .runtime
.iter() .iter()
.chain(manifest.compositions.supervisor.iter()) .chain(manifest.compositions.supervisor.iter())
.chain(manifest.compositions.supervisor_plan.iter())
.filter(|item| !item.starts_with('$')) .filter(|item| !item.starts_with('$'))
.cloned() .cloned()
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
@@ -408,6 +421,14 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
{ {
register("composition supervisor", section); 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.identity", identity);
register( register(
"composition supervisorChat.finalReply", "composition supervisorChat.finalReply",
@@ -436,9 +457,19 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
"composition supervisor", "composition supervisor",
"composition supervisorChat.identity", "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 { for (section, section_owners) in owners {
if section_owners.len() > 1 if section_owners.len() > 1
&& !(section == identity && section_owners == allowed_identity_owners) && !(section == identity && section_owners == allowed_identity_owners)
&& section_owners != allowed_plan_runtime_owners
&& section_owners != allowed_plan_supervisor_owners
{ {
return Err(format!( return Err(format!(
"Prompt section 跨语义所有者复用:{section} -> {section_owners:?}" "Prompt section 跨语义所有者复用:{section} -> {section_owners:?}"
@@ -675,11 +706,17 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
if catalog.supervisor.roles.len() != 1 { if catalog.supervisor.roles.len() != 1 {
return Err("agentCatalog.supervisor 必须且只能包含一个 role".to_string()); 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() { if catalog.groups.is_empty() {
return Err("agentCatalog.groups 不能为空".to_string()); return Err("agentCatalog.groups 不能为空".to_string());
} }
let mut group_brief_names = BTreeSet::new(); 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()) { if !group_brief_names.insert(group.brief_path_name.as_str()) {
return Err(format!( return Err(format!(
"agent group briefPathName 重复:{}", "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 { for group in &catalog.groups {
let generated = rust_identifier(&group.id); let generated = rust_identifier(&group.id);
if !generated if !generated
@@ -713,6 +753,12 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
&mut task_ids, &mut task_ids,
&mut tool_ids, &mut tool_ids,
)?; )?;
validate_agent_group(
&catalog.planning,
&mut group_ids,
&mut task_ids,
&mut tool_ids,
)?;
for group in &catalog.groups { for group in &catalog.groups {
validate_agent_group(group, &mut group_ids, &mut task_ids, &mut tool_ids)?; 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", "RUNTIME_PROMPT_SUPERVISOR_COMPOSITION",
&manifest.compositions.supervisor, &manifest.compositions.supervisor,
)); ));
output.push_str(&render_string_slice_const(
"RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION",
&manifest.compositions.supervisor_plan,
));
output.push_str(&format!( output.push_str(&format!(
"pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n", "pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n",
rust_literal(&manifest.compositions.supervisor_chat.identity), 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", "static PROJECT_SUPERVISOR_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
render_group_value(&catalog.supervisor, "&PROJECT_SUPERVISOR_AGENT_ROLES") 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 { for group in &catalog.groups {
let roles_name = format!("{}_AGENT_ROLES", rust_identifier(&group.id)); let roles_name = format!("{}_AGENT_ROLES", rust_identifier(&group.id));
output.push_str(&render_role_array(&roles_name, &group.roles)); output.push_str(&render_role_array(&roles_name, &group.roles));
@@ -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`
## 决定状态
- **初始需求**confirmeduser\_freeform,第 0 轮):用户提出制作原创、可落地的2D坦克大战MVP。
- **坦克对战的胜负目标**confirmeduser\_option,第 1 轮):采用区域争夺作为每局主要胜负目标。
- **区域争夺的占领方式**confirmeduser\_option,第 2 轮):采用少量多节点轮换占领,迫使双方在路线间转移争夺。
- **轮换节点的结束规则**confirmeduser\_option,第 3 轮):控制当前节点持续得分,任一方先达到目标分即获胜。
- **目标用户**default\_pendingdefault,第 0 轮):默认面向喜欢短局、直接操控、位置博弈和可重复挑战的休闲动作玩家。
- **美术方向**default\_pendingdefault,第 0 轮):默认采用风格化、轮廓清晰的原创战场与占位资产,优先验证可读性。
- **成长范围**default\_pendingdefault,第 0 轮):默认设置一条轻量成长线,仅提供少量战后选择,不影响单局闭环。
- **MVP内容边界**default\_pendingdefault,第 0 轮):默认首个版本只做单人对抗一名基础AI、单张地图和一套坦克配置。
## 一句话描述
驾驶原创装甲穿越掩体,在轮换能量节点间交火夺分,先达目标分者赢得一局紧凑的2D坦克区域争夺战。
## 游戏分类与美术
- 主类型:2D坦克区域争夺
- 融合类型:无
- 视觉类型:风格化俯视2D
- 关键词:轮廓清晰、几何掩体、弹道高亮、原创装甲
- 色彩氛围:冷青灰战场衬托橙蓝阵营高亮;节点被占领时产生清晰的环形脉冲,命中、受击和得分使用短促高对比反馈。
- MVP 美术边界:MVP 使用可复用的几何占位资产:一张俯视战场、两种原创装甲外形、节点与掩体模块;先保证阵营、弹道、占领状态在桌面和移动视口均清楚。
## 游戏支柱
### 移动夺势
- 玩家感受:始终知道下一处冲突在哪里,移动本身就是争夺优势而非赶路。
- 机制:节点位置与活跃状态持续改变,玩家必须在占领、转移、驻守和反攻之间做出即时选择。
- 决定状态:confirmed
### 掩体交火
- 玩家感受:每次探头、绕侧和开火都带来可读的风险回报。
- 机制:几何掩体阻挡弹道,玩家通过角度、射击窗口和绕行路线逼退对手,再进入节点控分。
- 决定状态:confirmed
### 清晰逆转
- 玩家感受:局势紧张但不迷惑,玩家理解自己为何领先或落后并愿意再开一局。
- 机制:目标分、活跃节点和控制状态持续可视化;落后方可通过夺回当前节点改变局势。
- 决定状态:confirmed
## 核心循环
1. 进入战场并观察当前活跃节点与敌方位置
2. 驾驶“棱虎机”在几何掩体间移动,瞄准并发射脉冲炮
3. 抵达活跃节点并在敌方干扰下完成占领或夺回
4. 控制节点持续积累“脉冲分”,节点轮换后转移路线
5. 先达到目标分的一方获胜,结算后可立即重开
## 目标用户
- 核心用户:偏好短局即时动作、方向操控、躲避射击和争夺空间的休闲玩家;可接受单人对抗基础AI。
- 偏好:喜欢规则一眼可懂、操作反馈直接、每局约十几分钟内完成,并能通过走位和时机而非复杂配装取胜。
- 单局时长:默认每局约10~20分钟;首个可玩闭环优先做到一局可完整开始、争夺、决胜和重开。
- 参考游戏:无
## Runtime 平台事实
- Runtimeself-contained-web
- 视口:desktop / mobile
- 输入:keyboard / touch
- 预览:local-http
## MVP 系统
### 坦克操控与射击
- 最小功能:提供八方向移动、旋转瞄准、单一脉冲炮射击、受击反馈与短暂失活重生,支持键盘和触控操作。
- 必要原因:直接构成坦克对战手感,也是区域争夺发生的主要冲突手段。
- 验证方式:试玩者能在一分钟内完成移动、瞄准、射击和躲入掩体,并能用射击驱离节点附近敌人。
- 决定状态:confirmed
### 轮换节点占领
- 最小功能:以少量节点组成单张战场,仅一个节点在任一时刻活跃;进入后按占优方推进占领,活跃节点轮换并提供清晰状态提示。
- 必要原因:落实用户确认的多节点轮换占领,并制造移动、停留和反攻决策。
- 验证方式:玩家无需额外说明即可找到活跃节点,读懂中立、己方和敌方控制状态,并在轮换后改变路线。
- 决定状态:confirmed
### 目标分胜负
- 最小功能:活跃节点由控制方持续获得脉冲分;任一方先达到目标分即胜,显示双方分数、当前节点和胜负结算。
- 必要原因:把占领行为闭合成清晰、可验证的单局目标。
- 验证方式:试玩者能预测哪方领先、理解如何逆转,并在达到目标分时明确知道对局结束。
- 决定状态:confirmed
### 俯视战场结构
- 最小功能:布置可绕行的几何掩体、节点路径与出生区域;掩体阻挡弹道并形成接近、驻守和侧袭路线。
- 必要原因:让位置博弈支撑区域争夺,避免节点规则沦为单纯站桩计分。
- 验证方式:观察玩家是否主动利用掩体接近节点或规避火力,而非只在开阔地互射。
- 决定状态:confirmed
### 基础对手AI
- 最小功能:提供一名基础AI对手:追踪活跃节点、靠近争夺、在射程内攻击并在失活后返回战场。
- 必要原因:在无多人条件下完成完整的对抗闭环并验证核心玩法。
- 验证方式:连续试玩中,AI应持续争夺节点并制造可理解的反攻机会,不出现长时间卡住或无目标游走。
- 决定状态:default\_pending
## 制作边界
- 多人联机与服务器
- 商城、赛季和复杂社交
- 开放世界与完整剧情
- 多地图、多武器树与复杂成长
- 可编辑关卡和排行榜
## 创作者提示
- 先做:先做单张紧凑俯视战场、玩家坦克、基础AI对手、移动瞄准射击、掩体碰撞、节点轮换、占领计分和胜负结算,确保一局可从开始玩到结束。
- 暂缓:暂缓多人联机、商城、服务器、开放世界、赛季、复杂社交、完整剧情、多武器树和多地图内容。
- 如何验证:用可操作原型观察玩家是否在首局主动驶向活跃节点、利用掩体交火、理解分数变化并完成一局;记录误读占领状态、无目标游走和胜负不明的情况。
- 何时扩展:仅当试玩者能无讲解理解活跃节点、占领状态和领先来源,并主动移动反攻时,再增加第二种坦克特性、第二张地图或轻量成长选择。
@@ -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)。
@@ -1,200 +0,0 @@
# 系统架构:《星露谷物语》
## 架构定位与目标
本阶段确定"哪些系统支撑一轮玩法",不展开单系统内部规则。
划分原则:将生活模拟 RPG 拆成职责清晰、可独立讨论的规则系统,同时保留少量跨系统入口,避免"每个功能都能互相调用"造成架构失控。系统划分服务于顶层循环:安排一天、执行活动、获得进展、投入成长、解锁新选择。
一句话架构:
> 玩家在有限的时间与体力下,通过农场、探索与社交三组活动系统产出资源与关系,经物品与经济系统转化为投资,由时间系统推进日终,把一天的成果变成下一天的选择。
变更记录:
- 2026-09-05:战斗与敌人系统定为伴生风险定位,深度刻意受限,不进入最小闭环核心链(依据:概念分析 D-03)。
## 系统地图
| 编号 | 系统 | 一句话职责 | 优先级 |
|---|---|---|---|
| S01 | 时间与日程 | 推进游戏时间、日期、季节、天气、营业时间、NPC 日程和日终结算 | P0 |
| S02 | 体力与状态 | 管理体力、负面状态、恢复、昏倒和行动成本 | P0 |
| S03 | 农场经营 | 管理土地、作物、畜牧、农场设施和生产状态 | P0 |
| S04 | 探索与地图 | 管理区域、出入口、可交互资源点、地图解锁和移动 | P0(基础) |
| S05 | 采集与钓鱼 | 管理野外采集、钓鱼活动、资源品质和获得物 | P1 |
| S06 | 战斗与敌人 | 管理矿区或危险区域中的战斗、伤害、敌人行为和战利品 | P1 |
| S07 | 物品、背包与制作 | 管理物品实例、堆叠、工具、装备、配方和制作队列 | P0 |
| S08 | 成长与技能 | 管理技能经验、等级、工具升级、职业选择和能力解锁 | P0(基础) |
| S09 | 经济与商店 | 管理货币、买卖、价格、商店库存、订单和资金流 | P0 |
| S10 | NPC 与关系 | 管理 NPC 日程、对话、好感度、礼物偏好和关系事件 | P1 |
| S11 | 任务与社区目标 | 管理任务状态、阶段目标、奖励、社区修复和区域解锁条件 | P1 |
| S12 | 事件与节日 | 管理季节事件、节日活动、条件触发和特殊奖励 | P1 |
支撑层(不拥有核心规则):
- 存档与进度系统:保存跨日、跨季节和跨阶段的持久状态。
- UI 与文本呈现层:展示状态、提供操作入口、呈现反馈与文本。
P0 段:
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
|---|---|---|---|---|
| S01 时间与日程 | 全局时钟与日终 | 各系统行动完成信号、日终触发 | 日期/季节/天气变化、日终结算、跨天 tick | 没有"一天",规划与取舍失去标尺 |
| S02 体力与状态 | 全局行动成本 | 各系统行动请求、食物与休息 | 体力变化、昏倒、状态效果 | 没有它,"想做的事多于做得到的"不成立 |
| S03 农场经营 | 核心产出与规划场 | 时间 tick、种子与工具、体力 | 作物畜产品、设施生产状态 | 概念核心承诺的载体 |
| S04 探索与地图 | 活动场景与空间约束 | 移动指令、区域解锁条件 | 位置、区域状态、资源点入口 | 没有空间结构,农/矿/镇一体失去意义 |
| S07 物品与制作 | 资源身份与转化 | 各系统获得物、配方请求 | 物品实例、制作结果 | 所有系统产出的公共语言 |
| S08 成长与技能 | 长期回报层 | 各活动经验提交 | 等级、能力与配方解锁 | 长期动机的最小载体 |
| S09 经济与商店 | 投资与回报换算 | 物品、金钱 | 价格、交易、库存 | 没有它,"变现 vs 投资"张力无载体 |
## 系统职责
| 系统 | 主要职责 | 不负责 → 移交谁 |
|---|---|---|
| S01 时间与日程 | 时间推进、日期、季节、天气、营业与日终 | 直接决定某项活动的奖励 → 各活动系统 |
| S02 体力与状态 | 行动消耗、恢复、昏倒、状态效果 | 农作物或敌人的具体配置 → S03/S06 |
| S03 农场经营 | 土地、作物、畜牧、设施生产 | 商店买卖规则和角色技能 → S09/S08 |
| S04 探索与地图 | 区域连接、进入条件、资源点位置、移动 | 具体掉落概率和战斗公式 → S05/S06 |
| S05 采集与钓鱼 | 采集和钓鱼行为、成功条件、获得物 | 物品价格和任务奖励 → S09/S11 |
| S06 战斗与敌人 | 战斗流程、敌人状态、伤害与战利品请求 | 角色长期成长和商店价格 → S08/S09 |
| S07 物品与制作 | 背包、物品、配方、制作与工具装备 | 物品最终经济价值的平衡目标 → S09 |
| S08 成长与技能 | 经验、等级、技能分支、能力解锁 | 单次行动的基础奖励 → 各活动系统 |
| S09 经济与商店 | 货币、交易、库存、订单、价格 | 任务剧情与 NPC 情感变化 → S10/S11 |
| S10 NPC 与关系 | 日程、互动、好感、关系事件 | 全局季节推进和商店库存 → S01/S09 |
| S11 任务与社区 | 目标、前置、奖励、社区进度和解锁 | NPC 的日常行为表现 → S10 |
| S12 事件与节日 | 周期事件、特殊流程和限定内容 | 常规日常行动的基础规则 → 各活动系统 |
职责说明:
### S01 时间与日程系统
负责一天制的节奏规则:什么时候推进日期、哪些系统收到跨天 tick、日终结算何时发生。它不负责奖励结算,也不负责作物成长规则——只负责"什么时候"和"谁被通知"。
### S06 战斗与敌人系统
负责战斗内状态、敌人行为与战利品请求。它不直接修改商店价格或 NPC 好感,不负责角色长期成长;战利品只提交请求,由 S07 物品系统入账。定位是矿井探索的风险与节奏变化,不是成长主轴(D-03)。
### S07 物品、背包与制作系统
负责物品身份、容器、堆叠与配方队列。它是全项目的公共语言层——任何系统的产出都以 `item_id` 入账;它不负责物品的经济价值平衡,价格只由 S09 维护。
## 依赖与数据流
```mermaid
flowchart TD
S1[S01 时间与日程] --> S2[S02 体力与状态]
S1 --> S3[S03 农场经营]
S1 --> S4[S04 探索与地图]
S1 --> S10[S10 NPC 与关系]
S1 --> S12[S12 事件与节日]
S3 --> S7[S07 物品与制作]
S4 --> S5[S05 采集与钓鱼]
S4 --> S6[S06 战斗与敌人]
S5 --> S7
S6 --> S7
S7 --> S9[S09 经济与商店]
S7 --> S8[S08 成长与技能]
S8 --> S7
S10 --> S11[S11 任务与社区]
S12 -.读取日期季节.-> S1
UI[UI 与文本呈现] -.读取状态.-> S1
UI -.读取状态.-> S3
UI -.读取状态.-> S7
SAVE[存档与进度] -.订阅持久状态.-> S1
```
```mermaid
flowchart LR
T[时间/体力] --> ACT[玩家行动]
ACT --> GAIN[物品·金钱·经验·关系·任务进度]
GAIN --> INV[制作·交易·升级·解锁]
INV --> NEW[新的行动选择]
NEW --> ACT
```
主要状态:
- 全局状态:日期、季节、天气、当前时间、已解锁区域、社区进度。
- 玩家状态:位置、体力、生命、技能等级、工具、装备、背包和金钱。
- 场景状态:土地、作物成长、设施生产、资源点、敌人和宝箱。
- 社会状态:NPC 位置、关系值、已触发事件、任务阶段和节日参与状态。
主数据归属规则:
- 规则文档描述"如何计算"和"何时发生";数据表描述"有哪些对象"和"每个对象的配置"。
- 系统之间通过稳定 ID 关联(物品 ID、NPC ID、区域 ID、任务 ID、配方 ID)。
- 任何系统都不复制另一系统的主数据;任务只引用物品 ID,不重新定义物品价格。
## 核心循环覆盖检查
| 顶层循环环节 | 认领系统 |
|---|---|
| 查看天气、日程与目标 | S01、S11、S12、UI |
| 选择活动并移动 | S04、S02 |
| 农务与生产 | S03、S07、S02 |
| 采集、钓鱼与战斗 | S04、S05、S06、S07 |
| 出售、购买与投资 | S09、S07、S03 |
| 社交与委托 | S10、S11、S07 |
| 日终结算与保存 | S01、S12、存档、UI |
## 目录映射
| 目录 | 本阶段定位 |
|---|---|
| 03_systems/S01_time_schedule/ | 时间推进、日期季节天气、营业时段、日终结算 |
| 03_systems/S02_stamina_status/ | 体力、状态效果、昏倒与恢复 |
| 03_systems/S03_farm_management/ | 土地、作物、畜牧、设施生产 |
| 03_systems/S04_exploration_map/ | 区域、连接、资源点、解锁与移动 |
| 03_systems/S05_foraging_fishing/ | 采集、钓鱼、品质与获得物 |
| 03_systems/S06_combat_enemies/ | 战斗、敌人行为、伤害与战利品请求 |
| 03_systems/S07_items_inventory_crafting/ | 物品、背包、配方与制作队列 |
| 03_systems/S08_progression_skills/ | 技能经验、等级、工具升级、能力解锁 |
| 03_systems/S09_economy_shop/ | 货币、买卖、价格、库存与订单 |
| 03_systems/S10_npc_relationship/ | NPC 日程、对话、好感与关系事件 |
| 03_systems/S11_quests_community/ | 任务、社区目标、奖励与解锁条件 |
| 03_systems/S12_events_festivals/ | 季节事件、节日、条件触发 |
| 支撑层不单开系统文档 | 存档与 UI 随实现层组织,规则不独立成文 |
## MVP 最小闭环
1. 玩家在一个游戏日内完成开垦、播种、浇灌,并看到成长状态反馈。
2. 在时间与体力约束下选择当日主目标(农场劳动或外出)。
3. 外出采集(或矿井轻度战斗)带回资源。
4. 通过出售或加工获得金钱,投资种子或工具。
5. 日终结算展示当日变化并保存。
6. 次日作物状态变化,玩家据此形成新计划。
7. 数个游戏日内出现第一次技能提升与配方解锁。
如果这条闭环不成立,不应继续增加钓鱼深度、节日、社区目标或更多区域。
## 统一数值基准
本案例采用"宽松治愈型"数值风格。全局单位:时间片、游戏日、货币、体力、经验;所有数值字段必须注明单位。
- 时间节奏基准:单次常规行动控制在短时间片内;玩家一天应能完成农务、一个主要外出目标和少量顺路活动;早期玩家不应因一次路线失误失去整天进度。
- 货币量级基准:主要货币只有一种;初期基础种子可用少量日常产出购买;一次普通收获不应立刻买下最高阶升级;任务奖励以补足短期资金为主,不替代生产交易。
- 成长回报基准:前几级在正常尝试一种活动的数个游戏日内出现;升级奖励优先采用节省时间体力、扩大选择和解锁配方,而非单纯提高伤害售价;专长分支宽松可恢复。
- 体力与风险基准:体力是规划提示不是严苛倒计时;普通农务与移动成本低,战斗、钓鱼和重型工具才产生明显取舍;失败成本采用时间、少量金钱或位置变化,不损毁进度。
(具体换算数值与前五日验算由技术文档层·数值策划承接。)
## 系统边界
- 农场经营只管理农场内的生产状态,不负责所有资源的通用背包逻辑。
- 探索与地图只管理"在哪里"和"能否进入",不管理每种活动的具体奖励。
- 战斗只管理战斗内状态和战利品请求,不直接修改商店价格或 NPC 好感。
- NPC 与关系负责互动和关系变化;任务与社区负责可验证目标,二者通过事件和条件连接。
- UI、文本和表现不反向承载核心规则;所有关键变化必须由规则系统确认。
- 本案例不拆出独立多人、拍卖、复杂天气模拟、动态市场或高复杂度叙事工具系统。
## 优先级与范围
- P0(最小可玩闭环):时间与日程、体力、农场、物品背包、经济、基础地图、基础成长和日终结算。
- P1(形成完整案例):采集、钓鱼、轻度战斗、NPC 关系、任务、社区目标、制作、商店、季节和节日。
- P2(扩展内容):更多区域、敌人、作物、配方、关系事件、节日小游戏和终局后的自由活动。
拆分系统不等于所有系统都要在最小版本同时实现;系统独立性是为了便于协作和后续裁剪。
## 风险与校验
| 风险 | 校验方式 |
|---|---|
| 农场变成例行公事,失去规划感 | 玩家是否在目标选择阶段出现真实取舍与计划调整 |
| 矿井战斗反客为主 | 战斗收益是否仍以"农场难以产出的材料"为主,而非直接金钱 |
| 时间压力变成打卡义务 | 休闲型玩家能否自由调低日程重量而不被惩罚 |
| 经济成长过快,后期失去决策 | 升级价格是否持续制造"效率 vs 规模"的选择 |
| UI 泄题,探索失去意义 | 关键信息是否保留为探索发现而非全量直读 |
| 系统间主数据重复维护 | 交叉检查:同一事实是否只有一个系统拥有写权 |
## 开放的结构问题
- 体力与生命是否保持为两个状态,还是在轻度战斗中共享一套风险资源?
- NPC 日程、任务条件和节日事件之间采用统一条件格式还是各自维护?
- 农场设施生产是否由农场系统统一管理,还是交给通用制作队列?
- 采集、钓鱼和战斗是否共享统一的"活动结果"接口?
- 哪些系统需要独立数据表,哪些小型配置应合并为一张内容表?

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