187 lines
5.0 KiB
JavaScript
187 lines
5.0 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { ensureArgon2Sync } from './argon2-polyfill.mjs';
|
|
import { loadProjectEnv } from './load-env.mjs';
|
|
import { parseCookies } from './local-auth.mjs';
|
|
import { createPlanSyncService } from './plan-sync.mjs';
|
|
|
|
ensureArgon2Sync();
|
|
import { bootstrapAdminServices } from './bootstrap.mjs';
|
|
import { createAdminApp } from './app.mjs';
|
|
|
|
const projectRoot = loadProjectEnv();
|
|
const pidFile = path.join(projectRoot, '.adm-api.pid');
|
|
|
|
const port = Number(process.env.ADM_API_PORT ?? 8085);
|
|
|
|
function readPidFile() {
|
|
try {
|
|
const raw = fs.readFileSync(pidFile, 'utf8').trim();
|
|
if (!raw) return null;
|
|
const pid = Number(raw);
|
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isProcessAlive(pid) {
|
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function cleanupStalePidFile() {
|
|
const existingPid = readPidFile();
|
|
if (!existingPid || existingPid === process.pid) return;
|
|
if (isProcessAlive(existingPid)) return;
|
|
try {
|
|
fs.unlinkSync(pidFile);
|
|
console.warn(`Removed stale PID file ${pidFile} (pid ${existingPid})`);
|
|
} catch {}
|
|
}
|
|
|
|
function writePidFile() {
|
|
try {
|
|
fs.writeFileSync(pidFile, `${process.pid}\n`, 'utf8');
|
|
} catch (err) {
|
|
console.warn(`Failed to write PID file ${pidFile}:`, err);
|
|
}
|
|
}
|
|
|
|
function removePidFile() {
|
|
const existingPid = readPidFile();
|
|
if (existingPid !== process.pid) return;
|
|
try {
|
|
fs.unlinkSync(pidFile);
|
|
} catch {}
|
|
}
|
|
|
|
function lookupPortOwner(listenPort) {
|
|
try {
|
|
const out = execFileSync('lsof', ['-nP', '-iTCP:' + listenPort, '-sTCP:LISTEN', '-Fpc'], {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
});
|
|
let pid = null;
|
|
let command = null;
|
|
for (const line of out.split('\n')) {
|
|
if (line.startsWith('p')) pid = line.slice(1);
|
|
if (line.startsWith('c')) command = line.slice(1);
|
|
if (pid && command) break;
|
|
}
|
|
if (!pid) return null;
|
|
return command ? `${command} (pid ${pid})` : `pid ${pid}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
cleanupStalePidFile();
|
|
|
|
const ready = bootstrapAdminServices();
|
|
const planSyncService = createPlanSyncService(console);
|
|
const services = {
|
|
ready,
|
|
parseCookies,
|
|
planSyncService,
|
|
};
|
|
|
|
ready
|
|
.then((bootstrapped) => {
|
|
const {
|
|
pool,
|
|
userAuth,
|
|
llmProviderService,
|
|
assetGatewayConfigService,
|
|
plazaOps,
|
|
createOpsApi,
|
|
wechatAdmin,
|
|
loadMindSpaceConfig,
|
|
updateMindSpaceConfig,
|
|
memoryV2ConfigService,
|
|
mindSearchConfigService,
|
|
orchestratorConfigService,
|
|
orchestratorObservabilityService,
|
|
personalMemoryCandidateStore,
|
|
skillRuntimeConfigService,
|
|
wechatScheduleLlmConfigService,
|
|
adminSystemTestService,
|
|
systemTestAccountService,
|
|
wordFilterService,
|
|
planCatalogService,
|
|
subscriptionService,
|
|
planSyncService,
|
|
USER_COOKIE,
|
|
userLoginCookies,
|
|
clearUserSessionCookie,
|
|
resolveCookieDomainForRequest,
|
|
} = bootstrapped;
|
|
Object.assign(services, {
|
|
pool,
|
|
userAuth,
|
|
llmProviderService,
|
|
assetGatewayConfigService,
|
|
plazaOps,
|
|
createOpsApi,
|
|
wechatAdmin,
|
|
loadMindSpaceConfig,
|
|
updateMindSpaceConfig,
|
|
memoryV2ConfigService,
|
|
mindSearchConfigService,
|
|
orchestratorConfigService,
|
|
orchestratorObservabilityService,
|
|
personalMemoryCandidateStore,
|
|
skillRuntimeConfigService,
|
|
wechatScheduleLlmConfigService,
|
|
adminSystemTestService,
|
|
systemTestAccountService,
|
|
wordFilterService,
|
|
planCatalogService,
|
|
subscriptionService,
|
|
planSyncService,
|
|
USER_COOKIE,
|
|
userLoginCookies,
|
|
clearUserSessionCookie,
|
|
resolveCookieDomainForRequest,
|
|
});
|
|
const app = createAdminApp(services);
|
|
const host = process.env.ADM_API_HOST?.trim() || '127.0.0.1';
|
|
const server = app.listen(port, host, () => {
|
|
writePidFile();
|
|
console.log(`TKMind Admin API @ http://${host}:${port}`);
|
|
});
|
|
server.on('close', removePidFile);
|
|
server.on('error', (err) => {
|
|
if (err?.code === 'EADDRINUSE') {
|
|
const owner = lookupPortOwner(port);
|
|
console.error(
|
|
`Admin API 无法启动:${host}:${port} 已被占用${owner ? `,当前占用者是 ${owner}` : ''}。`,
|
|
);
|
|
console.error(`如需重启,可先停止旧进程,或修改 .env 中的 ADM_API_PORT。`);
|
|
} else {
|
|
console.error('Admin API listen failed:', err);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
process.on('exit', removePidFile);
|
|
process.on('SIGINT', () => {
|
|
removePidFile();
|
|
server.close(() => process.exit(0));
|
|
});
|
|
process.on('SIGTERM', () => {
|
|
removePidFile();
|
|
server.close(() => process.exit(0));
|
|
});
|
|
globalThis.__tkmindAdminServer = server;
|
|
})
|
|
.catch((err) => {
|
|
console.error('Admin server bootstrap failed:', err);
|
|
process.exit(1);
|
|
});
|