Files
Genarrative/apps/ai-game-creator-shell/scripts/dev-port.mjs
T
kdletters 2bdee990a2 AGC 开发态补齐后台 Web 启动与端口汇总,端口归属探测从 WMI 换成 netstat
- dev-port.mjs 新增 resolveAgcAdminWebEndpoint:后台 Web 端口按 Linux 用户端口段 start + 3、非 Linux 优先 3102 解析,ADMIN_WEB_PORT 可显式指定,并把已解析的 AGC Vite 端口列为保留端口
- start-dev-stack.mjs 随 AGC 一起拉起 apps/admin-web(AGC_DEV_ADMIN_WEB=0 关闭):与 AGC Vite 一样由启动器直接持有并纳入信号与退出收束,不走会整体重写 .app/dev-stack.json 的 dev:admin-web
- start-dev-stack.mjs 新增启动汇总行,一次给出前端、后端、后台、数据库与 bgfilter-worker 的实际地址,端口漂移后以该行为准
- start-dev-stack.mjs 端口归属探测改用 netstat -ano 取端口到 PID、.NET Process 读可执行文件路径,仅在核对 SpacetimeDB --data-dir 归属时按 PID 取命令行并做 5 分钟 TTL 缓存:本机实测单轮探测由约 43 秒降到 0.37 秒,agc:serve 的 starting backend stack 到 backend ready 由约 80 秒降到 16.7 秒
- 后台 Web 端口解析或启动失败只告警,不阻断也不连带停止 AGC 客户端与配套后端
- tests/dev-port.test.ts、tests/start-dev-stack.test.ts 补充用例覆盖后台 Web 端口解析与严格占用、失败软化、启动汇总、netstat 探测脚本与命令行缓存失效
- docs/【开发运维】与 docs/technical 同步 AGC 开发态启动口径,pitfalls.md 记录 WMI 慢速探测的实测数据、netstat 改法与 PID 取最后一列的易错点
2026-09-14 21:02:08 +08:00

203 lines
5.7 KiB
JavaScript

