Files
memind/mindspace-service/mindspace-service-bootstrap.mjs
john 7f8d692d16 fix(agent): recover stale runs and improve new-user OA delivery
Fix DEV logout cookie clearing, materialize selected MindSpace OA assets before agent runs, and recover zombie runs from synced workspace pages. Add client run wait timeout, harness retry limits, page-edit asset forwarding, and logout/john2 scenario tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 20:05:52 +08:00

239 lines
8.0 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),
resolveWorkspaceRoot: (userId) => userAuth.resolveWorkingDir(userId),
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();
},
};
}