Files
memind/mindspace-service/mindspace-service-bootstrap.mjs

238 lines
7.9 KiB
JavaScript

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();
},
};
}