Add MindSpace service runtime release chain
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# MindSpace Service
|
||||
|
||||
Standalone MindSpace service source tracked inside the Memind repo.
|
||||
|
||||
This service is deployed to production as a separate runtime artifact under
|
||||
`/Users/john/MindSpace` and exposes the shared adapter RPC contract that Memind
|
||||
can call through `MINDSPACE_SERVER_ADAPTER=remote`.
|
||||
|
||||
Production defaults:
|
||||
|
||||
- MindSpace service listens on `127.0.0.1:8082`
|
||||
- Memind remains on `127.0.0.1:8081`
|
||||
- Runtime code lives at `/Users/john/MindSpace`
|
||||
- Runtime data continues to use the existing persisted Memind directories:
|
||||
`MindSpace/`, `data/mindspace/`, and `users/`
|
||||
|
||||
Key scripts in this repo:
|
||||
|
||||
- `node scripts/build-mindspace-service-runtime.mjs`
|
||||
- `bash scripts/release-mindspace-service-prod.sh --yes`
|
||||
@@ -0,0 +1,160 @@
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function loadContract(env = process.env) {
|
||||
const memindRoot = path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source');
|
||||
return import(
|
||||
pathToFileURL(path.join(memindRoot, 'mindspace-server-adapter-contract.mjs')).href
|
||||
);
|
||||
}
|
||||
|
||||
function parseBearerToken(headers) {
|
||||
const auth = String(headers.authorization ?? '');
|
||||
if (!auth.startsWith('Bearer ')) return '';
|
||||
return auth.slice('Bearer '.length).trim();
|
||||
}
|
||||
|
||||
async function readJsonBody(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const text = Buffer.concat(chunks).toString('utf8').trim();
|
||||
return text ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
function reviveRpcValue(value) {
|
||||
if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
value.type === 'Buffer' &&
|
||||
Array.isArray(value.data)
|
||||
) {
|
||||
return Buffer.from(value.data);
|
||||
}
|
||||
if (Array.isArray(value)) return value.map((item) => reviveRpcValue(item));
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [key, reviveRpcValue(item)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function json(res, statusCode, payload) {
|
||||
const body = JSON.stringify(payload);
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
export async function createMindSpaceRpcRequestHandler({
|
||||
adapter,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
serviceMeta = {},
|
||||
} = {}) {
|
||||
const {
|
||||
MINDSPACE_SERVER_ADAPTER_BINDINGS,
|
||||
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
|
||||
} = await loadContract(env);
|
||||
const operationBasePath = String(
|
||||
serviceMeta.operationBasePath ?? env.MINDSPACE_REMOTE_OPERATION_BASE_PATH ?? '/mindspace/v1/adapter',
|
||||
)
|
||||
.trim()
|
||||
.replace(/\/+$/, '') || '/mindspace/v1/adapter';
|
||||
const authToken = String(env.MINDSPACE_REMOTE_AUTH_TOKEN ?? '').trim();
|
||||
|
||||
return async function handleMindSpaceRpc(req, res) {
|
||||
try {
|
||||
const url = new URL(req.url, 'http://127.0.0.1');
|
||||
if (req.method === 'GET' && url.pathname === '/health') {
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
service: 'mindspace-service',
|
||||
adapterKind: adapter.kind,
|
||||
implementationStatus: adapter.implementationStatus,
|
||||
operationBasePath,
|
||||
backgroundJobsManagedLocally: adapter.kind === 'local',
|
||||
});
|
||||
}
|
||||
if (req.method === 'GET' && url.pathname === '/mindspace/v1/contract') {
|
||||
return json(res, 200, {
|
||||
bindings: MINDSPACE_SERVER_ADAPTER_BINDINGS,
|
||||
bindingKeys: MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
|
||||
});
|
||||
}
|
||||
if (req.method !== 'POST' || !url.pathname.startsWith(`${operationBasePath}/`)) {
|
||||
return json(res, 404, { message: 'Not found' });
|
||||
}
|
||||
if (authToken && parseBearerToken(req.headers) !== authToken) {
|
||||
return json(res, 401, { message: 'Unauthorized' });
|
||||
}
|
||||
const segments = url.pathname.slice(operationBasePath.length + 1).split('/').filter(Boolean);
|
||||
if (segments.length !== 2) {
|
||||
return json(res, 400, { message: 'Expected /:binding/:method' });
|
||||
}
|
||||
const [bindingKey, method] = segments.map(decodeURIComponent);
|
||||
const service = adapter?.[bindingKey];
|
||||
const allowedMethods = MINDSPACE_SERVER_ADAPTER_BINDINGS[bindingKey];
|
||||
if (!service || !allowedMethods) {
|
||||
return json(res, 404, { message: `Unknown binding: ${bindingKey}` });
|
||||
}
|
||||
if (!allowedMethods.includes(method)) {
|
||||
return json(res, 404, { message: `Unknown method: ${bindingKey}.${method}` });
|
||||
}
|
||||
const body = await readJsonBody(req);
|
||||
const args = Array.isArray(body?.args) ? reviveRpcValue(body.args) : [];
|
||||
const result = await service[method](...args);
|
||||
return json(res, 200, result);
|
||||
} catch (error) {
|
||||
logger.error?.('[MindSpace RPC Error]', error);
|
||||
return json(res, 500, {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function startMindSpaceRpcServer({
|
||||
adapter,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
serviceMeta = {},
|
||||
} = {}) {
|
||||
const handler = await createMindSpaceRpcRequestHandler({
|
||||
adapter,
|
||||
env,
|
||||
logger,
|
||||
serviceMeta,
|
||||
});
|
||||
const host = env.MINDSPACE_SERVICE_HOST ?? '127.0.0.1';
|
||||
const port = Number(env.MINDSPACE_SERVICE_PORT ?? 8082);
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
void handler(req, res);
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, host, () => resolve());
|
||||
});
|
||||
|
||||
return {
|
||||
server,
|
||||
host,
|
||||
port,
|
||||
close: () =>
|
||||
new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createMindSpaceRpcRequestHandler } from './mindspace-rpc-server.mjs';
|
||||
|
||||
function createStubAdapter() {
|
||||
const calls = {
|
||||
writeUploadContent: [],
|
||||
};
|
||||
return {
|
||||
calls,
|
||||
kind: 'local',
|
||||
implementationStatus: 'ready',
|
||||
serviceFacade: {
|
||||
async prepareConversationPackageRead() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
conversationPackageRegistry: {
|
||||
async ensurePackage() { return { ok: true }; },
|
||||
async putObjectForSession() { return { ok: true }; },
|
||||
async recordArtifact() { return { ok: true }; },
|
||||
async readArtifactObject() { return { ok: true }; },
|
||||
async readManifestForSession() { return { ok: true }; },
|
||||
async writeManifestForSession() { return { ok: true }; },
|
||||
},
|
||||
assetService: {
|
||||
async cancelUpload() { return { ok: true }; },
|
||||
async claimUploadArtifactsForConversation() { return []; },
|
||||
async completeUpload() { return { ok: true }; },
|
||||
async createChatAsset() { return { ok: true }; },
|
||||
async createUpload() { return { ok: true }; },
|
||||
async deleteAsset() { return { ok: true }; },
|
||||
async listAssets() { return []; },
|
||||
async readAsset() { return { ok: true }; },
|
||||
async readPublicAsset() { return { ok: true }; },
|
||||
async renderAssetPreview() { return { preview: true }; },
|
||||
async renderAssetThumbnail() { return { thumbnail: true }; },
|
||||
async syncWorkspaceAssets() { return { synced: true }; },
|
||||
async writeUploadContent(...args) {
|
||||
calls.writeUploadContent.push(args);
|
||||
return { isBuffer: Buffer.isBuffer(args[2]), size: args[2]?.length ?? 0 };
|
||||
},
|
||||
async expireStaleUploads() { return { expired: 0 }; },
|
||||
},
|
||||
pageService: {
|
||||
async createFromChat() { return { ok: true }; },
|
||||
async createPage() { return { ok: true }; },
|
||||
async deletePage() { return { ok: true }; },
|
||||
async findPageByRelativePath() { return null; },
|
||||
async findPageBySourceAsset() { return null; },
|
||||
async getDeletePreview() { return { ok: true }; },
|
||||
async getPage() { return { ok: true }; },
|
||||
async listPages() { return []; },
|
||||
async listVersions() { return []; },
|
||||
async localizePrivateResources() { return { ok: true }; },
|
||||
async redactPage() { return { ok: true }; },
|
||||
async regenerateThumbnail() { return { ok: true }; },
|
||||
async renderDraftPreview() { return { html: '' }; },
|
||||
async renderPreview() { return { html: '' }; },
|
||||
async renderThumbnail() { return { ok: true }; },
|
||||
async rewriteHtmlDownloadLinksForPage() { return { ok: true }; },
|
||||
async updatePage() { return { ok: true }; },
|
||||
async uploadThumbnail() { return { ok: true }; },
|
||||
},
|
||||
pageLiveEditService: {
|
||||
async applyAgentPatch() { return { ok: true }; },
|
||||
async bindSession() { return { ok: true }; },
|
||||
async getRevisionSnapshot() { return { ok: true }; },
|
||||
},
|
||||
assetAgentService: {
|
||||
async applyAgentDelete() { return { ok: true }; },
|
||||
},
|
||||
publicationService: {
|
||||
async check() { return { ok: true }; },
|
||||
async cleanupExpiredUnconfirmedPublications() { return { cleaned: 0 }; },
|
||||
async getCurrent() { return null; },
|
||||
async getPublicHomepage() { return null; },
|
||||
async getStats() { return { ok: true }; },
|
||||
async offline() { return { ok: true }; },
|
||||
async publish() { return { ok: true }; },
|
||||
async resolvePrivateLink() { return { ok: true }; },
|
||||
async resolvePublic() { return { ok: true }; },
|
||||
async updatePublicationStatus() { return { ok: true }; },
|
||||
},
|
||||
cleanupService: {
|
||||
async listCandidates() { return []; },
|
||||
async runCleanup() { return { ok: true }; },
|
||||
},
|
||||
agentJobService: {
|
||||
async cancelJob() { return { ok: true }; },
|
||||
async claimJob() { return { ok: true }; },
|
||||
async claimNextJob() { return null; },
|
||||
async completeJob() { return { ok: true }; },
|
||||
async createJob() { return { ok: true }; },
|
||||
async getAssetForJob() { return { ok: true }; },
|
||||
async getJob() { return { ok: true }; },
|
||||
async heartbeat() { return { ok: true }; },
|
||||
async listJobs() { return []; },
|
||||
async reapStaleJobs() { return 0; },
|
||||
async retryJob() { return { ok: true }; },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runRequest(handler, { method = 'GET', path = '/', headers = {}, body = '' } = {}) {
|
||||
const chunks = [];
|
||||
const req = {
|
||||
method,
|
||||
url: path,
|
||||
headers,
|
||||
async *[Symbol.asyncIterator]() {
|
||||
if (body) yield Buffer.from(body);
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
statusCode: 0,
|
||||
headers: {},
|
||||
writeHead(statusCode, responseHeaders) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = responseHeaders;
|
||||
},
|
||||
end(chunk) {
|
||||
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
||||
},
|
||||
};
|
||||
await handler(req, res);
|
||||
return {
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null,
|
||||
};
|
||||
}
|
||||
|
||||
test('health and contract endpoints are exposed without auth', async () => {
|
||||
const handler = await createMindSpaceRpcRequestHandler({
|
||||
adapter: createStubAdapter(),
|
||||
env: {
|
||||
MINDSPACE_MEMIND_ROOT: '..',
|
||||
},
|
||||
});
|
||||
|
||||
const health = await runRequest(handler, { path: '/health' });
|
||||
const contract = await runRequest(handler, { path: '/mindspace/v1/contract' });
|
||||
|
||||
assert.equal(health.statusCode, 200);
|
||||
assert.equal(health.body.ok, true);
|
||||
assert.equal(contract.statusCode, 200);
|
||||
assert.ok(contract.body.bindings.assetService.includes('renderAssetPreview'));
|
||||
});
|
||||
|
||||
test('rpc invocation enforces auth token and dispatches args to adapter method', async () => {
|
||||
const handler = await createMindSpaceRpcRequestHandler({
|
||||
adapter: createStubAdapter(),
|
||||
env: {
|
||||
MINDSPACE_MEMIND_ROOT: '..',
|
||||
MINDSPACE_REMOTE_AUTH_TOKEN: 'secret-token',
|
||||
},
|
||||
});
|
||||
|
||||
const unauthorized = await runRequest(handler, {
|
||||
method: 'POST',
|
||||
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
|
||||
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
|
||||
});
|
||||
const authorized = await runRequest(handler, {
|
||||
method: 'POST',
|
||||
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
|
||||
headers: {
|
||||
authorization: 'Bearer secret-token',
|
||||
},
|
||||
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
|
||||
});
|
||||
|
||||
assert.equal(unauthorized.statusCode, 401);
|
||||
assert.equal(authorized.statusCode, 200);
|
||||
assert.deepEqual(authorized.body, { preview: true });
|
||||
});
|
||||
|
||||
test('rpc invocation revives JSON-serialized buffers before dispatch', async () => {
|
||||
const adapter = createStubAdapter();
|
||||
const handler = await createMindSpaceRpcRequestHandler({
|
||||
adapter,
|
||||
env: {
|
||||
MINDSPACE_MEMIND_ROOT: '..',
|
||||
},
|
||||
});
|
||||
const payload = Buffer.from('asset parity payload');
|
||||
|
||||
const response = await runRequest(handler, {
|
||||
method: 'POST',
|
||||
path: '/mindspace/v1/adapter/assetService/writeUploadContent',
|
||||
body: JSON.stringify({ args: ['user-1', 'upload-1', payload] }),
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.body, { isBuffer: true, size: payload.length });
|
||||
assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "mindspace-service",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.mjs",
|
||||
"test": "node --test mindspace-rpc-server.test.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MEMIND_ROOT="${MEMIND_SHARED_ROOT:-/Users/john/Project/Memind}"
|
||||
|
||||
if [[ -f "${MEMIND_ROOT}/.env" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "${MEMIND_ROOT}/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
export NODE_ENV=production
|
||||
export MINDSPACE_MEMIND_ROOT="${MINDSPACE_MEMIND_ROOT:-${ROOT}/memind-source}"
|
||||
export MINDSPACE_SERVICE_H5_ROOT="${MINDSPACE_SERVICE_H5_ROOT:-${MEMIND_ROOT}}"
|
||||
export MINDSPACE_STORAGE_ROOT="${MINDSPACE_STORAGE_ROOT:-${MEMIND_ROOT}/data/mindspace}"
|
||||
export H5_USERS_ROOT="${H5_USERS_ROOT:-${MEMIND_ROOT}/users}"
|
||||
export MINDSPACE_SERVICE_PORT="${MINDSPACE_SERVICE_PORT:-8082}"
|
||||
export MINDSPACE_SERVICE_HOST="${MINDSPACE_SERVICE_HOST:-127.0.0.1}"
|
||||
export MINDSPACE_REMOTE_AUTH_TOKEN="${MINDSPACE_REMOTE_AUTH_TOKEN:-${TKMIND_SERVER__SECRET_KEY:-}}"
|
||||
export MINDSPACE_SERVICE_AGENT_WORKER_ENABLED="${MINDSPACE_SERVICE_AGENT_WORKER_ENABLED:-true}"
|
||||
export MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE="${MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE:-true}"
|
||||
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
if [[ ! -x "${NODE_BIN}" ]]; then
|
||||
NODE_BIN="$(command -v node)"
|
||||
fi
|
||||
|
||||
export NODE_PATH="${ROOT}/node_modules"
|
||||
|
||||
exec "${NODE_BIN}" "${ROOT}/server.mjs"
|
||||
@@ -0,0 +1,42 @@
|
||||
import { bootstrapMindSpaceService } from './mindspace-service-bootstrap.mjs';
|
||||
import { startMindSpaceRpcServer } from './mindspace-rpc-server.mjs';
|
||||
|
||||
const logger = console;
|
||||
|
||||
async function main() {
|
||||
const service = await bootstrapMindSpaceService({ env: process.env, logger });
|
||||
const rpc = await startMindSpaceRpcServer({
|
||||
adapter: service.adapter,
|
||||
env: process.env,
|
||||
logger,
|
||||
serviceMeta: {
|
||||
operationBasePath: service.runtime.remote.operationBasePath,
|
||||
},
|
||||
});
|
||||
|
||||
logger.log(
|
||||
`MindSpace service listening on http://${rpc.host}:${rpc.port}${service.runtime.remote.operationBasePath}`,
|
||||
);
|
||||
logger.log(`Memind root: ${service.memindRoot}`);
|
||||
logger.log(`Standalone data root: ${service.standaloneDataRoot}`);
|
||||
logger.log(`Standalone agent worker: ${service.backgroundJobs.agentWorker ? 'enabled' : 'disabled'}`);
|
||||
|
||||
const shutdown = async (signal) => {
|
||||
logger.log(`Received ${signal}, shutting down MindSpace service...`);
|
||||
await rpc.close().catch(() => {});
|
||||
await service.close().catch(() => {});
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
void shutdown('SIGINT');
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
void shutdown('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('MindSpace service failed to start:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user