合并 master 最新更新
Project CI / Frontend tests (push) Failing after 24s
Project CI / Repository checks (push) Failing after 43s
Project CI / Backend tests (push) Successful in 3m5s
Project CI / Native shell tests (push) Failing after 6m49s

合入 master 的 BgFilter、CI、运维与现役平台改造。

保留 AI 游戏创作 Runtime、独立锁文件与原生壳验证链路。

修复共享充值账单组件、LLM 网关与退役 Agent 兼容边界。

同步冲突文档、锁文件和开发脚本。
This commit is contained in:
AIGameCreator App
2026-07-27 18:28:26 +08:00
1304 changed files with 170414 additions and 36096 deletions
+224 -7
View File
@@ -19,6 +19,7 @@ import {
assertReusableSpacetimeProcessVersionMatchesWorkspace,
assertSpacetimeToolVersionMatchesWorkspace,
buildApiServerProcessEnv,
buildBgfilterWorkerProcessEnv,
buildDevStackSnapshot,
buildFrontendProcessEnv,
buildLocalRustProcessEnv,
@@ -32,6 +33,7 @@ import {
normalizeCargoVersionRequirement,
parseArgs,
parseProcessEnvBlock,
parseSpacetimeToolCommit,
parseSpacetimeToolVersion,
resolveDevStackStatePath,
resolveLocalSpacetimeApiIdentityPath,
@@ -85,6 +87,26 @@ describe('dev scheduler argument routing', () => {
expect(runner.resolveFrontendApiTarget()).toBe('http://127.0.0.1:8090');
});
test('独立 BgFilter worker 命令解析内部监听地址', () => {
const { command, explicitOptions, options } = parseArgs(
[
'bgfilter-worker',
'--bgfilter-worker-host',
'127.0.0.2',
'--bgfilter-worker-port',
'18083',
],
{},
);
expect(command).toBe('bgfilter-worker');
expect(explicitOptions).toEqual(
new Set(['bgfilterWorkerHost', 'bgfilterWorkerPort']),
);
expect(options.bgfilterWorkerHost).toBe('127.0.0.2');
expect(options.bgfilterWorkerPort).toBe(18083);
});
test('单独 dev:web 未显式指定 api 参数时沿用已有 Rust target', () => {
const testEnv = {
RUST_SERVER_TARGET: 'http://127.0.0.1:3100',
@@ -129,7 +151,7 @@ describe('dev scheduler argument routing', () => {
);
});
linuxTest('Linux 启动时按系统级端口段映射个 dev 端口', async () => {
linuxTest('Linux 启动时按系统级端口段映射个 dev 端口', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
try {
const { command, explicitOptions, options } = parseArgs([], {
@@ -156,7 +178,9 @@ describe('dev scheduler argument routing', () => {
expect(runner.options.apiPort).toBe(22001);
expect(runner.options.spacetimePort).toBe(22002);
expect(runner.options.adminWebPort).toBe(22003);
expect(runner.options.bgfilterWorkerPort).toBe(22004);
expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001');
expect(runner.state.bgfilterWorkerTarget).toBe('http://127.0.0.1:22004');
expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002');
} finally {
rmSync(tempDir, { recursive: true, force: true });
@@ -196,6 +220,7 @@ describe('dev scheduler argument routing', () => {
expect(runner.options.apiPort).toBe(22001);
expect(runner.options.spacetimePort).toBe(22002);
expect(runner.options.adminWebPort).toBe(22003);
expect(runner.options.bgfilterWorkerPort).toBe(22004);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
@@ -233,6 +258,7 @@ describe('dev scheduler argument routing', () => {
expect(runner.options.apiPort).toBe(8082);
expect(runner.options.spacetimePort).toBe(3101);
expect(runner.options.adminWebPort).toBe(3102);
expect(runner.options.bgfilterWorkerPort).toBe(8083);
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform);
@@ -282,6 +308,46 @@ describe('dev scheduler api-server env', () => {
expect(env.GENARRATIVE_PROCESS_ROLE).toBe('api');
});
test('父 API 与独立 BgFilter worker 共享实际 URL 和内部 token', () => {
const { options } = parseArgs([], {});
options.bgfilterWorkerPort = 18083;
const state = {
spacetimeServer: 'http://127.0.0.1:3199',
bgfilterWorkerTarget: 'http://127.0.0.1:18083',
};
const internalToken = 'local-bgfilter-token';
const apiEnv = buildApiServerProcessEnv({
baseEnv: {},
options,
state,
bgfilterInternalToken: internalToken,
processRole: 'all',
});
const workerEnv = buildBgfilterWorkerProcessEnv({
baseEnv: {},
options,
state,
bgfilterInternalToken: internalToken,
});
expect(apiEnv.GENARRATIVE_PROCESS_ROLE).toBe('all');
expect(workerEnv.GENARRATIVE_PROCESS_ROLE).toBe('bgfilter-worker');
expect(apiEnv.GENARRATIVE_BGFILTER_WORKER_BASE_URL).toBe(
state.bgfilterWorkerTarget,
);
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_BASE_URL).toBe(
state.bgfilterWorkerTarget,
);
expect(apiEnv.GENARRATIVE_BGFILTER_INTERNAL_TOKEN).toBe(internalToken);
expect(workerEnv.GENARRATIVE_BGFILTER_INTERNAL_TOKEN).toBe(internalToken);
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_HOST).toBe('127.0.0.1');
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_PORT).toBe('18083');
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_CONCURRENCY).toBe('16');
expect(workerEnv.GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS).toBe('5000');
expect(workerEnv.GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS).toBe('2048');
});
test('Windows 本地 dev 自动注入已安装的 FFmpeg 路径', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-ffmpeg-'));
try {
@@ -339,6 +405,100 @@ describe('dev scheduler api-server env', () => {
});
});
describe('dev scheduler Rust service orchestration', () => {
test('Rust 双进程重启时先全部停止,再先 ready BgFilter、后 ready API', async () => {
const { explicitOptions, options } = parseArgs([], {});
const runner = new DevRunner(options, {}, explicitOptions);
const events: string[] = [];
runner.command = 'all';
runner.windowsApiServerCleanupCompleted = true;
runner.services = new Map([
[
'api-server',
{
stop: async () => events.push('stop-api'),
start: async () => events.push('start-api'),
},
],
[
'bgfilter-worker',
{
stop: async () => events.push('stop-bgfilter'),
start: async () => events.push('start-bgfilter'),
},
],
]);
vi.spyOn(runner, 'waitForBgfilterWorker').mockImplementation(async () => {
events.push('ready-bgfilter');
});
vi.spyOn(runner, 'waitForApiServer').mockImplementation(async () => {
events.push('ready-api');
});
await runner.restartRustServicePair();
expect(events).toEqual([
'stop-api',
'stop-bgfilter',
'start-bgfilter',
'ready-bgfilter',
'start-api',
'ready-api',
]);
});
test('dev:api-server 安全自动带起同 runner 的 BgFilter worker', async () => {
const { explicitOptions, options } = parseArgs(['api-server'], {});
const runner = new DevRunner(options, {}, explicitOptions);
const startPair = vi
.spyOn(runner, 'startRustServicePair')
.mockResolvedValue(undefined);
const startWatchers = vi
.spyOn(runner, 'startWatchers')
.mockImplementation(() => {});
await runner.startCommand('api-server');
expect(startPair).toHaveBeenCalledOnce();
expect(startWatchers).toHaveBeenCalledWith([
'api-server',
'bgfilter-worker',
]);
});
test('完整栈只为两个 Rust 角色创建一套组合 watcher', () => {
const { explicitOptions, options } = parseArgs(['--watch'], {});
const runner = new DevRunner(options, {}, explicitOptions);
runner.command = 'all';
runner.registerServices();
try {
runner.startWatchers(['api-server', 'bgfilter-worker']);
expect(runner.watchers).toHaveLength(1);
} finally {
for (const watcher of runner.watchers) {
watcher.close();
}
runner.watchers = [];
}
});
test('BgFilter worker 在 readiness 前退出时立即失败', async () => {
const { explicitOptions, options } = parseArgs([], {});
const runner = new DevRunner(options, {}, explicitOptions);
runner.services = new Map([
['bgfilter-worker', { runtime: { status: 'failed' } }],
]);
globalThis.fetch = vi.fn(async () => ({
status: 503,
})) as unknown as typeof fetch;
await expect(runner.waitForBgfilterWorker()).rejects.toThrow(
'bgfilter-worker 在 readiness 前退出',
);
});
});
describe('dev scheduler local worker cleanup', () => {
const expected = {
expectedDatabase: 'xushi-p4wfr',
@@ -471,6 +631,8 @@ describe('dev scheduler stack state file', () => {
options: {
apiHost: '127.0.0.1',
apiPort: 8090,
bgfilterWorkerHost: '127.0.0.1',
bgfilterWorkerPort: 8091,
webHost: '0.0.0.0',
webPort: 3010,
adminWebHost: '127.0.0.1',
@@ -483,6 +645,7 @@ describe('dev scheduler stack state file', () => {
},
state: {
apiTarget: 'http://127.0.0.1:8090',
bgfilterWorkerTarget: 'http://127.0.0.1:8091',
adminWebTargetHost: '127.0.0.1',
spacetimeServer: 'http://127.0.0.1:3120',
},
@@ -530,6 +693,12 @@ describe('dev scheduler stack state file', () => {
port: 8090,
url: 'http://127.0.0.1:8090',
});
expect(snapshot.services['bgfilter-worker']).toMatchObject({
status: 'idle',
pid: null,
port: 8091,
url: 'http://127.0.0.1:8091',
});
});
});
@@ -572,7 +741,7 @@ describe('dev scheduler spacetime reuse guard', () => {
writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8');
writeFileSync(
join(tempDir, 'dev-spacetime-tool-version'),
`${workspaceSpacetimeVersionForTest()}\n`,
`${workspaceSpacetimeVersionForTest()}\nd220349adb7af7eefa810eb08a185609356b83f6\n`,
'utf8',
);
globalThis.fetch = vi.fn(async () => ({
@@ -601,7 +770,7 @@ describe('dev scheduler spacetime reuse guard', () => {
writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8');
writeFileSync(
join(tempDir, 'dev-spacetime-tool-version'),
`${workspaceSpacetimeVersionForTest()}\n`,
`${workspaceSpacetimeVersionForTest()}\nd220349adb7af7eefa810eb08a185609356b83f6\n`,
'utf8',
);
globalThis.fetch = vi.fn(async () => ({
@@ -712,12 +881,34 @@ describe('dev scheduler spacetime refresh', () => {
});
test('解析 spacetime --version 输出里的 tool version', () => {
const version = parseSpacetimeToolVersion(`
const output = `
A new version of SpacetimeDB is available: v2.6.1 (current: v2.5.0)
spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0;
`);
Commit: d220349adb7af7eefa810eb08a185609356b83f6
spacetimedb tool version 2.7.0; spacetimedb-lib version 2.7.0;
`;
expect(version).toBe('2.6.0');
expect(parseSpacetimeToolVersion(output)).toBe('2.7.0');
expect(parseSpacetimeToolCommit(output)).toBe(
'd220349adb7af7eefa810eb08a185609356b83f6',
);
});
test('同为 2.7.0 时拒绝裸 tag 并接受 hotfix3 commit', () => {
expect(() =>
assertSpacetimeToolVersionMatchesWorkspace({
toolVersion: '2.7.0',
toolCommit: 'a08663c7b94688a2542577532d472f751e641f5b',
workspaceVersion: '2.7.0',
}),
).toThrow('裸 tag');
expect(() =>
assertSpacetimeToolVersionMatchesWorkspace({
toolVersion: '2.7.0',
toolCommit: 'd220349adb7af7eefa810eb08a185609356b83f6',
workspaceVersion: '2.7.0',
}),
).not.toThrow();
});
test('本机 spacetime 版本和 workspace 锁定版本不一致时直接报清楚', () => {
@@ -751,6 +942,28 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0;
}
});
test('复用本地 SpacetimeDB standalone 时拒绝同版本裸 tag 记录', () => {
const tempDir = mkdtempSync(
join(tmpdir(), 'genarrative-spacetime-commit-'),
);
try {
writeFileSync(
join(tempDir, 'dev-spacetime-tool-version'),
'2.7.0\na08663c7b94688a2542577532d472f751e641f5b\n',
'utf8',
);
expect(() =>
assertReusableSpacetimeProcessVersionMatchesWorkspace({
dataDir: tempDir,
serverUrl: 'http://127.0.0.1:3101',
}),
).toThrow('裸 tag');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test('本地发布 403 时识别为身份权限问题,避免误杀 standalone', () => {
const error = new Error(
'Pre-publish check failed with status 403 Forbidden: c200... is not authorized to perform action on database c200...: update database',
@@ -1350,6 +1563,8 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0;
'migration-secret-hash',
GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET:
'runtime-secret',
GENARRATIVE_BGFILTER_INTERNAL_TOKEN: 'bgfilter-token',
GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE: 'bgfilter-token-file',
SAFE_VALUE: 'kept',
},
{ RUST_SERVER_TARGET: 'http://127.0.0.1:8082' },
@@ -1365,6 +1580,8 @@ spacetimedb tool version 2.6.0; spacetimedb-lib version 2.6.0;
expect(env).not.toHaveProperty(
'GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET',
);
expect(env).not.toHaveProperty('GENARRATIVE_BGFILTER_INTERNAL_TOKEN');
expect(env).not.toHaveProperty('GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE');
expect(env.SAFE_VALUE).toBe('kept');
});