96 lines
3.7 KiB
JavaScript
96 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Legacy classroom migration — P4 tooling.
|
||
*
|
||
* Scans the server classrooms dir (`data/classrooms/*.json`) and re-packages
|
||
* every classroom into a published frozen bundle (design doc §7 "旧课件迁移":
|
||
* migration = re-packaging, the packager consumes classroom-storage data
|
||
* directly).
|
||
*
|
||
* Byte resolution: media/narration bytes must come from somewhere the server
|
||
* can reach. Pass `--resolver <module>` to load a module exporting
|
||
* `createResolvers(): Promise<ClassroomByteResolvers>` (e.g. wired to the
|
||
* asset registry). Without a resolver, classrooms whose media is only in
|
||
* learner browsers are reported as `incomplete` — re-publish those from the
|
||
* ops workbench instead (which packages from IndexedDB).
|
||
*
|
||
* Usage:
|
||
* npx tsx scripts/migrate-coursewares.ts --base-url https://ops.example.com
|
||
* npx tsx scripts/migrate-coursewares.ts --resolver ./scripts/asset-resolvers.mjs
|
||
*
|
||
* Exit codes: 0 = all migrated or cleanly reported; 1 = hard error.
|
||
*/
|
||
|
||
import { migrateAllClassrooms, type ClassroomByteResolvers } from '@/lib/courseware-repo/migrate';
|
||
import { createLogger } from '@/lib/logger';
|
||
|
||
const log = createLogger('MigrateCLI');
|
||
|
||
function parseArgs(argv: string[]): { baseUrl: string; resolver?: string; classroomsDir?: string } {
|
||
const args: Record<string, string> = {};
|
||
for (let i = 0; i < argv.length; i++) {
|
||
const arg = argv[i];
|
||
if (arg.startsWith('--')) {
|
||
const key = arg.slice(2);
|
||
const value = argv[i + 1];
|
||
if (value && !value.startsWith('--')) {
|
||
args[key] = value;
|
||
i++;
|
||
} else {
|
||
args[key] = 'true';
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
baseUrl: args['base-url'] ?? 'http://localhost:3000',
|
||
resolver: args['resolver'],
|
||
classroomsDir: args['classrooms-dir'],
|
||
};
|
||
}
|
||
|
||
async function main() {
|
||
const { baseUrl, resolver, classroomsDir } = parseArgs(process.argv.slice(2));
|
||
|
||
let resolvers: ClassroomByteResolvers | undefined;
|
||
if (resolver) {
|
||
const resolverModule = await import(resolver);
|
||
const factory = resolverModule.createResolvers ?? resolverModule.default;
|
||
if (typeof factory !== 'function') {
|
||
throw new Error(`Resolver module ${resolver} must export createResolvers()`);
|
||
}
|
||
resolvers = await factory();
|
||
log.info('Loaded byte resolvers from', resolver);
|
||
} else {
|
||
log.warn(
|
||
'No resolver: classrooms whose media lives only in learner browsers will be reported incomplete. ' +
|
||
'Re-publish those from the ops workbench (export menu → 发布课件).',
|
||
);
|
||
}
|
||
|
||
const report = await migrateAllClassrooms({ baseUrl, resolvers, classroomsDir });
|
||
|
||
console.log('\n=== 迁移报告 (Migration report) ===');
|
||
console.log(`已发布 published: ${report.published.length}`);
|
||
for (const item of report.published) {
|
||
if (item.status !== 'published') continue;
|
||
console.log(` ✓ ${item.classroomId} → ${item.coursewareId} v${item.version}`);
|
||
}
|
||
console.log(`未完成 incomplete: ${report.incomplete.length}`);
|
||
for (const item of report.incomplete) {
|
||
if (item.status !== 'incomplete') continue;
|
||
console.log(` ✗ ${item.classroomId} (${item.missing.length} missing resources)`);
|
||
for (const line of item.missing.slice(0, 8)) console.log(` - ${line}`);
|
||
if (item.missing.length > 8) console.log(` … ${item.missing.length - 8} more`);
|
||
}
|
||
console.log(`跳过 skipped: ${report.skipped.length}`, report.skipped.join(', ') || '');
|
||
|
||
if (report.incomplete.length > 0) {
|
||
console.log('\n提示:未完成的课件请用运营端发布工作台重新发布(媒体在浏览器端时服务端无法取到字节)。');
|
||
}
|
||
}
|
||
|
||
main().catch((error) => {
|
||
log.error('Migration failed:', error);
|
||
process.exitCode = 1;
|
||
});
|