238 lines
7.9 KiB
JavaScript
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();
|
|
},
|
|
};
|
|
}
|