import {spawn} from 'node:child_process'; import {existsSync, readFileSync} from 'node:fs'; import {resolve} from 'node:path'; const repoRoot = process.cwd(); const shellEnvKeys = new Set(Object.keys(process.env)); function loadEnvFile(path, target) { if (!existsSync(path)) { return; } const rawText = readFileSync(path, 'utf8'); for (const rawLine of rawText.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u); if (!match) { continue; } const [, key, rawValue] = match; // 中文注释:命令行显式传入的目标优先,`.env.local` 再覆盖 `.env`,与 api-server 启动脚本保持一致。 if (shellEnvKeys.has(key)) { continue; } target[key] = rawValue.replace(/^['"]|['"]$/gu, ''); } } const fileEnv = {...process.env}; loadEnvFile(resolve(repoRoot, '.env'), fileEnv); loadEnvFile(resolve(repoRoot, '.env.local'), fileEnv); loadEnvFile(resolve(repoRoot, '.env.secrets.local'), fileEnv); function resolveConfiguredTarget() { if (fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET) { return fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET; } if (fileEnv.RUST_SERVER_TARGET) { return fileEnv.RUST_SERVER_TARGET; } if (fileEnv.GENARRATIVE_API_TARGET) { return fileEnv.GENARRATIVE_API_TARGET; } if (fileEnv.GENARRATIVE_API_PORT) { return `http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT}`; } return ''; } function buildFallbackCandidates() { const candidates = [ 'http://127.0.0.1:3100', 'http://127.0.0.1:8082', ].filter(Boolean); return Array.from(new Set(candidates)); } async function isTargetReachable(target) { const healthUrl = new URL('/healthz', target); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1200); try { const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal, }); return response.ok; } catch { return false; } finally { clearTimeout(timeoutId); } } async function resolveRuntimeTarget() { const configuredTarget = resolveConfiguredTarget(); if (configuredTarget) { return { target: configuredTarget, fallbackUsed: false, targetUnavailable: !(await isTargetReachable(configuredTarget)), }; } for (const target of buildFallbackCandidates()) { if (await isTargetReachable(target)) { return { target, fallbackUsed: true, targetUnavailable: false, }; } } return { target: 'http://127.0.0.1:3100', fallbackUsed: false, targetUnavailable: true, }; } const runtimeTarget = await resolveRuntimeTarget(); if (runtimeTarget.fallbackUsed) { console.warn( `[dev:web] 配置的 Rust target 不可用,已切换到 ${runtimeTarget.target}`, ); } if (runtimeTarget.targetUnavailable) { console.warn( `[dev:web] Rust target 当前不可用: ${runtimeTarget.target},请先启动 api-server。`, ); } const mergedEnv = { ...fileEnv, RUST_SERVER_TARGET: runtimeTarget.target, GENARRATIVE_RUNTIME_SERVER_TARGET: runtimeTarget.target, }; console.log(`[dev:web] backend=rust target=${mergedEnv.GENARRATIVE_RUNTIME_SERVER_TARGET}`); const child = spawn( 'node', ['scripts/vite-cli.mjs', '--port=3000', '--host=0.0.0.0', '--strictPort'], { cwd: process.cwd(), env: mergedEnv, stdio: 'inherit', shell: process.platform === 'win32', }, ); child.on('error', (error) => { console.error(`[dev:web] 启动 Vite 失败: ${error.message}`); process.exit(1); }); child.on('exit', (code, signal) => { if (signal) { console.error(`[dev:web] Vite 被信号终止: ${signal}`); process.exit(1); } process.exit(code ?? 0); });