Files
Genarrative/server-node/src/server.ts
2026-04-10 15:37:02 +08:00

151 lines
4.6 KiB
TypeScript

import { pathToFileURL } from 'node:url';
import { createApp } from './app.js';
import { type AppConfig, loadConfig } from './config.js';
import type { AppContext } from './context.js';
import { createDatabase } from './db.js';
import { createLogger } from './logging.js';
import { AuthIdentityRepository } from './repositories/authIdentityRepository.js';
import { AuthAuditLogRepository } from './repositories/authAuditLogRepository.js';
import { AuthRiskBlockRepository } from './repositories/authRiskBlockRepository.js';
import { RuntimeRepository } from './repositories/runtimeRepository.js';
import { SmsAuthEventRepository } from './repositories/smsAuthEventRepository.js';
import { UserRepository } from './repositories/userRepository.js';
import { UserSessionRepository } from './repositories/userSessionRepository.js';
import { CustomWorldSessionStore } from './services/customWorldSessionStore.js';
import { UpstreamLlmClient } from './services/llmClient.js';
import { CaptchaChallengeStore } from './services/captchaChallengeStore.js';
import { createSmsVerificationService } from './services/smsVerificationService.js';
import { createWechatAuthService } from './services/wechatAuthService.js';
import { WechatAuthStateStore } from './services/wechatAuthStateStore.js';
function resolveListenTarget(serverAddr: string) {
const trimmed = serverAddr.trim();
if (!trimmed) {
return { host: '0.0.0.0', port: 8081 };
}
if (trimmed.startsWith(':')) {
return {
host: '0.0.0.0',
port: Number(trimmed.slice(1)),
};
}
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
const url = new URL(trimmed);
return {
host: url.hostname,
port: Number(url.port || 80),
};
}
if (trimmed.includes(':')) {
const [host, portText] = trimmed.split(':');
return {
host: host || '0.0.0.0',
port: Number(portText),
};
}
return {
host: '0.0.0.0',
port: Number(trimmed),
};
}
function describeDatabase(databaseUrl: string) {
if (databaseUrl.startsWith('pg-mem://')) {
return {
database_engine: 'pg-mem',
database_name: databaseUrl.slice('pg-mem://'.length) || 'memory',
};
}
try {
const url = new URL(databaseUrl);
return {
database_engine: url.protocol.replace(/:$/u, ''),
database_host: url.hostname,
database_port: Number(url.port || 5432),
database_name: url.pathname.replace(/^\/+/u, '') || 'postgres',
};
} catch {
return {
database_engine: 'postgresql',
database_target: 'configured',
};
}
}
export async function createAppContext(config: AppConfig = loadConfig()) {
const logger = createLogger(config);
const db = await createDatabase(config);
const context: AppContext = {
config,
logger,
db,
userRepository: new UserRepository(db),
authIdentityRepository: new AuthIdentityRepository(db),
authAuditLogRepository: new AuthAuditLogRepository(db),
authRiskBlockRepository: new AuthRiskBlockRepository(db),
smsAuthEventRepository: new SmsAuthEventRepository(db),
userSessionRepository: new UserSessionRepository(db),
runtimeRepository: new RuntimeRepository(db),
llmClient: new UpstreamLlmClient(config, logger),
customWorldSessions: new CustomWorldSessionStore(),
smsVerificationService: createSmsVerificationService(config, logger),
wechatAuthService: createWechatAuthService(config, logger),
wechatAuthStates: new WechatAuthStateStore(),
captchaChallenges: new CaptchaChallengeStore(),
};
return context;
}
async function main() {
const context = await createAppContext();
const app = createApp(context);
const { host, port } = resolveListenTarget(context.config.serverAddr);
const server = app.listen(port, host, () => {
context.logger.info(
{
host,
port,
...describeDatabase(context.config.databaseUrl),
},
'server-node started',
);
});
let shuttingDown = false;
const shutdown = () => {
if (shuttingDown) {
return;
}
shuttingDown = true;
context.logger.info('server-node shutting down');
server.close(() => {
void context.db
.close()
.then(() => {
process.exit(0);
})
.catch((error) => {
context.logger.error({ err: error }, 'failed to close database');
process.exit(1);
});
});
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
const isEntryPoint =
typeof process.argv[1] === 'string' &&
import.meta.url === pathToFileURL(process.argv[1]).href;
if (isEntryPoint) {
void main().catch((error) => {
console.error(error);
process.exit(1);
});
}