Files

127 lines
4.9 KiB
TypeScript

import { constants } from 'node:fs';
import { access, mkdir, writeFile, unlink } from 'node:fs/promises';
import path from 'node:path';
import { nanoid } from 'nanoid';
import { COURSE_FRAMEWORK_DIR } from '@/lib/course-framework/store';
import { COURSE_MANIFEST_DIR } from '@/lib/course-manifest-repo/store';
import { COURSEWARES_DIR } from '@/lib/courseware-repo/store';
import { getServerProviders, getServerTTSProviders } from '@/lib/server/provider-config';
import { CLASSROOM_JOBS_DIR, CLASSROOMS_DIR } from '@/lib/server/classroom-storage';
import { configuredLearningRuntimeToken } from './auth';
import { configuredLearningEngineRuntimeBaseUrl } from './ops-engine-control';
import { resolveDeploymentRole } from '@/lib/config/deployment-role';
export const LEARNING_ENGINE_SERVICE = 'openmaic-learning-engine';
export const LEARNING_ENGINE_API_VERSION = 'v1';
export type LearningReadinessCheck = {
ok: boolean;
detail?: string;
};
function dataRoot(): string {
return path.resolve(process.env.LEARNING_DATA_DIR?.trim() || path.join(process.cwd(), 'data'));
}
export function learningPersistenceDirectories(): Record<string, string> {
const root = dataRoot();
return {
classrooms: process.env.CLASSROOM_DATA_DIR || CLASSROOMS_DIR,
classroomJobs: process.env.CLASSROOM_JOBS_DIR || CLASSROOM_JOBS_DIR,
courseFrameworks: process.env.COURSE_FRAMEWORK_DIR || COURSE_FRAMEWORK_DIR,
courseManifests: process.env.COURSE_MANIFEST_DIR || COURSE_MANIFEST_DIR,
coursewares: process.env.COURSEWARE_DATA_DIR || COURSEWARES_DIR,
coursewareBundles:
process.env.COURSEWARE_BUNDLE_DIR || path.join(root, 'courseware-bundles'),
learningPackages:
process.env.LEARNING_COURSE_PACKAGE_DIR ||
process.env.MAKELORE_COURSE_PACKAGE_DIR ||
path.join(root, 'makelore-packages'),
learningCourses:
process.env.LEARNING_COURSE_STORE_DIR ||
process.env.MAKELORE_COURSE_STORE_DIR ||
path.join(root, 'makelore-courses'),
};
}
async function verifyWritableDirectory(directory: string): Promise<void> {
await mkdir(directory, { recursive: true });
await access(directory, constants.R_OK | constants.W_OK);
const probe = path.join(directory, `.learning-ready-${process.pid}-${nanoid(6)}.tmp`);
await writeFile(probe, 'ready', { flag: 'wx' });
await unlink(probe);
}
function configuredOrigin(name: string): boolean {
const raw = process.env[name]?.trim();
if (!raw) return false;
try {
const url = new URL(raw);
return (
(url.protocol === 'https:' || url.protocol === 'http:') &&
!url.username &&
!url.password &&
!url.search &&
!url.hash &&
(url.pathname === '' || url.pathname === '/')
);
} catch {
return false;
}
}
export async function inspectLearningReadiness(): Promise<{
ready: boolean;
checks: Record<string, LearningReadinessCheck>;
}> {
const role = resolveDeploymentRole(
process.env.OPENMAIC_DEPLOYMENT_ROLE ?? process.env.NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE,
process.env.NODE_ENV,
);
const checks: Record<string, LearningReadinessCheck> = role === 'ops'
? {
worksIdentityOrigin: configuredOrigin('WORKS_SQUARE_API_BASE_URL')
? { ok: true }
: { ok: false, detail: 'WORKS_SQUARE_API_BASE_URL is not configured as an origin' },
opsPublicOrigin: configuredOrigin('OPS_PUBLIC_ORIGIN')
? { ok: true }
: { ok: false, detail: 'OPS_PUBLIC_ORIGIN is not configured as an origin' },
engineControlEndpoint: configuredLearningEngineRuntimeBaseUrl()
? { ok: true }
: {
ok: false,
detail: 'LEARNING_ENGINE_BASE_URL or COURSE_PUBLISH_SERVER_BASE_URL is not configured',
},
runtimeToken: configuredLearningRuntimeToken()
? { ok: true }
: { ok: false, detail: 'LEARNING_ENGINE_TOKEN is not configured' },
}
: {
runtimeToken: configuredLearningRuntimeToken()
? { ok: true }
: { ok: false, detail: 'LEARNING_ENGINE_TOKEN is not configured' },
llmProvider:
Object.keys(getServerProviders()).length > 0
? { ok: true }
: { ok: false, detail: 'No server-managed LLM provider is configured' },
ttsProvider:
Object.values(getServerTTSProviders()).some((provider) => !provider.disabled)
? { ok: true }
: { ok: false, detail: 'No enabled server-managed TTS provider is configured' },
};
for (const [name, directory] of Object.entries(learningPersistenceDirectories())) {
try {
await verifyWritableDirectory(directory);
checks[`storage.${name}`] = { ok: true };
} catch (error) {
checks[`storage.${name}`] = {
ok: false,
detail: error instanceof Error ? error.message : 'Storage is not writable',
};
}
}
return { ready: Object.values(checks).every((check) => check.ok), checks };
}