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 { AuthAuditLogRepository } from './repositories/authAuditLogRepository.js'; import { AuthIdentityRepository } from './repositories/authIdentityRepository.js'; import { AuthRiskBlockRepository } from './repositories/authRiskBlockRepository.js'; import { RpgAgentSessionRepository } from './repositories/RpgAgentSessionRepository.js'; import { RpgWorldProfileRepository } from './repositories/RpgWorldProfileRepository.js'; import { RpgSaveArchiveRepository } from './repositories/rpg-entry/RpgSaveArchiveRepository.js'; import { RpgWorldLibraryRepository } from './repositories/rpg-entry/RpgWorldLibraryRepository.js'; import { RpgBrowseHistoryRepository } from './repositories/rpg-profile/RpgBrowseHistoryRepository.js'; import { RpgProfileDashboardRepository } from './repositories/rpg-profile/RpgProfileDashboardRepository.js'; import { RpgRuntimeSnapshotRepository } from './repositories/rpg-runtime/RpgRuntimeSnapshotRepository.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 { CaptchaChallengeStore } from './services/captchaChallengeStore.js'; import { CustomWorldAgentAutoAssetService } from './services/customWorldAgentAutoAssetService.js'; import { CustomWorldAgentOrchestrator } from './services/customWorldAgentOrchestrator.js'; import { CustomWorldAgentSessionStore } from './services/customWorldAgentSessionStore.js'; import { UpstreamLlmClient } from './services/llmClient.js'; import { RpgWorldWorkSummaryService } from './services/RpgWorldWorkSummaryService.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 rpgAgentSessionRepository = new RpgAgentSessionRepository(db); const rpgWorldProfileRepository = new RpgWorldProfileRepository(db); const runtimeRepository = new RuntimeRepository(db); const rpgProfileDashboardRepository = new RpgProfileDashboardRepository( runtimeRepository, ); const rpgBrowseHistoryRepository = new RpgBrowseHistoryRepository( runtimeRepository, ); const rpgSaveArchiveRepository = new RpgSaveArchiveRepository( runtimeRepository, ); const rpgWorldLibraryRepository = new RpgWorldLibraryRepository( runtimeRepository, ); const rpgRuntimeSnapshotRepository = new RpgRuntimeSnapshotRepository( runtimeRepository, ); const userRepository = new UserRepository(db); const customWorldAgentSessions = new CustomWorldAgentSessionStore( rpgAgentSessionRepository, ); const rpgWorldWorkSummaryService = new RpgWorldWorkSummaryService( rpgWorldProfileRepository, customWorldAgentSessions, ); const autoAssetService = new CustomWorldAgentAutoAssetService( config, config.dashScope.apiKey.trim() ? CustomWorldAgentAutoAssetService.createDashScopeCharacterVisualGenerator( config, ) : CustomWorldAgentAutoAssetService.createFallbackCharacterVisualGenerator( config, ), config.dashScope.apiKey.trim() ? CustomWorldAgentAutoAssetService.createDashScopeSceneActBackgroundGenerator( config, ) : CustomWorldAgentAutoAssetService.createFallbackSceneActBackgroundGenerator( config, ), ); const context: AppContext = { config, logger, db, userRepository, authIdentityRepository: new AuthIdentityRepository(db), authAuditLogRepository: new AuthAuditLogRepository(db), authRiskBlockRepository: new AuthRiskBlockRepository(db), smsAuthEventRepository: new SmsAuthEventRepository(db), userSessionRepository: new UserSessionRepository(db), rpgAgentSessionRepository, rpgWorldProfileRepository, rpgProfileDashboardRepository, rpgBrowseHistoryRepository, rpgSaveArchiveRepository, rpgWorldLibraryRepository, rpgRuntimeSnapshotRepository, runtimeRepository, llmClient: new UpstreamLlmClient(config, logger), customWorldAgentSessions, customWorldAgentOrchestrator: new CustomWorldAgentOrchestrator( customWorldAgentSessions, config.llm.apiKey.trim() ? new UpstreamLlmClient(config, logger) : null, { autoAssetService, rpgWorldProfileRepository, userRepository, }, ), rpgWorldWorkSummaryService, 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); } function isEntryModule() { if (typeof process.argv[1] !== 'string') { return false; } const entryHref = pathToFileURL(process.argv[1]).href; if (typeof import.meta.url === 'string' && import.meta.url === entryHref) { return true; } return ( typeof __filename === 'string' && pathToFileURL(__filename).href === entryHref ); } const isEntryPoint = isEntryModule(); if (isEntryPoint) { void main().catch((error) => { console.error(error); process.exit(1); }); }