import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function resolveMemindRoot(env = process.env) { return path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source'); } function resolveStandaloneDataRoot(env = process.env) { return path.resolve(env.MINDSPACE_SERVICE_H5_ROOT ?? path.join(__dirname, 'runtime')); } function ensureStandaloneRuntimeLayout(standaloneDataRoot) { fs.mkdirSync(standaloneDataRoot, { recursive: true }); fs.mkdirSync(path.join(standaloneDataRoot, 'MindSpace'), { recursive: true }); fs.mkdirSync(path.join(standaloneDataRoot, 'data'), { recursive: true }); fs.mkdirSync(path.join(standaloneDataRoot, 'users'), { recursive: true }); } function loadDotenvFile(targetEnv, filepath) { if (!fs.existsSync(filepath) || !fs.statSync(filepath).isFile()) return; const content = fs.readFileSync(filepath, 'utf8'); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#') || !line.includes('=')) continue; const [rawKey, ...rawValueParts] = line.split('='); const key = rawKey.trim(); if (!key || key in targetEnv) continue; const rawValue = rawValueParts.join('=').trim(); const unwrapped = (rawValue.startsWith('"') && rawValue.endsWith('"')) || (rawValue.startsWith("'") && rawValue.endsWith("'")) ? rawValue.slice(1, -1) : rawValue; targetEnv[key] = unwrapped; } } function applyMemindEnvDefaults(memindRoot, env = process.env) { loadDotenvFile(env, path.join(memindRoot, '.env')); loadDotenvFile(env, path.join(memindRoot, '.env.local')); return env; } async function importFromMemindRoot(memindRoot, relativePath) { return import(pathToFileURL(path.join(memindRoot, relativePath)).href); } async function loadMemindModules(memindRoot) { const [ db, config, conversationMemory, localServerAdapter, publicHtmlHelpers, runtimeConfig, sessionSnapshot, userAuth, agentRunner, experienceService, workspaceThumbnails, workspaceSync, ] = await Promise.all([ importFromMemindRoot(memindRoot, 'db.mjs'), importFromMemindRoot(memindRoot, 'mindspace-config.mjs'), importFromMemindRoot(memindRoot, 'conversation-memory.mjs'), importFromMemindRoot(memindRoot, 'mindspace-local-server-adapter.mjs'), importFromMemindRoot(memindRoot, 'mindspace-conversation-package-public-html.mjs'), importFromMemindRoot(memindRoot, 'mindspace-runtime-config.mjs'), importFromMemindRoot(memindRoot, 'session-snapshot.mjs'), importFromMemindRoot(memindRoot, 'user-auth.mjs'), importFromMemindRoot(memindRoot, 'mindspace-agent-runner.mjs'), importFromMemindRoot(memindRoot, 'experience-service.mjs'), importFromMemindRoot(memindRoot, 'mindspace-workspace-thumbnails.mjs'), importFromMemindRoot(memindRoot, 'mindspace-workspace-sync.mjs'), ]); return { ...db, ...config, ...conversationMemory, ...localServerAdapter, ...publicHtmlHelpers, ...runtimeConfig, ...sessionSnapshot, ...userAuth, ...agentRunner, ...experienceService, ...workspaceThumbnails, ...workspaceSync, }; } function parseApiTargets(env = process.env) { const csvTargets = String(env.TKMIND_API_TARGETS ?? '') .split(',') .map((value) => value.trim()) .filter(Boolean); const legacyTargets = [env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006', env.TKMIND_API_TARGET_1].filter( Boolean, ); return [...new Set([...csvTargets, ...legacyTargets])]; } async function resolveUserIdForAgentSession(pool, sessionId) { const [rows] = await pool.query( `SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`, [sessionId], ); return rows[0]?.user_id ?? null; } async function resolveUserIdByDirKey(pool, dirKey) { const key = String(dirKey ?? '').trim(); if (!key) return null; const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [key]); return rows[0]?.id ?? key; } export async function bootstrapMindSpaceService({ env = process.env, logger = console, } = {}) { const memindRoot = resolveMemindRoot(env); applyMemindEnvDefaults(memindRoot, env); const standaloneDataRoot = resolveStandaloneDataRoot(env); ensureStandaloneRuntimeLayout(standaloneDataRoot); const { createDbPool, initSchema, isDatabaseConfigured, ensureMindSpaceConfig, createConversationMemoryService, createMindSpaceLocalServerAdapter, createMindSpaceAgentRunner, createSessionSnapshotService, createUserAuth, createExperienceService, registerPublicHtmlArtifactsForConversationPackage, resolveMindSpaceServerRuntimeOptions, startWorkspaceAssetSyncWatcher, startWorkspaceThumbnailWatcher, } = await loadMemindModules(memindRoot); if (!isDatabaseConfigured()) { throw new Error('MindSpace service requires DATABASE_URL or MYSQL_* configuration'); } const runtime = resolveMindSpaceServerRuntimeOptions(standaloneDataRoot, env); const pool = createDbPool(); await initSchema(pool); await ensureMindSpaceConfig(pool, { env }); const conversationMemoryService = createConversationMemoryService(pool); const sessionSnapshotService = createSessionSnapshotService(pool, { conversationMemoryService, }); const usersRoot = path.resolve(env.H5_USERS_ROOT ?? path.join(standaloneDataRoot, 'users')); const userAuth = createUserAuth(pool, { usersRoot, h5Root: standaloneDataRoot, env, defaultSignupBalanceCents: Number(env.H5_SIGNUP_BALANCE_CENTS ?? 500), }); const adapter = createMindSpaceLocalServerAdapter({ pool, h5Root: standaloneDataRoot, env, maxFileBytes: runtime.maxFileBytes, publicPageLimit: runtime.publicPageLimit, resolveUserIdForAgentSession: (sessionId) => resolveUserIdForAgentSession(pool, sessionId), resolveSessionSnapshot: (sessionId) => sessionSnapshotService.get(sessionId), registerPublicHtmlArtifactsForConversation: (payload) => registerPublicHtmlArtifactsForConversationPackage({ conversationPackageRegistry: adapter.conversationPackageRegistry, h5Root: standaloneDataRoot, env, ...payload, }), logger, }); const apiTargets = parseApiTargets(env); const apiTarget = apiTargets[0] ?? 'https://127.0.0.1:18006'; const apiSecret = env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret'; let experienceService = null; if (runtime.experienceEnabled) { experienceService = createExperienceService(pool); } const agentRunner = createMindSpaceAgentRunner({ apiTarget, apiSecret, userAuth, agentJobService: adapter.agentJobService, experienceService, }); const standaloneAgentWorkerEnabled = env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED === '1' || env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED === 'true' || (env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED == null && runtime.agentWorker.enabled); const backgroundJobs = adapter.startBackgroundJobs({ publicationCleanupIntervalMs: 60 * 1000, agentWorker: { ...runtime.agentWorker, enabled: standaloneAgentWorkerEnabled, }, agentRunner, workspaceMaintenanceEnabled: env.MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE === 'true', publishRoot: runtime.publishRoot, startWorkspaceThumbnailWatcher, startWorkspaceAssetSyncWatcher, syncUserWorkspaceByDirKey: async (dirKey, options) => { const userId = await resolveUserIdByDirKey(pool, dirKey); if (!userId) return; await adapter.assetService.syncWorkspaceAssets(userId, options); }, expireStaleUploadsIntervalMs: 5 * 60 * 1000, }); return { memindRoot, standaloneDataRoot, runtime, pool, conversationMemoryService, sessionSnapshotService, userAuth, agentRunner, adapter, backgroundJobs, async close() { await pool.end(); }, }; }