import {
findAvailablePort,
formatPortDecision,
mapDevPortsToPortRange,
normalizePort,
reserveLinuxDevPortRange,
} from '../../../scripts/dev-stack-port-utils.mjs';
const agcDevHost = '127.0.0.1';
const legacyAgcDevPort = 3080;
const agcVitePortEnvKey = 'GENARRATIVE_AGC_VITE_PORT';
const agcAdminWebHost = '127.0.0.1';
const legacyAgcAdminWebPort = 3102;
// 与 scripts/dev.mjs 的后台 Web 端口配置保持同一环境变量名。
const agcAdminWebPortEnvKey = 'ADMIN_WEB_PORT';
function readConfiguredAgcDevPort(env = process.env) {
const rawPort = String(env[agcVitePortEnvKey] ?? '').trim();
if (!rawPort) {
return null;
}
const port = normalizePort(rawPort, -1);
if (port < 1024) {
throw new Error(`${agcVitePortEnvKey} 必须是 1024-65535 的有效端口`);
}
return port;
}
function createAgcDevEndpoint(port, portRange = null) {
const url = `http://${agcDevHost}:${port}/`;
return {
host: agcDevHost,
port,
url,
markerUrl: `${url}__agc_dev_server.json`,
portRange,
};
}
function readAgcDevEndpoint(env = process.env) {
return createAgcDevEndpoint(
readConfiguredAgcDevPort(env) ?? legacyAgcDevPort,
);
}
async function resolveAgcDevEndpoint({
env = process.env,
platform = process.platform,
strictConfigured = false,
reservePortRange = reserveLinuxDevPortRange,
findPort = findAvailablePort,
} = {}) {
const configuredPort = readConfiguredAgcDevPort(env);
let portRange = null;
let preferredPort = configuredPort ?? legacyAgcDevPort;
if (platform === 'linux') {
const allocation = await reservePortRange({ env });
if (!allocation?.range) {
throw new Error('无法取得当前 Linux 用户的 dev 端口段');
}
portRange = allocation.range;
const mappedAgcVitePort = mapDevPortsToPortRange(portRange)?.agcVitePort;
if (!Number.isInteger(mappedAgcVitePort)) {
throw new Error(
`当前 Linux dev 端口段 ${portRange.label} 缺少 AGC Vite 槽位;请先迁移为至少 6 个端口且不与其它用户重叠的端口段`,
);
}
if (configuredPort != null && configuredPort < mappedAgcVitePort) {
throw new Error(
`${agcVitePortEnvKey} ${configuredPort} 占用了 Linux dev 端口段 ${portRange.label} 的前五个服务槽位`,
);
}
preferredPort = configuredPort ?? mappedAgcVitePort;
}
const port = await findPort({
host: agcDevHost,
preferredPort,
portRange,
strict: strictConfigured && configuredPort != null,
});
console.log(
formatPortDecision({
name: 'ai-game-creator-shell',
host: agcDevHost,
preferredPort,
resolvedPort: port,
}),
);
if (portRange) {
console.log(`[ai-game-creator-shell] dev port-range: ${portRange.label}`);
}
return createAgcDevEndpoint(port, portRange);
}
function withAgcDevEndpointEnv(endpoint, env = process.env) {
return {
...env,
[agcVitePortEnvKey]: String(endpoint.port),
};
}
function readConfiguredAgcAdminWebPort(env = process.env) {
const rawPort = String(env[agcAdminWebPortEnvKey] ?? '').trim();
if (!rawPort) {
return null;
}
const port = normalizePort(rawPort, -1);
if (port < 1024) {
throw new Error(`${agcAdminWebPortEnvKey} 必须是 1024-65535 的有效端口`);
}
return port;
}
function createAgcAdminWebEndpoint(port, portRange = null) {
const origin = `http://${agcAdminWebHost}:${port}`;
return {
host: agcAdminWebHost,
port,
origin,
basePath: '/admin/',
url: `${origin}/admin/`,
portRange,
};
}
// AGC 开发态的后台 Web 与 `npm run dev` 的后台 Vite 共用同一套优先端口约定:
// Linux 取当前用户端口段的 `start + 3` 槽位,非 Linux 保留 `3102` 兼容首选并允许统一漂移。
async function resolveAgcAdminWebEndpoint({
env = process.env,
platform = process.platform,
strictConfigured = false,
reservedPorts = [],
reservePortRange = reserveLinuxDevPortRange,
findPort = findAvailablePort,
} = {}) {
const configuredPort = readConfiguredAgcAdminWebPort(env);
let portRange = null;
let preferredPort = configuredPort ?? legacyAgcAdminWebPort;
if (platform === 'linux') {
const allocation = await reservePortRange({ env });
if (!allocation?.range) {
throw new Error('无法取得当前 Linux 用户的 dev 端口段');
}
portRange = allocation.range;
const mappedAdminWebPort = mapDevPortsToPortRange(portRange)?.adminWebPort;
if (!Number.isInteger(mappedAdminWebPort)) {
throw new Error(
`当前 Linux dev 端口段 ${portRange.label} 缺少后台 Web 槽位;请先迁移为至少 6 个端口且不与其它用户重叠的端口段`,
);
}
preferredPort = configuredPort ?? mappedAdminWebPort;
}
const reservedPortSet = new Set(
reservedPorts.filter((value) => Number.isInteger(value) && value > 0),
);
const port = await findPort({
host: agcAdminWebHost,
preferredPort,
portRange,
reservedPorts: reservedPortSet,
strict: strictConfigured && configuredPort != null,
});
console.log(
formatPortDecision({
name: 'ai-game-creator-shell-admin-web',
host: agcAdminWebHost,
preferredPort,
resolvedPort: port,
}),
);
if (portRange) {
console.log(
`[ai-game-creator-shell] admin-web port-range: ${portRange.label}`,
);
}
return createAgcAdminWebEndpoint(port, portRange);
}
export {
agcAdminWebHost,
agcAdminWebPortEnvKey,
agcDevHost,
agcVitePortEnvKey,
createAgcAdminWebEndpoint,
createAgcDevEndpoint,
legacyAgcAdminWebPort,
legacyAgcDevPort,
readAgcDevEndpoint,
readConfiguredAgcAdminWebPort,
readConfiguredAgcDevPort,
resolveAgcAdminWebEndpoint,
resolveAgcDevEndpoint,
withAgcDevEndpointEnv,
};