合并最新master分支

合入 origin/master 最新画布、外部接口与后端模块化改动。

按新队列语义解决画布测试和原生壳检查冲突。

把编辑器 Agent 迁移到模块化实现并适配 LlmRunRequest。

删除已废弃的旧编辑器 Agent 单文件与个人 Claude 本地配置。
This commit is contained in:
AIGameCreator App
2026-07-20 16:08:24 +08:00
443 changed files with 22979 additions and 9298 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ const repoRoot = path.resolve(
'..',
);
const database = 'admin-account-smoke';
const expectedSpacetimeVersion = '2.6.0';
const expectedSpacetimeVersion = '2.6.1';
const commandTimeoutMs = 5 * 60 * 1000;
function assert(condition, message) {
File diff suppressed because it is too large Load Diff
+6 -61
View File
@@ -1411,47 +1411,7 @@ const desktopCapabilityFlowContracts = [
],
},
];
const h5NativeAppRouteFlowContracts = [
{
route: '/child-motion-demo',
label: 'child motion demo',
files: [
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
'src/routing/appRoutes.tsx',
'src/routing/appRoutes.test.ts',
'src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx',
],
snippets: [
[
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
"navigateHostNativePage('/child-motion-demo')",
],
[
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
"window.location.assign('/child-motion-demo')",
],
['src/routing/appRoutes.tsx', "normalizedPath === '/child-motion-demo'"],
['src/routing/appRoutes.test.ts', "matchAppRoute('/child-motion-demo')"],
[
'src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx',
"request.method === 'navigation.openNativePage'",
],
[
'src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx',
"url === '/child-motion-demo'",
],
],
tests: [
'src/routing/appRoutes.test.ts',
],
targetedTests: [
{
filePath: 'src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx',
name: 'native app opens child motion demo through host navigation bridge',
},
],
},
];
const h5NativeAppRouteFlowContracts = [];
const expectedWechatHostBridgeFiles = [
'dispatch.js',
'payment.js',
@@ -1758,8 +1718,6 @@ const h5HostBridgeTests = [
'src/services/clipboard.test.ts',
'src/services/appTitle.test.ts',
];
const h5PlatformHostBridgeIntegrationTest =
'native app jump hop draft completion sends host notification and badge from platform shell';
const h5NativeAppRouteFlowTestSteps = h5NativeAppRouteFlowContracts.flatMap(
(contract) =>
(contract.targetedTests ?? []).map((test) => ({
@@ -1790,18 +1748,6 @@ const steps = [
command: npmCommand,
args: ['run', 'test', '--', ...h5HostBridgeTests],
},
{
label: 'h5-platform-host-bridge-integration',
command: npmCommand,
args: [
'run',
'test',
'--',
'src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx',
'-t',
h5PlatformHostBridgeIntegrationTest,
],
},
...h5NativeAppRouteFlowTestSteps,
{
label: 'wechat-shell-tests',
@@ -1833,6 +1779,11 @@ const steps = [
command: npmCommand,
args: ['run', 'mobile-shell:export'],
},
{
label: 'desktop-shell-test',
command: npmCommand,
args: ['run', 'desktop-shell:test'],
},
{
label: 'desktop-shell-typecheck',
command: npmCommand,
@@ -1848,11 +1799,6 @@ const steps = [
command: npmCommand,
args: ['run', 'ai-game-creator-shell:build', '--', '--no-bundle'],
},
{
label: 'desktop-shell-test',
command: npmCommand,
args: ['run', 'desktop-shell:test'],
},
{
label: 'desktop-shell-release-build-smoke',
command: npmCommand,
@@ -2586,7 +2532,6 @@ function assertH5HostBridgePayloadBoundaries() {
'HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS',
'HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS',
'HOST_BRIDGE_SCANNER_TIMEOUT_MS',
'HOST_BRIDGE_APP_TITLE_MAX_LENGTH',
]) {
if (!h5HostBridgeSource.includes(`${sharedBoundary},`)) {
throw new Error(
@@ -1,18 +1,18 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
chmodSync,
copyFileSync,
existsSync,
symlinkSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import path from 'node:path';
@@ -3,7 +3,6 @@
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
+1 -1
View File
@@ -3,8 +3,8 @@
import { spawnSync } from 'node:child_process';
import {
existsSync,
mkdtempSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
+3 -3
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env node
import { createHash, randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import http from 'node:http';
import https from 'node:https';
import net from 'node:net';
import tls from 'node:tls';
import { createHash, randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import nodePath from 'node:path';
import tls from 'node:tls';
const DEFAULT_TIMEOUT_MS = 5000;
const DEFAULT_SPACETIME_DATABASE = 'genarrative-prod';
+1 -1
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { accessSync, constants, readFileSync } from 'node:fs';
import net from 'node:net';
import { spawnSync } from 'node:child_process';
import { userInfo } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -3,7 +3,6 @@
import { spawnSync } from 'node:child_process';
import {
chmodSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
@@ -7,8 +7,8 @@ import {
existsSync,
mkdirSync,
mkdtempSync,
realpathSync,
readFileSync,
realpathSync,
rmSync,
statSync,
symlinkSync,
@@ -4,8 +4,8 @@ import { spawnSync } from 'node:child_process';
import {
chmodSync,
existsSync,
mkdtempSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
+116
View File
@@ -13,11 +13,56 @@ const checks = [
includes: 'npm run check:rustfmt',
reason: 'API 生产构建必须在编译前执行 Rust 格式检查。',
},
{
file: 'jenkins/Jenkinsfile.production-api-build',
includes: 'npm run check:server-rs-ddd',
reason: 'API 生产构建必须执行 server-rs DDD/schema/runtime-access 门禁。',
},
{
file: 'jenkins/Jenkinsfile.production-api-build',
includes: 'cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml',
reason: 'API 生产构建必须执行 api-server 全 target 编译检查。',
},
{
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
includes: 'npm run check:rustfmt',
reason: 'Stdb module 生产构建必须在编译前执行 Rust 格式检查。',
},
{
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
includes: 'npm run check:server-rs-ddd',
reason: 'Stdb module 生产构建必须执行 DDD/schema/runtime-access 门禁。',
},
{
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
includes: 'npm run check:admin-account-procedures',
reason: 'Stdb module 生产构建必须执行管理员 procedure smoke。',
},
{
file: 'jenkins/Jenkinsfile.production-web-build',
includes: 'npm run check:production-ops',
reason: 'Web 生产构建必须执行生产运维静态门禁。',
},
{
file: 'jenkins/Jenkinsfile.production-web-build',
includes: 'npm run lint:eslint',
reason: 'Web 生产构建必须执行 ESLint 门禁。',
},
{
file: 'jenkins/Jenkinsfile.production-web-build',
includes: 'npm run typecheck',
reason: 'Web 生产构建必须执行主站类型检查。',
},
{
file: 'jenkins/Jenkinsfile.production-web-build',
includes: 'npm run admin-web:typecheck',
reason: 'Web 生产构建必须执行后台类型检查。',
},
{
file: 'jenkins/Jenkinsfile.production-web-build',
includes: 'npm run test',
reason: 'Web 生产构建必须执行当前维护范围内的 Vitest。',
},
{
file: 'server-rs/crates/spacetime-module/src/migration.rs',
includes:
@@ -514,6 +559,76 @@ const checks = [
reason:
'生产冷备份 service 必须用 node -- 分隔脚本参数,避免 Node 22 抢占业务 --env-file。',
},
{
file: 'deploy/systemd/genarrative-database-backup.service',
excludes: '--storage-format files',
reason: '生产数据库备份主 service 必须继续保持 archive full 默认行为。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
includes: 'ExecStart=\nExecStart=/usr/bin/node -- /opt/genarrative/current/scripts/database-backup-to-oss.mjs --env-file',
reason: 'files-history drop-in 必须先清空主 service 的 ExecStart,并使用 current release 脚本。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
includes: '--storage-format files --mode history --work-dir /var/lib/genarrative/database-backups/files-history',
reason: 'files-history drop-in 必须只执行逐文件历史归档,并复用已建立基线的独立 work-dir。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--stop-service',
reason: 'files-history 在线归档不可停止 SpacetimeDB。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--database',
reason: 'files-history drop-in 不得写死数据库名,必须从环境文件读取。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--bucket',
reason: 'files-history drop-in 不得写死 OSS bucket,必须从环境文件读取。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'DATABASE_BACKUP_PROFILE="${DATABASE_BACKUP_PROFILE:-archive-full}"',
reason: 'Server-Provision 的数据库备份 profile 必须默认保持 archive-full。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'archive-full|files-history)',
reason: 'Server-Provision 必须拒绝未知数据库备份 profile。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: '--storage-format files --mode history --work-dir',
reason: 'Server-Provision 启用 files-history 前必须用真实 current release 脚本执行只读 baseline 预检。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'run_cmd rm -f "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" "${DATABASE_BACKUP_LEGACY_DEV_DROP_IN}"',
reason: 'Server-Provision 切回 archive-full 时必须移除托管及现场遗留的 history drop-in。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'install_file "${rendered_drop_in}" "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" 0644',
reason: 'Server-Provision 必须从仓库模板安装托管的 files-history drop-in。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
includes: "choice(name: 'DATABASE_BACKUP_PROFILE', choices: ['archive-full', 'files-history']",
reason: 'Server-Provision Job 必须显式暴露 archive-first 的数据库备份 profile。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
includes: "string(name: 'DATABASE_BACKUP_FILES_HISTORY_WORK_DIR', defaultValue: '/var/lib/genarrative/database-backups/files-history'",
reason: 'Server-Provision Job 必须允许 dev/release 为 files-history 选择各自的 baseline state 目录。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
excludes: "params.DEPLOY_TARGET == 'release' && databaseBackupProfile == 'files-history'",
reason: 'release 必须能在显式选择 profile 且 baseline 预检通过后启用 files-history。',
},
{
file: 'scripts/database-backup-to-oss.mjs',
includes: 'assertSufficientWorkDirSpace({dataDir, workDir, args, env})',
@@ -6837,6 +6952,7 @@ const checks = [
const nodeEnvFileCommandFiles = [
'package.json',
'deploy/systemd/genarrative-database-backup.service',
'deploy/systemd/genarrative-database-backup-files-history.conf',
'scripts/deploy/production-stdb-publish.sh',
'scripts/deploy/pingora-direct-enable.sh',
'scripts/deploy/pingora-direct-rollback.sh',
+3 -3
View File
@@ -37,11 +37,11 @@ chmod +x "${TARGET_BIN_DIR}/otelcol-contrib"
cat >"${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-cli" <<'EOF'
#!/usr/bin/env bash
echo "spacetimedb-cli 2.6.0"
echo "spacetimedb-cli 2.6.1"
EOF
cat >"${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-standalone" <<'EOF'
#!/usr/bin/env bash
echo "spacetimedb-standalone 2.6.0"
echo "spacetimedb-standalone 2.6.1"
EOF
chmod +x \
"${SPACETIME_ROOT_DIR}/bin/current/spacetimedb-cli" \
@@ -58,7 +58,7 @@ if ! (
OTELCOL_TARGET_BIN="${TARGET_BIN_DIR}/otelcol-contrib" \
OTELCOL_VERSION="0.151.0" \
SPACETIME_ROOT="${SPACETIME_ROOT_DIR}" \
SPACETIME_EXPECTED_VERSION="2.6.0" \
SPACETIME_EXPECTED_VERSION="2.6.1" \
"${REPO_ROOT}/scripts/prepare-server-provision-tools.sh" \
>"${OUTPUT_LOG}" 2>&1
); then
+4 -1
View File
@@ -146,8 +146,11 @@ function collectMigrationTables() {
return new Set();
}
const migrationTableBody = macroMatch[1]
.replace(/\/\*[\s\S]*?\*\//gu, ' ')
.replace(/\/\/[^\r\n]*/gu, ' ');
return new Set(
[...macroMatch[1].matchAll(/\b([a-z][a-z0-9_]*)\b/gu)]
[...migrationTableBody.matchAll(/\b([a-z][a-z0-9_]*)\b/gu)]
.map((match) => match[1])
.filter((name) => !['arg'].includes(name)),
);
@@ -108,6 +108,12 @@ const forbiddenSnippets = [
snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {',
reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/editor_project_storage.rs',
snippet: 'require_editor_generation_runtime_service_identity(ctx, ctx.sender())',
reason:
'procedure runtime writer 鉴权必须显式传 caller,兼容 TxContext sender 为 Identity::ZERO 的 SpacetimeDB 2.6.0',
},
];
const procedureResultFiles = [
+4 -4
View File
@@ -481,9 +481,9 @@ function getChangedFiles(baseRef) {
tryGit(['ls-files', '--others', '--exclude-standard', '-z', bindingsRoot]) ?? '';
return new Set(
[
...diffOutput.split(/\u0000/u),
...untrackedModuleOutput.split(/\u0000/u),
...untrackedBindingsOutput.split(/\u0000/u),
...diffOutput.split(String.fromCharCode(0)),
...untrackedModuleOutput.split(String.fromCharCode(0)),
...untrackedBindingsOutput.split(String.fromCharCode(0)),
]
.map(normalizePath)
.filter(Boolean),
@@ -575,7 +575,7 @@ function compareTables(baseTables, currentTables) {
}
}
for (const [accessor, table] of currentTables) {
for (const accessor of currentTables.keys()) {
if (!baseTables.has(accessor)) {
schemaChanged = true;
}
@@ -180,10 +180,6 @@ function hasExternalPlatformMarker(line) {
return externalPlatformPatterns.some((pattern) => pattern.test(line));
}
function formatHits(hits) {
return hits.map((hit) => ` - ${hit.file}:${hit.lineNumber} ${hit.text}`);
}
const documentFiles = collectFiles(documentTargets);
const visualNovelFiles = collectFiles(visualNovelImplementationTargets);
+1 -1
View File
@@ -761,7 +761,7 @@ function composeEnv() {
}
function localSpacetimeImageName() {
return `${projectName}-spacetimedb:2.6.0`;
return `${projectName}-spacetimedb:2.6.1`;
}
function spacetimeServerUrl(state) {
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
#!/usr/bin/env node
import {
chownSync,
chmodSync,
chownSync,
lstatSync,
mkdtempSync,
readFileSync,
@@ -2,8 +2,8 @@
import { spawnSync } from 'node:child_process';
import {
chownSync,
chmodSync,
chownSync,
lstatSync,
mkdtempSync,
readFileSync,
+2 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { execFileSync, spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import {
accessSync,
@@ -15,9 +16,8 @@ import {
rmSync,
statSync,
} from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import path from 'node:path';
import { userInfo } from 'node:os';
import path from 'node:path';
const DEFAULT_SERVICE_USER = 'genarrative';
const DEFAULT_SERVICE_GROUP = 'genarrative';
@@ -1,11 +1,12 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import {
callSpacetimeProcedureViaCli,
ensureProcedureOk,
validateSpacetimeDatabaseName,
} from '../spacetime-migration-common.mjs';
import { spawn } from 'node:child_process';
const PROCEDURE = 'rotate_editor_generation_runtime_service_identity_and_return';
+2 -2
View File
@@ -8,9 +8,9 @@ import {
rmSync,
writeFileSync,
} from 'node:fs';
import {createServer} from 'node:net';
import {hostname, userInfo} from 'node:os';
import {dirname, resolve} from 'node:path';
import {createServer} from 'node:net';
const LINUX_DEV_PORT_RANGE_REGISTRY_ROOT = '/var/tmp/genarrative-dev-port-ranges';
const LINUX_DEV_PORT_RANGE_POOL_START = 10000;
@@ -308,7 +308,7 @@ function nextLinuxPortRangeCandidate(registry) {
async function withLinuxPortRangeRegistryLock(lockPath, action) {
ensureWorldWritableDirectory(dirname(lockPath));
while (true) {
for (;;) {
try {
writeFileSync(
lockPath,
+25 -4
View File
@@ -1,8 +1,10 @@
import {createServer} from 'node:net';
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {createServer} from 'node:net';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, expect, it} from 'vitest';
import {
findAvailablePort,
mapDevPortsToPortRange,
@@ -23,6 +25,27 @@ function reservePort(port) {
});
}
async function reserveConsecutivePorts() {
for (let attempt = 0; attempt < 20; attempt += 1) {
const firstServer = await reservePort(0);
const firstPort = firstServer.address().port;
if (firstPort === 65535) {
await new Promise((resolve) => firstServer.close(resolve));
continue;
}
try {
const secondServer = await reservePort(firstPort + 1);
return {firstServer, secondServer, firstPort};
} catch {
await new Promise((resolve) => firstServer.close(resolve));
}
}
throw new Error('无法为测试保留两个连续端口');
}
describe('dev stack port utils', () => {
it('解析端口段并映射到四个 dev 端口', () => {
expect(parsePortRangeSpec('10000-10099')).toEqual({
@@ -39,9 +62,7 @@ describe('dev stack port utils', () => {
});
it('使用端口可用性检查为被占用端口寻找后续可用端口', async () => {
const firstServer = await reservePort(0);
const firstPort = firstServer.address().port;
const secondServer = await reservePort(firstPort + 1);
const {firstServer, secondServer, firstPort} = await reserveConsecutivePorts();
try {
const availablePort = await findAvailablePort({

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