import { createAssetGatewayConfigService } from '../asset-gateway.mjs'; import { createExperienceService } from '../experience-service.mjs'; import { createImageMakeAdminConfigService } from '../image-make-admin-config.mjs'; import { createImageMakeClientFromEnv } from '../image-make-client.mjs'; import { createLlmProviderService, RELAY_BOOTSTRAP } from '../llm-providers.mjs'; import { createMindSpaceAgentRunner } from '../mindspace-agent-runner.mjs'; import { createMindSpaceAuditWriter } from '../mindspace-audit.mjs'; import { createMindSpaceImageGenerationService } from '../mindspace-image-generation.mjs'; import { createMindSpaceImageReviewService } from '../mindspace-image-review.mjs'; import { createWordFilterService } from '../word-filter.mjs'; import { isPassiveCanaryRuntime } from './portal-runtime-role.mjs'; async function createPgExperienceServiceFromModule(options) { const { createPgExperienceService } = await import( '../experience-service-pg.mjs' ); return createPgExperienceService(options); } export async function bootstrapPortalAgentServices({ pool, env = process.env, runtime, apiTarget, apiTargets, apiSecret, userAuth, sessionAccess, mindSpaceRuntimeAdapter, mindSpaceAssets, resolveUserIdByDirKey, workspaceMaintenanceEnabled = true, startWorkspaceThumbnailWatcher, startWorkspaceAssetSyncWatcher, logger = console, createExperienceServiceFn = createExperienceService, createPgExperienceServiceFn = createPgExperienceServiceFromModule, createMindSpaceAgentRunnerFn = createMindSpaceAgentRunner, createMindSpaceAuditWriterFn = createMindSpaceAuditWriter, createLlmProviderServiceFn = createLlmProviderService, createAssetGatewayConfigServiceFn = createAssetGatewayConfigService, createImageMakeAdminConfigServiceFn = createImageMakeAdminConfigService, createImageMakeClientFromEnvFn = createImageMakeClientFromEnv, createMindSpaceImageReviewServiceFn = createMindSpaceImageReviewService, createMindSpaceImageGenerationServiceFn = createMindSpaceImageGenerationService, createWordFilterServiceFn = createWordFilterService, relayBootstrap = RELAY_BOOTSTRAP, } = {}) { if ( !pool || !runtime || !userAuth || !sessionAccess || !mindSpaceRuntimeAdapter?.agentJobService || typeof mindSpaceRuntimeAdapter.startBackgroundJobs !== 'function' || !mindSpaceAssets || typeof resolveUserIdByDirKey !== 'function' || typeof startWorkspaceThumbnailWatcher !== 'function' || typeof startWorkspaceAssetSyncWatcher !== 'function' ) { throw new Error( 'bootstrapPortalAgentServices requires runtime service dependencies', ); } const mindSpaceAgentJobs = mindSpaceRuntimeAdapter.agentJobService; let experienceService = null; if (runtime.experienceEnabled) { if (env.EXPERIENCE_PG_URL) { try { experienceService = await createPgExperienceServiceFn({ connectionString: env.EXPERIENCE_PG_URL, }); logger.log( 'Experience store: PostgreSQL + pgvector', ); } catch (error) { logger.error( 'Experience PG init failed, falling back to MySQL store:', error instanceof Error ? error.message : error, ); experienceService = createExperienceServiceFn(pool); } } else { experienceService = createExperienceServiceFn(pool); } } const mindSpaceAgentRunner = createMindSpaceAgentRunnerFn({ apiTarget, apiSecret, userAuth, sessionAccess, agentJobService: mindSpaceAgentJobs, experienceService, }); const mindSpaceAudit = createMindSpaceAuditWriterFn(pool); if (isPassiveCanaryRuntime(env)) { logger.log('Passive candidate runtime: MindSpace background jobs disabled'); } else { mindSpaceRuntimeAdapter.startBackgroundJobs({ publicationCleanupIntervalMs: 60 * 1000, agentWorker: runtime.agentWorker, agentRunner: mindSpaceAgentRunner, workspaceMaintenanceEnabled, publishRoot: runtime.publishRoot, startWorkspaceThumbnailWatcher, startWorkspaceAssetSyncWatcher, syncUserWorkspaceByDirKey: async ( dirKey, options, ) => { const userId = await resolveUserIdByDirKey(dirKey); if (!userId) return; await mindSpaceAssets.syncWorkspaceAssets( userId, options, ); }, expireStaleUploadsIntervalMs: 5 * 60 * 1000, }); } await userAuth.ensureAdminUser(); const userDataSpaceBackfill = await userAuth.ensureAllUserDataSpaces(); if (userDataSpaceBackfill.errors.length > 0) { logger.warn( `[PageData] PG space backfill incomplete: ${userDataSpaceBackfill.errors.length} user(s) failed`, ); } const llmProviderService = createLlmProviderServiceFn(pool, { apiTarget, apiTargets, apiSecret, }); const assetGatewayConfigService = createAssetGatewayConfigServiceFn(pool, { llmProviderService, }); const imageMakeAdminConfigService = createImageMakeAdminConfigServiceFn(pool, { env, llmProviderService, }); await imageMakeAdminConfigService.ensureSchema(); let imageMakeClient = null; try { imageMakeClient = createImageMakeClientFromEnvFn(env); } catch (error) { logger.warn( '[image_make] invalid local configuration; integration stays disabled:', error?.message ?? error, ); } const imageReviewService = createMindSpaceImageReviewServiceFn({ llmProviderService, env, logger, }); const mindSpaceImageGeneration = createMindSpaceImageGenerationServiceFn({ configService: assetGatewayConfigService, assetService: mindSpaceAssets, imageMakeClient, imageReviewService, env, logger, }); const wordFilterService = createWordFilterServiceFn(pool); const relayBootstrapTask = env.H5_RELAY_BOOTSTRAP_DISABLED === '1' ? Promise.resolve({ ok: true, created: false, skipped: true }) : llmProviderService .ensureBootstrapRelay() .then((result) => { if (result.created) { logger.log( `LLM relay bootstrap created: ${relayBootstrap.name}`, ); } }) .catch((error) => { logger.warn( 'LLM relay bootstrap skipped:', error instanceof Error ? error.message : error, ); }); void relayBootstrapTask; const providerSyncTask = llmProviderService .syncSelectedToGoosed() .catch((error) => { logger.warn( 'LLM provider boot sync skipped:', error instanceof Error ? error.message : error, ); }); void providerSyncTask; return { mindSpaceAgentJobs, experienceService, mindSpaceAgentRunner, mindSpaceAudit, userDataSpaceBackfill, llmProviderService, assetGatewayConfigService, imageMakeAdminConfigService, imageMakeClient, imageReviewService, mindSpaceImageGeneration, wordFilterService, relayBootstrapTask, providerSyncTask, }; }