支持App Run并行开发并修复资源画布
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 3m28s
Project CI / Native shell tests (pull_request) Failing after 9m27s

新增App Run独立开发profile并隔离端口、数据库、Tauri身份与AppData
常驻资源排列切换并避免布局状态挤压操作入口
显式扩展资源画布横向滚动范围并收敛依赖图可见连线
补充启动器、资源布局、界面回归测试与项目文档
This commit is contained in:
2026-08-07 10:13:25 +08:00
parent dc7c7dc6cc
commit 53e37ea361
21 changed files with 533 additions and 458 deletions
@@ -8,17 +8,56 @@ import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const viteHost = '127.0.0.1';
const vitePort = 3080;
const devProfileName = process.env.GENARRATIVE_AGC_DEV_PROFILE || 'default';
function resolveDevStackProfile(name = 'default') {
switch (name) {
case 'default':
return {
name,
viteHost: '127.0.0.1',
vitePort: 3080,
apiPort: 8082,
bgfilterWorkerPort: 8083,
spacetimePort: 3101,
backendDatabase: 'genarrative-game-creator-dev',
backendSpacetimeDataDir: resolve(
repoRoot,
'server-rs/.spacetimedb/ai-game-creator/data',
),
};
case 'app-run':
return {
name,
viteHost: '127.0.0.1',
vitePort: 3081,
apiPort: 8084,
bgfilterWorkerPort: 8085,
spacetimePort: 3103,
backendDatabase: 'genarrative-game-creator-app-run-dev',
backendSpacetimeDataDir: resolve(
repoRoot,
'server-rs/.spacetimedb/ai-game-creator-app-run/data',
),
};
default:
throw new Error(`未知 AI 游戏创作开发 profile: ${name}`);
}
}
const devProfile = resolveDevStackProfile(devProfileName);
const {
viteHost,
vitePort,
apiPort,
bgfilterWorkerPort,
spacetimePort,
backendDatabase,
backendSpacetimeDataDir,
} = devProfile;
const viteUrl = `http://${viteHost}:${vitePort}/`;
const viteMarkerUrl = `${viteUrl}__agc_dev_server.json`;
const defaultApiTarget =
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
const backendDatabase = 'genarrative-game-creator-dev';
const backendSpacetimeDataDir = resolve(
repoRoot,
'server-rs/.spacetimedb/ai-game-creator/data',
);
process.env.RUST_SERVER_TARGET || `http://127.0.0.1:${apiPort}`;
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const childLifecycles = new WeakMap();
@@ -72,6 +111,7 @@ function resolveBackendTargetsFromState(
expectedDatabase = backendDatabase,
expectedSpacetimeDataDir = backendSpacetimeDataDir,
fallbackApiTarget = defaultApiTarget,
fallbackSpacetimeTarget = `http://127.0.0.1:${spacetimePort}`,
} = {},
) {
const apiServer = state?.services?.['api-server'];
@@ -100,7 +140,7 @@ function resolveBackendTargetsFromState(
? spacetime.url
: requireAgcBackend
? ''
: 'http://127.0.0.1:3101';
: fallbackSpacetimeTarget;
return {
apiUrl,
spacetimeUrl,
@@ -131,13 +171,24 @@ async function isBackendReady() {
);
}
async function readExistingViteServer() {
return httpGetText(viteUrl);
function resolveViteProfileUrls(profile = devProfile) {
const profileViteUrl = `http://${profile.viteHost}:${profile.vitePort}/`;
return {
viteUrl: profileViteUrl,
viteMarkerUrl: `${profileViteUrl}__agc_dev_server.json`,
};
}
function isVitePortListening() {
async function readExistingViteServer(profile = devProfile) {
return httpGetText(resolveViteProfileUrls(profile).viteUrl);
}
function isVitePortListening(profile = devProfile) {
return new Promise((resolveRequest) => {
const socket = net.connect({ host: viteHost, port: vitePort });
const socket = net.connect({
host: profile.viteHost,
port: profile.vitePort,
});
socket.once('connect', () => {
socket.destroy();
resolveRequest(true);
@@ -160,8 +211,11 @@ function isAiGameCreatorServer(response) {
);
}
async function readExistingViteMarker() {
const response = await httpGetText(viteMarkerUrl, 2000);
async function readExistingViteMarker(profile = devProfile) {
const response = await httpGetText(
resolveViteProfileUrls(profile).viteMarkerUrl,
2000,
);
if (!response || response.statusCode !== 200) {
return null;
}
@@ -173,15 +227,17 @@ async function readExistingViteMarker() {
}
async function preflightExistingVite({
readServer = readExistingViteServer,
portListening = isVitePortListening,
readMarker = readExistingViteMarker,
profile = devProfile,
readServer = () => readExistingViteServer(profile),
portListening = () => isVitePortListening(profile),
readMarker = () => readExistingViteMarker(profile),
} = {}) {
const { viteUrl: profileViteUrl } = resolveViteProfileUrls(profile);
const existing = await readServer();
if (!existing) {
if (await portListening()) {
throw new Error(
`${viteUrl} is already in use by a non-HTTP or unrecognized server. Stop it before starting Tauri dev.`,
`${profileViteUrl} is already in use by a non-HTTP or unrecognized server. Stop it before starting Tauri dev.`,
);
}
return { status: 'available', apiTarget: '' };
@@ -189,7 +245,7 @@ async function preflightExistingVite({
if (!isAiGameCreatorServer(existing)) {
throw new Error(
`${viteUrl} is already in use by another server. Stop it before starting Tauri dev.`,
`${profileViteUrl} is already in use by another server. Stop it before starting Tauri dev.`,
);
}
@@ -202,7 +258,7 @@ async function preflightExistingVite({
: '';
const actualTarget = markerApiTarget || 'unknown';
throw new Error(
`${viteUrl} is already running with API target ${actualTarget}. Its owning worktree cannot be proven, so it will not be reused. Stop that Vite dev server before starting Tauri dev.`,
`${profileViteUrl} is already running with API target ${actualTarget}. Its owning worktree cannot be proven, so it will not be reused. Stop that Vite dev server before starting Tauri dev.`,
);
}
@@ -507,27 +563,50 @@ async function waitForBackendReady(backendChild, timeoutMs = 600_000) {
throw new Error('等待配套后端和数据库启动超时');
}
function buildBackendStartArguments(profile = devProfile) {
return [
'--prefix',
'../..',
'run',
'agc:backend',
'--',
'--database',
profile.backendDatabase,
'--spacetime-data-dir',
profile.backendSpacetimeDataDir,
'--api-port',
String(profile.apiPort),
'--bgfilter-worker-port',
String(profile.bgfilterWorkerPort),
'--spacetime-port',
String(profile.spacetimePort),
'--no-interactive',
];
}
function buildViteStartArguments(profile = devProfile) {
return [
'--prefix',
'../..',
'exec',
'vite',
'--',
'--config',
'vite.config.ts',
'--host',
profile.viteHost,
'--port',
String(profile.vitePort),
'--strictPort',
];
}
async function ensureBackend({
onBackendChild = () => {},
checkBackendReady = isBackendReady,
resolveTargets = readBackendTargets,
spawnBackend = () =>
spawnChild(
npm,
[
'--prefix',
'../..',
'run',
'agc:backend',
'--',
'--database',
backendDatabase,
'--spacetime-data-dir',
backendSpacetimeDataDir,
'--no-interactive',
],
{ cwd: appRoot },
),
spawnChild(npm, buildBackendStartArguments(), { cwd: appRoot }),
waitUntilReady = waitForBackendReady,
} = {}) {
if (await checkBackendReady()) {
@@ -569,11 +648,7 @@ async function startVite(apiTarget) {
);
}
return spawnChild(
npm,
['--prefix', '../..', 'exec', 'vite', '--', '--config', 'vite.config.ts'],
{ cwd: appRoot },
);
return spawnChild(npm, buildViteStartArguments(), { cwd: appRoot });
}
async function main() {
@@ -593,6 +668,9 @@ async function main() {
}
try {
console.log(
`[ai-game-creator-shell] dev profile ${devProfile.name}: vite=${viteUrl} api=${apiPort} spacetime=${spacetimePort}`,
);
await preflightExistingVite();
const backend = await ensureBackend({
onBackendChild(child) {
@@ -650,6 +728,8 @@ function isDirectModuleExecution() {
}
export {
buildBackendStartArguments,
buildViteStartArguments,
ensureBackend,
formatChildFailure,
isDirectModuleExecution,
@@ -657,6 +737,7 @@ export {
preflightExistingVite,
readChildFailure,
readLinuxProcessGroupAlive,
resolveDevStackProfile,
resolveBackendTargetsFromState,
runWindowsTaskkill,
spawnChild,