Merge remote-tracking branch 'origin/master' into feat/jenkins-mac-build
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 17s
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 19s
Project CI / Frontend tests (pull_request) Failing after 6s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s

# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
2026-09-20 21:34:08 +08:00
129 changed files with 20769 additions and 6582 deletions
+17
View File
@@ -302,6 +302,23 @@ jobs:
sleep $((attempt * 2))
done
- name: Prepare Godot plugin Rust dependencies
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo 'Godot plugin Cargo dependency fetch failed after 5 attempts.' >&2
exit 1
fi
sleep $((attempt * 2))
done
- name: Run AI game creator shell shared crate gates
run: npm run check:native-shells:agc-rust-crates
@@ -0,0 +1,330 @@
/**
* AGC 总版本号(发号源)。
*
* 唯一事实源是 OSS 对象 `agc/global-version.json`;渠道清单只写各自本次拿到的号。
* 仓库里的 5 个版本文件仍由构建改写,但只作构建输入参考,不作为事实源。
*
* 发号顺序固定为「先写总号 → 再构建 → 再发渠道清单」:任何一步失败都不回滚,
* 只烧号。这样渠道之间不会复用同一个号,代价是可能出现空洞。
*/
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
export const AGC_GLOBAL_VERSION_OBJECT_KEY = 'agc/global-version.json';
const defaultOssBaseUrl =
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc';
const versionPattern = /^\d+\.\d+\.\d+$/u;
function trimTrailingSlashes(value) {
return value.replace(/\/+$/u, '');
}
export function ossBaseUrl(env = process.env) {
return trimTrailingSlashes(
env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl,
);
}
export function globalVersionUrl(env = process.env) {
return `${ossBaseUrl(env)}/global-version.json`;
}
export function parseVersion(value, label) {
if (typeof value !== 'string' || !versionPattern.test(value.trim())) {
throw new Error(`${label} 不是有效的三段版本号:${String(value)}`);
}
return value.trim();
}
export function compareVersions(left, right) {
const leftParts = parseVersion(left, '左版本').split('.').map(Number);
const rightParts = parseVersion(right, '右版本').split('.').map(Number);
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1;
}
}
return 0;
}
/** 取较大版本;任一为空时返回另一个。 */
export function maxVersion(...versions) {
return versions
.filter((value) => typeof value === 'string' && value.trim())
.map((value) => parseVersion(value, '候选版本'))
.reduce(
(best, current) =>
best == null || compareVersions(current, best) > 0 ? current : best,
null,
);
}
export function nextVersion(current) {
const [major, minor, patch] = parseVersion(current, '总版本')
.split('.')
.map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${current}`);
}
return `${major}.${minor}.${patch + 1}`;
}
export function readReleaseDryRun(env = process.env) {
const value = env.AGC_RELEASE_DRY_RUN?.trim().toLowerCase();
return value === '1' || value === 'true';
}
async function fetchJson(url, label, { fetchImpl = fetch } = {}) {
let response;
try {
response = await fetchImpl(url, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`读取 ${label} 失败:${error.message}`);
}
if (response.status === 404) return null;
if (!response.ok) {
throw new Error(`读取 ${label} 失败:HTTP ${response.status}`);
}
try {
return await response.json();
} catch (error) {
throw new Error(`${label} 不是有效 JSON${error.message}`);
}
}
/** 渠道清单版本;缺失或 404 时返回 null(首次启用渠道)。 */
export async function readChannelVersion(channel, options = {}) {
const payload = await fetchJson(
`${ossBaseUrl(options.env)}/${channel}/latest.json`,
`${channel} 渠道清单`,
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
if (!version) {
throw new Error(`${channel} 渠道清单缺少 version 字段`);
}
return parseVersion(version, `${channel} 渠道清单 version`);
}
/** 旧协议迁移指针 `agc/latest.json`;只在迁移窗口内存在,仅参与播种。 */
export async function readLegacyPointerVersion(options = {}) {
const payload = await fetchJson(
`${ossBaseUrl(options.env)}/latest.json`,
'OSS 迁移指针',
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
return version ? parseVersion(version, 'OSS 迁移指针 version') : null;
}
export async function readGlobalVersion(options = {}) {
const payload = await fetchJson(
globalVersionUrl(options.env),
'AGC 总版本号',
options,
);
if (payload == null) return null;
const version = typeof payload.version === 'string' ? payload.version : '';
if (!version) {
throw new Error('AGC 总版本号对象缺少 version 字段');
}
return {
...payload,
version: parseVersion(version, 'AGC 总版本号 version'),
};
}
/**
* 一次性播种基线:仓库当前版本、渠道清单与旧迁移指针里的最大值。
* 基线本身不发给客户端,首个发放号是 baseline + 1。
*/
export async function resolveSeedBaseline({
channels = ['dev-win', 'dev-mac'],
repoVersion = null,
env = process.env,
fetchImpl = fetch,
} = {}) {
const candidates = [];
if (repoVersion) candidates.push(parseVersion(repoVersion, '仓库当前版本'));
for (const channel of channels) {
const version = await readChannelVersion(channel, { env, fetchImpl });
if (version) candidates.push(version);
}
const legacy = await readLegacyPointerVersion({ env, fetchImpl });
if (legacy) candidates.push(legacy);
const baseline = maxVersion(...candidates);
if (!baseline) {
throw new Error('无法确定总版本号播种基线:仓库版本与渠道清单都不可用');
}
return baseline;
}
/**
* 组装 ossutil 参数。
*
* 该桶与凭据按 v1 签名使用(ossutil v2 默认 v4,缺 region 会直接失败),
* 因此默认显式传 `--sign-version v1`;需要 v4 时用 `AGC_OSS_SIGN_VERSION=v4`
* 并同时给 `AGC_OSS_REGION`。
*/
export function buildOssutilArgs({
args,
endpoint,
accessKeyId,
accessKeySecret,
env = process.env,
}) {
const finalArgs = [...args, '--endpoint', endpoint];
const region = env.AGC_OSS_REGION?.trim();
if (region) finalArgs.push('--region', region);
finalArgs.push('--sign-version', env.AGC_OSS_SIGN_VERSION?.trim() || 'v1');
if (accessKeyId) {
finalArgs.push(
'--access-key-id',
accessKeyId,
'--access-key-secret',
accessKeySecret,
);
}
return finalArgs;
}
function runOssutil(args, { env = process.env } = {}) {
const binary = env.OSSUTIL_BIN?.trim() || 'ossutil';
const endpoint =
env.AGC_OSS_ENDPOINT?.trim() || 'oss-rg-china-mainland.aliyuncs.com';
const accessKeyId = env.AGC_OSS_ACCESS_KEY_ID?.trim();
const accessKeySecret = env.AGC_OSS_ACCESS_KEY_SECRET;
if (Boolean(accessKeyId) !== Boolean(accessKeySecret)) {
throw new Error('OSS AccessKey ID 和 Secret 必须同时提供');
}
const result = spawnSync(
binary,
buildOssutilArgs({
args,
endpoint,
accessKeyId,
accessKeySecret,
env,
}),
{ stdio: 'inherit', shell: false, env },
);
if (result.error) {
throw new Error(`无法执行 ${binary},请先安装并配置 ossutil`);
}
if (result.status !== 0) {
throw new Error(`${binary} 执行失败,退出码 ${result.status}`);
}
}
/** 写入总版本号对象;dry-run 下只打印将要执行的上传。 */
export function writeGlobalVersion(payload, options = {}) {
const { env = process.env, dryRun = readReleaseDryRun(env) } = options;
const bucket = env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
const body = Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf8');
const tempDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'agc-global-version-'),
);
const tempPath = path.join(tempDirectory, 'global-version.json');
try {
fs.writeFileSync(tempPath, body);
const ossUrl = `oss://${bucket}/${AGC_GLOBAL_VERSION_OBJECT_KEY}`;
if (dryRun) {
console.log(
`[dry-run] 不写总版本号:${ossUrl} <- ${JSON.stringify(payload)}`,
);
return { written: false, objectUrl: ossUrl, payload };
}
runOssutil(['cp', '--force', tempPath, ossUrl], { env });
return { written: true, objectUrl: ossUrl, payload };
} finally {
fs.rmSync(tempDirectory, { force: true, recursive: true });
}
}
/**
* 发一次号并写回总版本号对象。
*
* 写后回读校验:若远端值与自己写下的不一致,说明有并发发号,按失败关闭处理
* (号已烧,不重试、不回滚),由人工确认后再发。
*/
export async function issueGlobalVersion({
channel,
commit = null,
buildId = null,
repoVersion = null,
env = process.env,
fetchImpl = fetch,
now = () => new Date().toISOString(),
writeImpl = writeGlobalVersion,
} = {}) {
if (!channel) throw new Error('发号必须显式指定 channel');
const dryRun = readReleaseDryRun(env);
const current = await readGlobalVersion({ env, fetchImpl });
let baseline = current?.version ?? null;
let seeded = false;
if (!baseline) {
baseline = await resolveSeedBaseline({ repoVersion, env, fetchImpl });
seeded = true;
}
const issued = nextVersion(baseline);
const payload = {
version: issued,
updatedAt: now(),
channel,
commit,
buildId,
};
console.log(
`[agc-global-version] ${
seeded ? `按播种基线 ${baseline} 首发` : `总号 ${baseline}`
} -> ${issued}channel=${channel} dry-run=${dryRun}`,
);
writeImpl(payload, { env, dryRun });
if (!dryRun) {
const stored = await readGlobalVersion({ env, fetchImpl });
if (!stored || stored.version !== issued) {
throw new Error(
`总版本号写后回读不一致:期望 ${issued},远端 ${
stored?.version ?? '不存在'
};可能存在并发发号,本次构建失败关闭`,
);
}
}
return issued;
}
/** 渠道高水位断言:请求号低于本渠道清单版本即失败关闭。 */
export function assertRequestedVersionNotBelowChannel({
requested,
channelVersion,
channel,
}) {
const requestedVersion = parseVersion(requested, '请求版本');
if (channelVersion == null) return requestedVersion;
const current = parseVersion(channelVersion, `${channel} 渠道版本`);
if (compareVersions(requestedVersion, current) < 0) {
throw new Error(
`请求版本 ${requestedVersion} 低于 ${channel} 渠道当前清单版本 ${current};拒绝回退发布`,
);
}
return requestedVersion;
}
/** 只读预览:不写回、不烧号。 */
export async function previewNextGlobalVersion(options = {}) {
const current = await readGlobalVersion(options);
const baseline =
current?.version ??
(await resolveSeedBaseline({
repoVersion: options.repoVersion,
env: options.env,
fetchImpl: options.fetchImpl,
}));
return nextVersion(baseline);
}
@@ -0,0 +1,211 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
assertRequestedVersionNotBelowChannel,
buildOssutilArgs,
issueGlobalVersion,
maxVersion,
nextVersion,
previewNextGlobalVersion,
resolveSeedBaseline,
} from './agc-global-version.mjs';
function jsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
/** 以 URL 为键的假 OSS:只读 fetch + 记录写入。 */
function createFakeOss({ objects = {} } = {}) {
const state = { ...objects };
const writes = [];
return {
state,
writes,
fetchImpl: async (url) => {
const key = String(url).replace(/^https?:\/\/[^/]+\//u, '');
if (!(key in state)) return jsonResponse(null, 404);
return jsonResponse(state[key]);
},
writeImpl: (payload, options = {}) => {
writes.push({ payload, dryRun: Boolean(options.dryRun) });
if (!options.dryRun) state['agc/global-version.json'] = payload;
return { written: !options.dryRun, payload };
},
};
}
test('播种基线取仓库版本、各渠道清单与旧迁移指针的最大值', async () => {
const oss = createFakeOss({
objects: {
'agc/dev-win/latest.json': { version: '0.1.57' },
'agc/dev-mac/latest.json': { version: '0.1.12' },
'agc/latest.json': { version: '0.1.60' },
},
});
assert.equal(
await resolveSeedBaseline({
repoVersion: '0.1.48',
env: {},
fetchImpl: oss.fetchImpl,
}),
'0.1.60',
);
assert.equal(maxVersion('0.1.9', '0.1.10', null), '0.1.10');
});
test('无总号时按播种基线发首号,并把总号写回唯一事实源', async () => {
const oss = createFakeOss({
objects: {
'agc/dev-win/latest.json': { version: '0.1.57' },
'agc/dev-mac/latest.json': { version: '0.1.12' },
},
});
const issued = await issueGlobalVersion({
channel: 'dev-win',
commit: 'a'.repeat(40),
buildId: '319',
repoVersion: '0.1.48',
env: {},
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
now: () => '2026-09-20T00:00:00.000Z',
});
assert.equal(issued, '0.1.58');
assert.deepEqual(
oss.writes.map((entry) => entry.payload.version),
['0.1.58'],
);
assert.equal(oss.state['agc/global-version.json'].version, '0.1.58');
assert.equal(oss.state['agc/global-version.json'].channel, 'dev-win');
assert.equal(oss.state['agc/global-version.json'].buildId, '319');
});
test('已有总号时只递增,不再回看渠道清单', async () => {
const oss = createFakeOss({
objects: {
'agc/global-version.json': { version: '0.2.7' },
// 渠道清单被手工改小也不能把总号拉回去。
'agc/dev-win/latest.json': { version: '0.1.10' },
},
});
const issued = await issueGlobalVersion({
channel: 'dev-mac',
env: {},
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
});
assert.equal(issued, '0.2.8');
assert.equal(oss.state['agc/global-version.json'].version, '0.2.8');
});
test('dry-run 只预览下一位,不写回、不烧号', async () => {
const oss = createFakeOss({
objects: { 'agc/global-version.json': { version: '0.3.4' } },
});
const preview = await previewNextGlobalVersion({
env: {},
fetchImpl: oss.fetchImpl,
});
assert.equal(preview, '0.3.5');
assert.equal(oss.writes.length, 0);
const issued = await issueGlobalVersion({
channel: 'dev-win',
env: { AGC_RELEASE_DRY_RUN: '1' },
fetchImpl: oss.fetchImpl,
writeImpl: oss.writeImpl,
});
assert.equal(issued, '0.3.5');
assert.deepEqual(
oss.writes.map((entry) => entry.dryRun),
[true],
);
assert.equal(oss.state['agc/global-version.json'].version, '0.3.4');
});
test('传入低于本渠道清单的号时失败关闭', () => {
assert.equal(
assertRequestedVersionNotBelowChannel({
requested: '0.1.60',
channelVersion: '0.1.60',
channel: 'dev-win',
}),
'0.1.60',
);
assert.throws(
() =>
assertRequestedVersionNotBelowChannel({
requested: '0.1.59',
channelVersion: '0.1.60',
channel: 'dev-win',
}),
/低于 dev-win 渠道当前清单版本/u,
);
assert.equal(
assertRequestedVersionNotBelowChannel({
requested: '0.1.1',
channelVersion: null,
channel: 'dev-mac',
}),
'0.1.1',
);
});
test('写后回读不一致(并发发号)时失败关闭', async () => {
const oss = createFakeOss({
objects: { 'agc/global-version.json': { version: '0.5.1' } },
});
await assert.rejects(
issueGlobalVersion({
channel: 'dev-win',
env: {},
// 模拟另一个发号进程在写入后覆盖了总号。
writeImpl: (payload, options) => {
const result = oss.writeImpl(payload, options);
// 另一个发号进程紧随其后覆盖总号。
oss.state['agc/global-version.json'] = { version: '0.5.9' };
return result;
},
fetchImpl: oss.fetchImpl,
}),
/写后回读不一致/u,
);
});
test('nextVersion 只在 patch 位递增', () => {
assert.equal(nextVersion('0.1.9'), '0.1.10');
assert.equal(nextVersion('1.0.0'), '1.0.1');
assert.throws(() => nextVersion('0.1'), /不是有效的三段版本号/u);
});
test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => {
const base = {
args: [
'cp',
'--force',
'/tmp/a.json',
'oss://agc-dev/agc/global-version.json',
],
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
accessKeyId: 'id',
accessKeySecret: 'secret',
env: {},
};
const v1 = buildOssutilArgs(base);
assert.equal(v1[v1.indexOf('--sign-version') + 1], 'v1');
assert.ok(!v1.includes('--region'));
assert.equal(v1[v1.indexOf('--access-key-id') + 1], 'id');
assert.equal(v1[v1.indexOf('--access-key-secret') + 1], 'secret');
const v4 = buildOssutilArgs({
...base,
env: { AGC_OSS_SIGN_VERSION: 'v4', AGC_OSS_REGION: 'cn-beijing' },
});
assert.equal(v4[v4.indexOf('--region') + 1], 'cn-beijing');
assert.equal(v4[v4.indexOf('--sign-version') + 1], 'v4');
});
@@ -5,6 +5,10 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
assertRequestedVersionNotBelowChannel,
issueGlobalVersion as issueAgcGlobalVersion,
} from './agc-global-version.mjs';
import {
defaultEditorFeatures,
withDefaultCargoFeatures,
@@ -102,6 +106,7 @@ export const agcReleasePathPatterns = [
'server-rs/crates/',
'plugins/agc-cocos-editor/',
'plugins/agc-unity-editor/',
'plugins/agc-godot-editor/',
'apps/desktop-shell/src-tauri/icons/',
'package.json',
'package-lock.json',
@@ -309,14 +314,35 @@ function replaceVersionLine(source, version, pattern, label) {
return source.replace(pattern, `$1${version}$3`);
}
/**
* 版本来源固定为 OSS 总版本号(`agc/global-version.json`):
* - CI 统一构建由发号 Job 先发号,再通过 AGC_RELEASE_VERSION 透传给各渠道;
* - 未传入时(本地手工兜底)由本函数现场发号并写回总号;
* - 渠道高水位只做断言:传入号低于本渠道清单版本即失败关闭。
*/
export async function prepareReleaseVersion(context = resolveReleaseContext()) {
const { channel, target } = context;
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await resolveRemoteHighWaterVersion(channel, target);
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
const nextVersion = requestedVersion
? parseVersion(requestedVersion, '指定版本')
: nextPatchVersion(localVersion, remoteVersion);
? assertRequestedVersionNotBelowChannel({
requested: requestedVersion,
channelVersion: remoteVersion,
channel,
})
: await issueAgcGlobalVersion({
channel,
commit:
process.env.COMMIT_HASH?.trim() ||
process.env.GIT_COMMIT?.trim() ||
null,
buildId:
process.env.BUILD_NUMBER?.trim() ||
process.env.AGC_BUILD_ID?.trim() ||
null,
repoVersion: localVersion,
});
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
@@ -375,8 +401,8 @@ export async function prepareReleaseVersion(context = resolveReleaseContext()) {
console.log(
requestedVersion
? `[ai-game-creator-shell] 渠道 ${channel} 使用指定版本 ${nextVersion}(本${localVersion} / OSS ${remoteVersion ?? '不存在'}`
: `[ai-game-creator-shell] 渠道 ${channel} ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
? `[ai-game-creator-shell] 渠道 ${channel} 使用发号 Job 下发的总号 ${nextVersion}(本渠道清单 ${remoteVersion ?? '不存在'} / 仓库 ${localVersion}`
: `[ai-game-creator-shell] 渠道 ${channel}地兜底发号 ${nextVersion}(本渠道清单 ${remoteVersion ?? '不存在'} / 仓库 ${localVersion}`,
);
return nextVersion;
}
@@ -535,7 +535,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
spawn: (_binary, command) => {
assert.ok(
command.includes(
'--features=cocos-editor-execute,unity-editor-execute',
'--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute',
),
);
assert.ok(command.includes('user-config.json'));
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
export function defaultEditorFeatures(target) {
return target === 'win32' || target.includes('windows')
? ['cocos-editor-execute', 'unity-editor-execute']
? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute']
: [];
}
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
[
'build',
'--features=cocos-editor-execute,unity-editor-execute',
'--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute',
'--target',
'x86_64-pc-windows-msvc',
],
@@ -0,0 +1,121 @@
/**
* AGC 总版本号发号入口(CI 发号 Job 与本地手工兜底共用)。
*
* 用法:
* node scripts/issue-global-version.mjs --channel dev-win [--commit <sha>] [--build-id <id>] [--out <file>]
* node scripts/issue-global-version.mjs --seed-only
* node scripts/issue-global-version.mjs --dry-run --channel dev-win # 只预览,不写回、不烧号
*
* 输出固定为一行 `AGC_GLOBAL_VERSION=<version>`,便于 Jenkins 直接读取。
*/
import fs from 'node:fs';
import {
issueGlobalVersion,
previewNextGlobalVersion,
readGlobalVersion,
readReleaseDryRun,
resolveSeedBaseline,
writeGlobalVersion,
} from './agc-global-version.mjs';
function parseArgs(argv) {
const options = {
channel: '',
commit: '',
buildId: '',
repoVersion: '',
out: '',
seedOnly: false,
dryRun: readReleaseDryRun(),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const readValue = (label) => {
const value = argv[index + 1];
if (value == null || value.startsWith('--')) {
throw new Error(`${label} 缺少取值`);
}
index += 1;
return value;
};
switch (arg) {
case '--channel':
options.channel = readValue('--channel');
break;
case '--commit':
options.commit = readValue('--commit');
break;
case '--build-id':
options.buildId = readValue('--build-id');
break;
case '--repo-version':
options.repoVersion = readValue('--repo-version');
break;
case '--out':
options.out = readValue('--out');
break;
case '--seed-only':
options.seedOnly = true;
break;
case '--dry-run':
options.dryRun = true;
break;
default:
throw new Error(`未知参数:${arg}`);
}
}
return options;
}
function emit(version, options) {
console.log(`AGC_GLOBAL_VERSION=${version}`);
if (options.out) {
fs.writeFileSync(options.out, `${version}\n`, 'utf8');
}
}
const options = parseArgs(process.argv.slice(2));
process.env.AGC_RELEASE_DRY_RUN = options.dryRun ? '1' : '0';
if (options.seedOnly) {
const current = await readGlobalVersion();
if (current) {
console.log(
`[agc-global-version] 总号已存在(${current.version}),播种跳过;需要重新播种请先人工确认`,
);
emit(current.version, options);
} else {
const baseline = await resolveSeedBaseline({
repoVersion: options.repoVersion || null,
});
const payload = {
version: baseline,
updatedAt: new Date().toISOString(),
channel: options.channel || 'seed',
commit: options.commit || null,
buildId: options.buildId || null,
};
writeGlobalVersion(payload, { dryRun: options.dryRun });
console.log(
`[agc-global-version] 播种基线 ${baseline}(尚未发号,首发为下一位)`,
);
emit(baseline, options);
}
} else if (options.dryRun) {
const preview = await previewNextGlobalVersion({
repoVersion: options.repoVersion || null,
});
console.log(
`[agc-global-version] 预览下一个总号 ${preview}dry-run 不写回、不烧号)`,
);
emit(preview, options);
} else {
const issued = await issueGlobalVersion({
channel: options.channel || 'manual',
commit: options.commit || null,
buildId: options.buildId || null,
repoVersion: options.repoVersion || null,
});
emit(issued, options);
}
@@ -312,34 +312,15 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
},
);
const failureLines = [];
let inFailureList = false;
// Rust 的首个 failures: 后有空行,不能按空行结束采集,否则会丢掉 panic 详情。
// 只保存有界尾部;成功时不输出,失败时优先输出完整失败段。
let stdoutTail = '';
let stderr = '';
const consumeLine = (rawLine) => {
const line = rawLine.replace(/\r$/, '');
if (line.includes('failures:')) {
inFailureList = true;
return;
}
if (inFailureList) {
if (line.trim().length === 0) {
inFailureList = false;
return;
}
failureLines.push(line.trim());
}
};
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
let stdoutBuffer = '';
child.stdout.on('data', (chunk) => {
stdoutBuffer += chunk;
const lines = stdoutBuffer.split('\n');
stdoutBuffer = lines.pop() ?? '';
for (const line of lines) {
consumeLine(line);
}
stdoutTail = (stdoutTail + chunk).slice(-64_000);
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
@@ -356,12 +337,17 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
});
});
child.on('close', (code) => {
const output = stdoutTail.replace(/\r\n/g, '\n');
const failureStart = output.indexOf('failures:\n');
resolve({
label,
ok: code === 0,
durationMs: Date.now() - startedAt,
testCount: shardTestNames.length,
failures: failureLines,
failures: output
.slice(failureStart < 0 ? 0 : failureStart)
.trim()
.split('\n'),
stderr,
});
});
@@ -438,7 +424,7 @@ async function main() {
}
failed = true;
console.error(
`[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
`[rust-shards] ${result.label} FAILED (selected ${result.testCount} test(s)) in ${formatDuration(result.durationMs)}`,
);
for (const failure of result.failures) {
console.error(`[rust-shards] ${failure}`);
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const runner = fileURLToPath(
new URL('./run-rust-shell-test-shards.mjs', import.meta.url),
);
function runFixture(t, source) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-shard-output-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(
path.join(root, 'Cargo.toml'),
'[package]\nname = "shard-output-fixture"\nversion = "0.1.0"\nedition = "2021"\n',
);
fs.writeFileSync(path.join(root, 'src/lib.rs'), source);
const result = spawnSync(
process.execPath,
[
runner,
`--manifest=${path.join(root, 'Cargo.toml')}`,
'--target-kind=lib',
'--no-locked',
'--shards=1',
`--shard-tmp-root=${path.join(root, 'tmp')}`,
],
{
encoding: 'utf8',
timeout: 60_000,
windowsHide: true,
env: { ...process.env, CARGO_TARGET_DIR: path.join(root, 'target') },
},
);
assert.ifError(result.error);
return { status: result.status, output: result.stdout + result.stderr };
}
test('failed shard retains panic details and separates selected count from failures', (t) => {
const result = runFixture(
t,
`
#[test]
fn passing_case() {}
#[test]
fn failing_case() {
assert_eq!(1, 2, "shard panic evidence");
}
`,
);
assert.equal(result.status, 1, result.output);
assert.match(result.output, /FAILED \(selected 2 test\(s\)\)/);
assert.match(result.output, /failing_case/);
assert.match(result.output, /panicked at src[\\/]lib\.rs:/);
assert.match(result.output, /shard panic evidence/);
assert.match(result.output, /left: 1/);
assert.match(result.output, /right: 2/);
assert.match(result.output, /1 passed; 1 failed/);
});
test('successful shard keeps its compact summary', (t) => {
const result = runFixture(t, '#[test]\nfn passing_case() {}\n');
assert.equal(result.status, 0, result.output);
assert.match(result.output, /shard 1\/1 ok: 1 test\(s\)/);
assert.doesNotMatch(result.output, /test passing_case \.\.\. ok/);
});
@@ -9,7 +9,9 @@ export const EXPECTED_SKILL_NAMES = Object.freeze([
'agc-browser-playtest',
'agc-client-projection',
'agc-game-production-workflow',
'agc-godot-editor',
'agc-project-structure',
'agc-unity-editor',
'agc-web-game-development',
'taonier-art-assets',
]);
+13
View File
@@ -1755,6 +1755,7 @@ dependencies = [
"editor-adapter-api",
"futures",
"getrandom 0.3.4",
"godot-editor-bridge",
"http",
"image",
"jsonschema",
@@ -1981,6 +1982,18 @@ dependencies = [
"system-deps",
]
[[package]]
name = "godot-editor-bridge"
version = "0.1.0"
dependencies = [
"editor-adapter-api",
"serde",
"serde_json",
"sha2",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "gtk"
version = "0.18.2"
@@ -13,6 +13,7 @@ cocos-editor = ["cocos-editor-bridge/process-discovery"]
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
unity-editor-execute = []
godot-editor-execute = []
[build-dependencies]
serde = { version = "1", features = ["derive"] }
@@ -29,6 +30,7 @@ agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
unity-editor-bridge = { path = "../../../plugins/agc-unity-editor/native/unity-editor-bridge" }
godot-editor-bridge = { path = "../../../plugins/agc-godot-editor/native/godot-editor-bridge" }
base64 = "0.22"
axum = "0.8"
chromiumoxide = "0.9.1"
@@ -2,6 +2,8 @@
mod codex_bundle;
#[path = "build_support/frontend_dist_guard.rs"]
mod frontend_dist_guard;
#[path = "build_support/godot_bundle.rs"]
mod godot_bundle;
#[path = "build_support/runtime_prompt_bundle.rs"]
mod runtime_prompt_bundle;
@@ -213,6 +215,7 @@ fn main() {
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
stage_bundled_codex_cli(&manifest_dir);
prepare_unity_editor_helper(&manifest_dir);
prepare_godot_editor_extension(&manifest_dir);
stage_plugin_workspace(&manifest_dir);
stage_cocos_editor_payload(&manifest_dir);
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
@@ -384,6 +387,39 @@ fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBu
}
}
fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_GODOT_EDITOR_EXECUTE");
if env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_none()
|| env::var("TARGET").expect("Cargo TARGET") != "x86_64-pc-windows-msvc"
{
return;
}
let root = manifest_dir.join("../../../plugins/agc-godot-editor/native/gdextension");
for source in godot_bundle::source_files(&root).unwrap_or_else(|error| panic!("{error}")) {
println!("cargo:rerun-if-changed={}", source.display());
}
assert!(
cfg!(windows),
"构建 Godot 原生扩展需要 Windows x64 C 编译器"
);
let status = std::process::Command::new("powershell.exe")
// Cargo 可能从 PowerShell 7 启动,Windows PowerShell 应使用自身模块目录。
.env_remove("PSModulePath")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
])
.arg(root.join("build.ps1"))
.current_dir(&root)
.status()
.expect("无法启动 Godot 原生扩展构建脚本");
assert!(status.success(), "Godot 原生扩展构建失败");
godot_bundle::validate(&root).unwrap_or_else(|error| panic!("{error}"));
}
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
///
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
@@ -444,6 +480,15 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
}
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
}
if name == "agc-godot-editor" {
godot_bundle::stage(
&plugin_root.join("native/gdextension"),
&destination.join("native/gdextension"),
&target,
env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_some(),
)
.unwrap_or_else(|error| panic!("{error}"));
}
println!("cargo:rerun-if-changed={}", plugin_root.display());
}
}
@@ -0,0 +1,215 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub const BUNDLE_FILES: [&str; 4] = [
"bin/win-x64/agc_godot_editor.dll",
"bin/win-x64/metadata.json",
"vendor/LICENSE.txt",
"vendor/provenance.json",
];
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("Godot 资源不可读 {}{error}", path.display()))?;
#[cfg(windows)]
let linked = {
use std::os::windows::fs::MetadataExt;
metadata.file_attributes() & 0x400 != 0
};
#[cfg(not(windows))]
let linked = metadata.file_type().is_symlink();
if linked {
return Err(format!("Godot 资源不能经过链接:{}", path.display()));
}
Ok(metadata)
}
fn read_bundle_file(root: &Path, relative: &str) -> Result<Vec<u8>, String> {
plain_metadata(root)?;
let mut path = root.to_path_buf();
for component in Path::new(relative).components() {
path.push(component);
plain_metadata(&path)?;
}
let metadata = plain_metadata(&path)?;
if !metadata.is_file() || metadata.len() == 0 {
return Err(format!("Godot 随包资源缺失或为空:{}", path.display()));
}
if relative.ends_with("metadata.json") && metadata.len() > 64 * 1024 {
return Err("Godot 构建元数据超过 64 KiB".to_string());
}
fs::read(&path).map_err(|error| format!("读取 Godot 资源失败:{error}"))
}
pub fn validate(root: &Path) -> Result<Vec<(&'static str, Vec<u8>)>, String> {
let files = BUNDLE_FILES
.iter()
.map(|relative| read_bundle_file(root, relative).map(|bytes| (*relative, bytes)))
.collect::<Result<Vec<_>, _>>()?;
let metadata: serde_json::Value = serde_json::from_slice(&files[1].1)
.map_err(|error| format!("Godot 构建元数据无效:{error}"))?;
for (field, expected) in [
("protocol", "agc.godot.editor.v1"),
("platform", "windows"),
("arch", "x86_64"),
("entrySymbol", "agc_godot_editor_init"),
("minimumGodotVersion", "4.7"),
] {
if metadata[field].as_str() != Some(expected) {
return Err(format!("Godot 构建元数据 {field} 不匹配"));
}
}
if !metadata["buildId"].as_str().is_some_and(|value| {
value.strip_prefix("sha256:").is_some_and(|digest| {
digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
})
}) {
return Err("Godot 构建身份无效".to_string());
}
let actual_sha256 = format!("{:x}", Sha256::digest(&files[0].1));
if metadata["sha256"].as_str() != Some(actual_sha256.as_str()) {
return Err("Godot DLL 与构建元数据 SHA256 不匹配".to_string());
}
Ok(files)
}
pub fn stage(root: &Path, destination: &Path, target: &str, enabled: bool) -> Result<(), String> {
if target != "x86_64-pc-windows-msvc" || !enabled {
return Ok(());
}
for (relative, bytes) in validate(root)? {
let path = destination.join(relative);
fs::create_dir_all(path.parent().expect("Godot resource parent"))
.map_err(|error| format!("创建 Godot 资源目录失败:{error}"))?;
fs::write(&path, bytes).map_err(|error| format!("写入 Godot 资源失败:{error}"))?;
}
Ok(())
}
pub fn source_files(root: &Path) -> Result<Vec<PathBuf>, String> {
plain_metadata(root)?;
let mut sources = Vec::new();
for entry in fs::read_dir(root).map_err(|error| format!("读取 Godot 源码失败:{error}"))?
{
let entry = entry.map_err(|error| format!("读取 Godot 源码目录项失败:{error}"))?;
if matches!(entry.file_name().to_str(), Some("bin" | ".build")) {
continue;
}
let metadata = plain_metadata(&entry.path())?;
if metadata.is_dir() {
sources.extend(source_files(&entry.path())?);
} else if metadata.is_file() {
sources.push(entry.path());
}
}
sources.sort();
Ok(sources)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(root: &Path) {
for relative in BUNDLE_FILES {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, b"fixture").unwrap();
}
fs::write(
root.join(BUNDLE_FILES[1]),
serde_json::to_vec(&serde_json::json!({
"protocol": "agc.godot.editor.v1",
"platform": "windows",
"arch": "x86_64",
"entrySymbol": "agc_godot_editor_init",
"minimumGodotVersion": "4.7",
"buildId": format!("sha256:{}", "a".repeat(64)),
"sha256": format!("{:x}", Sha256::digest(b"fixture")),
}))
.unwrap(),
)
.unwrap();
}
#[test]
fn stage_only_verified_windows_runtime_and_not_build_inputs() {
let source = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
fixture(source.path());
fs::write(source.path().join("bridge.gd"), "source").unwrap();
fs::write(source.path().join("bin/win-x64/extra.dll"), "excluded").unwrap();
stage(
source.path(),
destination.path(),
"x86_64-pc-windows-msvc",
true,
)
.unwrap();
for relative in BUNDLE_FILES {
assert_eq!(
fs::read(source.path().join(relative)).unwrap(),
fs::read(destination.path().join(relative)).unwrap()
);
}
assert!(!destination.path().join("bridge.gd").exists());
assert!(!destination.path().join("bin/win-x64/extra.dll").exists());
}
#[test]
fn unsupported_or_disabled_targets_need_no_native_artifacts() {
let destination = tempfile::tempdir().unwrap();
for (target, enabled) in [
("aarch64-apple-darwin", true),
("x86_64-apple-darwin", true),
("x86_64-unknown-linux-gnu", true),
("aarch64-pc-windows-msvc", true),
("x86_64-pc-windows-msvc", false),
] {
stage(
Path::new("missing-godot-native"),
destination.path(),
target,
enabled,
)
.unwrap();
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
}
}
#[test]
fn incomplete_or_tampered_bundle_fails_before_copying() {
let source = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
fixture(source.path());
fs::write(source.path().join(BUNDLE_FILES[0]), b"tampered").unwrap();
assert!(stage(
source.path(),
destination.path(),
"x86_64-pc-windows-msvc",
true
)
.unwrap_err()
.contains("SHA256"));
assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0);
fixture(source.path());
fs::remove_file(source.path().join("vendor/LICENSE.txt")).unwrap();
assert!(validate(source.path()).is_err());
}
#[test]
fn source_watch_list_excludes_build_outputs() {
let source = tempfile::tempdir().unwrap();
fixture(source.path());
fs::create_dir(source.path().join(".build")).unwrap();
fs::write(source.path().join(".build/bridge.obj"), "generated").unwrap();
fs::write(source.path().join("bridge.gd"), "source").unwrap();
let sources = source_files(source.path()).unwrap();
assert_eq!(sources.len(), 3);
assert!(sources.contains(&source.path().join("bridge.gd")));
assert!(!sources.iter().any(|path| path
.components()
.any(|component| component.as_os_str() == "bin" || component.as_os_str() == ".build")));
}
}
@@ -1,4 +1,3 @@
顾问阶段遵照用户的具体指示行动。
顾问阶段遵照用户的具体指示行动,不自主推进项目或主动安排下一步,不提交阶段审批
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定。
顾问阶段以完成用户当前请求并汇报结果为结束点。
@@ -3,7 +3,7 @@
版本: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 开放问题回执互引。
> 编号口径:D-01~D-13 与 templates/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
## 当前待办(活队列)
@@ -1,7 +1,7 @@
# 顶层设计:《星露谷物语》
## 顶层定位与规模锚点
顶层设计让玩家每天都在想:
顶层不是做长线农场生产线,也不是做以探索战斗为主的活动清单,而是让玩家每天都在想:
> "今天做什么?——下雨天不用浇水,正好下矿井;回来的路上把罗宾的生日礼物送了。"
| 项 | 定义 |
@@ -5,7 +5,7 @@ name: game-gdd-architecture
description: 写游戏策划案(GDD)系统架构时使用。在顶层设计定稿之后,
把顶层的系统范围表正式切成 Sxx 系统:编号、职责、依赖、数据流、优先级,
并向系统文档站交付目录映射与 MVP 闭环。配套:templates/architecture.md、
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、exemplars/stardew-analysis.md(全局一份)。
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、templates/stardew-analysis.md(全局一份)。
---
# 系统架构写法(策划 agent · 系统架构分册)
@@ -5,7 +5,7 @@ name: game-gdd-concept
description: 写游戏策划案(GDD)概念层时使用。把一句话游戏想法写成一份
"一次写对、之后不动"的立项概念文档——它是后续所有设计争议的仲裁依据。
任何游戏类型通用。配套:templates/concept-design.md、templates/analysis.md(全局一份)、
exemplars/stardew-concept.md、exemplars/stardew-analysis.md(全局一份)。
exemplars/stardew-concept.md、templates/stardew-analysis.md(全局一份)。
---
# 概念层写法(策划 agent · 概念层分册)
@@ -5,7 +5,7 @@ name: game-gdd-top-design
description: 写游戏策划案(GDD)顶层设计时使用。在概念层定稿之后,
回答"玩家为什么一直玩"——把概念变成可玩的时间结构(循环/资源/取舍/节奏),
并向架构层交付系统范围。配套:templates/top-design.md、templates/analysis.md(全局一份)、
exemplars/stardew-top-design.md、exemplars/stardew-analysis.md(全局一份)。
exemplars/stardew-top-design.md、templates/stardew-analysis.md(全局一份)。
---
# 顶层设计写法(策划 agent · 顶层设计分册)
@@ -45,7 +45,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
|---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 1 | 顶层定位与规模锚点 | 承概念定稿 + 按需说明易混淆方向及排除理由 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
@@ -88,6 +88,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md
### 1. 顶层定位与规模锚点
承接概念定稿说明核心定位;存在容易混淆的方向时,说明排除方向及理由,表述按项目需要组织。
顶层设计让玩家每天都在想:
> "__(玩家每天惦记的那件事)"
规模锚点表:循环单位 / 段落构成 / 操作复杂度 / 经营复杂度 / 长期主轴排序。
@@ -1,4 +1,4 @@
### C1 例子_星露谷_分析.md(分析金样;→ exemplars/stardew-analysis.md
### C1 例子_星露谷_分析.md(分析金样;→ templates/stardew-analysis.md
# 分析:《星露谷物语》
@@ -5,6 +5,9 @@
# 顶层设计:《游戏名》
## 顶层定位与规模锚点
核心定位:__。
容易混淆的方向及排除理由(按需):__。
顶层设计让玩家每天都在想:
> "__"
@@ -7,12 +7,12 @@
正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。
阶段审批是每个阶段的最终检查,将已完成的本阶段产物交给用户检阅。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
阶段审批是五个策划阶段各自的最终检查,将已完成的本阶段产物交给用户检阅。需要用户选择的关键问题先通过问询解决,阶段审批不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
过程文档用于记录关键依据、决定和待办。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。用户主动质疑或出现新的约束冲突时,再重新讨论相关决定。
用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
用户说“继续”时,继续推进当前阶段最有价值的工作。在五个策划阶段中,判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
用户口头表示已经批准或要求进入下一阶段时,先调用 `get_workflow_status` 确认 Runtime 当前阶段。只有用户批准正式审批请求后,Runtime 才会推进阶段;审批工具是推进阶段的唯一方式。
@@ -9,5 +9,5 @@
{"type":"function","function":{"name":"write_file","description":"创建或覆盖工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"],"additionalProperties":false}}},
{"type":"function","function":{"name":"search_text","description":"在工作目录内搜索文本。","parameters":{"type":"object","properties":{"query":{"type":"string"},"path":{"type":"string"}},"required":["query"],"additionalProperties":false}}},
{"type":"function","function":{"name":"ask_clarification","description":"向用户展示多选项问询澄清卡片,选项数2-4。多选一场景时优先使用本工具,其他场景可以纯文本进行问询。每轮最多调用一次。","parameters":{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["question"],"additionalProperties":false}}},
{"type":"function","function":{"name":"submit_phase_for_approval","description":"提交当前策划阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}}
{"type":"function","function":{"name":"submit_phase_for_approval","description":"提交五个策划阶段中的当前阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}}
]

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