openlearn-next 0.4.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +26 -21
  2. package/cli-data.mjs +535 -40
  3. package/cli-doctor.mjs +456 -17
  4. package/cli.mjs +214 -12
  5. package/dist/assets/AcademicYearFilter-DUUGpDKt.js +1 -0
  6. package/dist/assets/ActivityWorkspaceWidget-5PzyxF6a.js +1 -0
  7. package/dist/assets/AdminDirectoryView-BCtmQTEK.js +5 -0
  8. package/dist/assets/AssessmentCenter-B43ojHG-.js +1 -0
  9. package/dist/assets/BatchPickerModal-CbmjInp7.js +1 -0
  10. package/dist/assets/ClassesView-DEnMaDOt.js +32 -0
  11. package/dist/assets/ClassroomCountdownWidget-CHq4983w.js +2 -0
  12. package/dist/assets/CloudDriveModal-D2t3Uyzr.js +1 -0
  13. package/dist/assets/ComputerLabView-YtnoT5Rb.js +1 -0
  14. package/dist/assets/CourseManagement-DOXyJ_j-.js +1 -0
  15. package/dist/assets/CourseWizardModal-Dbi6Caw2.js +3 -0
  16. package/dist/assets/CoursewareHubPanel-CyxDIJAF.js +1 -0
  17. package/dist/assets/Dashboard-2rLu_ZBy.js +8 -0
  18. package/dist/assets/ExportWeightModal-BrmAh6VX.js +1 -0
  19. package/dist/assets/GLTFLoader-CZJbo_y1.js +1 -0
  20. package/dist/assets/HelpTour-D_q1z_MI.js +1 -0
  21. package/dist/assets/HelpView-CVBrKDrd.js +204 -0
  22. package/dist/assets/ImportLessonsModal-PhHtX31R.js +1 -0
  23. package/dist/assets/ImportModal-DqjqNeH4.js +2 -0
  24. package/dist/assets/InteractiveCoursewareViewer-Vi6lLz7a.js +1 -0
  25. package/dist/assets/InteractiveWhiteboard-lTzeayX5.js +65 -0
  26. package/dist/assets/LazyCourseware-KIt0rGtW.js +2 -0
  27. package/dist/assets/LazyWhiteboard-CCN3pYcW.js +2 -0
  28. package/dist/assets/LessonEditorView-D_GVhHuM.js +1 -0
  29. package/dist/assets/LiveClassroomView-BRsZ6QEq.js +61 -0
  30. package/dist/assets/NotificationDetailModal-tY5rxYCN.js +1 -0
  31. package/dist/assets/OrbitControls-CQjN_uwS.js +1 -0
  32. package/dist/assets/PluginView-CedpAW6d.js +77 -0
  33. package/dist/assets/ProcessLogsModal-Cfr-T9Gd.js +1 -0
  34. package/dist/assets/QuizGeneratorModal-bUA002kP.js +1 -0
  35. package/dist/assets/StudentAssignmentView-BhAz6Hfk.js +1 -0
  36. package/dist/assets/StudentLessonView-DC1yX4kG.js +1 -0
  37. package/dist/assets/StudentView-PHKxIV9T.js +2 -0
  38. package/dist/assets/SystemResourceLibraryModal-DgXYe6_x.js +1 -0
  39. package/dist/assets/TeacherView-DscJtOJ9.js +2 -0
  40. package/dist/assets/TimetableView-D2ozoj_D.js +6 -0
  41. package/dist/assets/UTIF-FfCsrHhe.js +1 -0
  42. package/dist/assets/__vite-optional-peer-dep_ai_ooxml-ui-BIHI7g3E.js +1 -0
  43. package/dist/assets/browser-DEI0QxZC.js +1 -0
  44. package/dist/assets/html2canvas-pro.esm-Bo50djkB.js +12 -0
  45. package/dist/assets/index-2676X8YB.js +131 -0
  46. package/dist/assets/index-C6hlh8pw.css +1 -0
  47. package/dist/assets/{index.es-C5eWEO4o.js → index.es-DyEF0CZ9.js} +1 -1
  48. package/dist/assets/pluginParsers-e4PSm2Iy.js +1 -0
  49. package/dist/assets/react_pptx-react-viewer-BIHI7g3E.js +1 -0
  50. package/dist/assets/teaching-modes-client-BeVaDdJ-.js +1 -0
  51. package/dist/assets/three.module-iOYB3rD4.js +4228 -0
  52. package/dist/assets/timeline-duration-BgdJCSrW.js +1 -0
  53. package/dist/assets/{vendor-charts-qLbgF1eL.js → vendor-charts-SrpBsvM3.js} +2 -2
  54. package/dist/assets/{vendor-icons-BRmhY6fg.js → vendor-icons-CXrZ3R2V.js} +2 -2
  55. package/dist/assets/{vendor-konva-CEOq4dcl.js → vendor-konva-Dne0Z-WC.js} +1 -1
  56. package/dist/assets/{vendor-pdf-D7rGevI1.js → vendor-pdf-C1Vh2Aqq.js} +56 -56
  57. package/dist/assets/vendor-pptx-Bzn6whfK.js +11311 -0
  58. package/dist/assets/vendor-pptx-DIWdXF1L.css +1 -0
  59. package/dist/assets/{vendor-react-B0t7H3hh.js → vendor-react-Kv_JATwr.js} +1 -1
  60. package/dist/assets/vendor-utils-aM7aYvPP.js +1 -0
  61. package/dist/cli-migrator.cjs +310 -0
  62. package/dist/env-validation.cjs +351 -0
  63. package/dist/index.html +13 -10
  64. package/dist/plugin-net-preload.cjs +263 -0
  65. package/dist/server.cjs +51140 -27229
  66. package/migrations/000_initial_schema.sql +410 -0
  67. package/migrations/001_add_execution_mode.sql +6 -0
  68. package/migrations/002_add_client_session_expiry.sql +6 -0
  69. package/migrations/003_classroom_runtime.sql +33 -0
  70. package/migrations/004_courseware_score_config.sql +26 -0
  71. package/migrations/005_assignment_hub.sql +155 -0
  72. package/migrations/006_classroom_event_bus.sql +28 -0
  73. package/migrations/007_interactive_classroom.sql +119 -0
  74. package/migrations/008_teaching_modes.sql +36 -0
  75. package/migrations/009_classroom_peer_review.sql +159 -0
  76. package/migrations/010_class_groups.sql +33 -0
  77. package/migrations/011_classroom_feed.sql +34 -0
  78. package/migrations/012_auto_record_score.sql +48 -0
  79. package/migrations/013_score_source_strategy.sql +81 -0
  80. package/migrations/014_performance_indexes.sql +21 -0
  81. package/migrations/015_demo_data_registry.sql +26 -0
  82. package/migrations/016_preset_polls_and_passcode_expiry.sql +33 -0
  83. package/migrations/017_pacing_signals_segment.sql +6 -0
  84. package/migrations/018_processes_plugin_owner.sql +26 -0
  85. package/migrations/019_plugins_version.sql +61 -0
  86. package/migrations/020_upload_max_file_mb.sql +14 -0
  87. package/migrations/021_class_owner_and_password_gate.sql +11 -0
  88. package/migrations/022_plugin_public_routes.sql +14 -0
  89. package/migrations/023_encrypt_class_passcodes.sql +18 -0
  90. package/migrations/024_segment_gates.sql +37 -0
  91. package/migrations/025_assignment_lookup_indexes.sql +11 -0
  92. package/migrations/026_plugin_grants.sql +21 -0
  93. package/migrations/027_academic_year.sql +26 -0
  94. package/migrations/028_classroom_indexes.sql +14 -0
  95. package/migrations/029_clean_lesson_think_artifacts.sql +13 -0
  96. package/migrations/030_backfill_academic_year.sql +30 -0
  97. package/migrations/031_assessment_core.sql +158 -0
  98. package/migrations/032_progress_verification.sql +17 -0
  99. package/migrations/033_student_lock_mode.sql +9 -0
  100. package/migrations/034_plugin_orphan_data.sql +27 -0
  101. package/migrations/035_icebreaker_checkins.sql +26 -0
  102. package/migrations/036_student_read_notifications_created_at.sql +19 -0
  103. package/migrations/037_plugin_config_scopes.sql +40 -0
  104. package/migrations/038_assessment_ai_grading_suggestion.sql +10 -0
  105. package/migrations/039_question_bank_sharing_and_papers.sql +14 -0
  106. package/migrations/040_assignment_grade_history.sql +37 -0
  107. package/migrations/041_course_package_idempotency.sql +33 -0
  108. package/migrations/042_assignment_grade_resubmit_audit.sql +12 -0
  109. package/migrations/043_mark_sha256_passwords.sql +31 -0
  110. package/migrations/README.md +61 -0
  111. package/migrations/schema-golden.json +640 -0
  112. package/package.json +54 -28
  113. package/dist/assets/AdminDirectoryView-DY--WM5Z.js +0 -1
  114. package/dist/assets/BatchPickerModal-BpWu0Lil.js +0 -1
  115. package/dist/assets/ClassesView-KbHEqJto.js +0 -22
  116. package/dist/assets/ClassroomCountdownWidget-96WFRO9A.js +0 -2
  117. package/dist/assets/CloudDriveModal-DEQ1nkNk.js +0 -1
  118. package/dist/assets/ComputerLabView-1qWDNRQ2.js +0 -1
  119. package/dist/assets/CourseManagement-D3ZpxY0l.js +0 -1
  120. package/dist/assets/CourseWizardModal-Ck_-M8pk.js +0 -3
  121. package/dist/assets/CoursewareHubPanel-CaEizhxl.js +0 -1
  122. package/dist/assets/Dashboard-CYbpbofV.js +0 -8
  123. package/dist/assets/ExportWeightModal-DW2bu9Zz.js +0 -1
  124. package/dist/assets/HelpTour-CMJbbGne.js +0 -1
  125. package/dist/assets/HelpView-CtzdqhBq.js +0 -204
  126. package/dist/assets/ImportLessonsModal-jBuIK453.js +0 -1
  127. package/dist/assets/ImportModal-D7L-KNhZ.js +0 -1
  128. package/dist/assets/InteractiveCoursewareViewer-Bj7ahXMs.js +0 -1
  129. package/dist/assets/InteractiveWhiteboard-B7JcJvDu.js +0 -77
  130. package/dist/assets/LazyCourseware-CuiPxhDg.js +0 -2
  131. package/dist/assets/LazyWhiteboard-BJwCEZAU.js +0 -2
  132. package/dist/assets/LessonEditorView-DFrOp2qp.js +0 -1
  133. package/dist/assets/LiveClassroomView-BZ5nGee3.js +0 -59
  134. package/dist/assets/NotificationDetailModal-Baqnc5pU.js +0 -1
  135. package/dist/assets/PluginView-CCbIuZ24.js +0 -66
  136. package/dist/assets/ProcessLogsModal-DmFXg9mG.js +0 -1
  137. package/dist/assets/QuizGeneratorModal-BBEDssp7.js +0 -1
  138. package/dist/assets/StudentAssignmentView-DgIjUsXz.js +0 -1
  139. package/dist/assets/StudentLessonView-CBJbI5x6.js +0 -1
  140. package/dist/assets/StudentPreviewModal-Bh8OZs0t.js +0 -1
  141. package/dist/assets/StudentView--uGxc5TJ.js +0 -2
  142. package/dist/assets/SystemResourceLibraryModal-BVCclcqA.js +0 -1
  143. package/dist/assets/TeacherView-D4Zsc0eZ.js +0 -2
  144. package/dist/assets/TimetableView-BQG-rEb5.js +0 -6
  145. package/dist/assets/index-BKBCBKvy.js +0 -217
  146. package/dist/assets/index-CvGu7LSj.css +0 -1
  147. package/dist/assets/purify.es-CYR4BTuT.js +0 -3
  148. package/dist/assets/vendor-pptx-C5-U7ieS.js +0 -39
  149. package/dist/assets/vendor-utils-m0Urvtws.js +0 -1
package/cli-doctor.mjs CHANGED
@@ -406,28 +406,56 @@ export function checkCoreVersionDrift(fix = false, fixes = []) {
406
406
  }
407
407
 
408
408
  const coreContent = fs.readFileSync(coreVersionFile, 'utf-8');
409
- const match = coreContent.match(/PLATFORM_VERSION\s*=\s*['"]([^'"]+)['"]/);
410
- if (!match) {
411
- return check('warn', `未在 packages/core/version.ts 中解析到 PLATFORM_VERSION`);
409
+ const staticMatch = coreContent.match(/PLATFORM_VERSION\s*=\s*['"]([^'"]+)['"]/);
410
+ if (staticMatch) {
411
+ const coreVer = staticMatch[1];
412
+ if (coreVer === pkgVer) {
413
+ return check('ok', `v${pkgVer} (package.json 与 packages/core/version.ts 严格一致)`);
414
+ }
415
+
416
+ if (fix) {
417
+ const updated = coreContent.replace(/PLATFORM_VERSION\s*=\s*['"][^'"]+['"]/, `PLATFORM_VERSION = '${pkgVer}'`);
418
+ fs.writeFileSync(coreVersionFile, updated, 'utf-8');
419
+ fixes.push(`已自动将 packages/core/version.ts 同步为 v${pkgVer}`);
420
+ return check('ok', `v${pkgVer} (已自动修复版本漂移: 从 v${coreVer} 同步为 v${pkgVer})`);
421
+ }
422
+
423
+ return check(
424
+ 'err',
425
+ `检测到内核版本漂移: package.json (v${pkgVer}) 与 packages/core/version.ts (v${coreVer}) 不一致!`,
426
+ true,
427
+ );
412
428
  }
413
429
 
414
- const coreVer = match[1];
415
- if (coreVer === pkgVer) {
416
- return check('ok', `v${pkgVer} (package.json 与 packages/core/version.ts 严格一致)`);
430
+ // 动态模式:检查是否为 readPlatformVersion() 架构并对齐 FALLBACK_VERSION
431
+ const isDynamic = coreContent.includes('readPlatformVersion');
432
+ const fallbackMatch = coreContent.match(/FALLBACK_VERSION\s*=\s*['"]([^'"]+)['"]/);
433
+
434
+ if (isDynamic && fallbackMatch) {
435
+ const fallbackVer = fallbackMatch[1];
436
+ if (fallbackVer === pkgVer) {
437
+ return check('ok', `v${pkgVer} (package.json 与 packages/core/version.ts 动态自解析完全对齐)`);
438
+ }
439
+
440
+ if (fix) {
441
+ const updated = coreContent.replace(/FALLBACK_VERSION\s*=\s*['"][^'"]+['"]/, `FALLBACK_VERSION = '${pkgVer}'`);
442
+ fs.writeFileSync(coreVersionFile, updated, 'utf-8');
443
+ fixes.push(`已自动将 packages/core/version.ts FALLBACK_VERSION 同步为 v${pkgVer}`);
444
+ return check('ok', `v${pkgVer} (已自动修复版本漂移: FALLBACK_VERSION 从 v${fallbackVer} 同步为 v${pkgVer})`);
445
+ }
446
+
447
+ return check(
448
+ 'err',
449
+ `检测到内核兜底版本漂移: package.json (v${pkgVer}) 与 packages/core/version.ts FALLBACK_VERSION (v${fallbackVer}) 不一致!`,
450
+ true,
451
+ );
417
452
  }
418
453
 
419
- if (fix) {
420
- const updated = coreContent.replace(/PLATFORM_VERSION\s*=\s*['"][^'"]+['"]/, `PLATFORM_VERSION = '${pkgVer}'`);
421
- fs.writeFileSync(coreVersionFile, updated, 'utf-8');
422
- fixes.push(`已自动将 packages/core/version.ts 同步为 v${pkgVer}`);
423
- return check('ok', `v${pkgVer} (已自动修复版本漂移: 从 v${coreVer} 同步为 v${pkgVer})`);
454
+ if (isDynamic) {
455
+ return check('ok', `v${pkgVer} (动态自解析模式)`);
424
456
  }
425
457
 
426
- return check(
427
- 'err',
428
- `检测到内核版本漂移: package.json (v${pkgVer}) 与 packages/core/version.ts (v${coreVer}) 不一致!`,
429
- true,
430
- );
458
+ return check('warn', `未在 packages/core/version.ts 中解析到 PLATFORM_VERSION 或 FALLBACK_VERSION`);
431
459
  } catch (e) {
432
460
  return check('err', `版本一致性检查失败: ${e.message}`);
433
461
  }
@@ -594,7 +622,24 @@ export async function runDoctor(options = {}) {
594
622
  }
595
623
 
596
624
  // 4. 端口可用性检查
597
- const targetPort = options.port || parseInt(process.env.PORT || '9000', 10);
625
+ // PORT 非法时 `parseInt` 会得到 NaN,`NaN || x` 仍是 NaN —— 随后
626
+ // `server.listen(NaN)` 抛 ERR_SOCKET_BAD_PORT,把「诊断工具」自己搞崩。
627
+ // doctor 的职责是把问题**报出来**,所以这里回落到默认端口并把非法值记成一条检查项。
628
+ const rawPort = options.port ?? parseInt(process.env.PORT || '9000', 10);
629
+ const portInvalid = !Number.isInteger(rawPort) || rawPort < 1 || rawPort > 65535;
630
+ const targetPort = portInvalid ? 9000 : rawPort;
631
+ if (portInvalid) {
632
+ // 先把「PORT 本身非法」作为一条失败项报出来,否则用户只会看到
633
+ // 「Port 9000 可用」这种与真实配置无关的结论。
634
+ checks.push({
635
+ name: 'PORT 配置',
636
+ // 必须是 'err':`allOk` 只把 'err' 判为阻断('warn' 仍算通过),
637
+ // 用一个自造的状态会让「报了问题但退出码 0」同时成立。
638
+ status: 'err',
639
+ message: `PORT 非法(实际为 ${JSON.stringify(process.env.PORT ?? options.port ?? null)}):服务入口会拒绝启动。已按 9000 继续探测。`,
640
+ fixable: false,
641
+ });
642
+ }
598
643
  const isPortAvailable = await checkPortAvailable(targetPort);
599
644
  if (isPortAvailable) {
600
645
  checks.push({
@@ -688,3 +733,397 @@ export async function runDoctor(options = {}) {
688
733
 
689
734
  return { ok: allOk, checks, fixes };
690
735
  }
736
+
737
+ /**
738
+ * 检查官方 npm 远程源是否存在 openlearn-next 平台更新
739
+ * @param {Object} [options]
740
+ * @returns {Promise<{ ok: boolean, hasUpdate: boolean, currentVer: string, latestVer?: string }>}
741
+ */
742
+ export async function runCheckUpdate(options = {}) {
743
+ const log = options.silent ? () => {} : (msg) => console.log(`[openlearn-next] ${msg}`);
744
+ const pkgPath = path.join(__dirname, 'package.json');
745
+ const currentVer = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version : '0.6.0';
746
+
747
+ if (!options.silent) {
748
+ console.log(`[openlearn-next] 正在检查 npm 官方源... (当前本地版本: v${currentVer})`);
749
+ }
750
+
751
+ let latestVer = null;
752
+ const registries = [
753
+ 'https://registry.npmjs.org/openlearn-next/latest',
754
+ 'https://registry.npmmirror.com/openlearn-next/latest',
755
+ ];
756
+
757
+ for (const url of registries) {
758
+ try {
759
+ const controller = new AbortController();
760
+ const timer = setTimeout(() => controller.abort(), 3500);
761
+ const res = await fetch(url, { signal: controller.signal });
762
+ clearTimeout(timer);
763
+ if (res.ok) {
764
+ const data = await res.json();
765
+ if (data.version) {
766
+ latestVer = data.version;
767
+ break;
768
+ }
769
+ }
770
+ } catch {}
771
+ }
772
+
773
+ if (!latestVer) {
774
+ log(`⚠ 网络连接超时或离线,未能获取远程版本信息。`);
775
+ return { ok: true, hasUpdate: false, currentVer };
776
+ }
777
+
778
+ const hasUpdate = compareVersions(latestVer, currentVer) > 0;
779
+ if (!options.silent) {
780
+ const bold = '\x1b[1m';
781
+ const green = '\x1b[32m';
782
+ const yellow = '\x1b[33m';
783
+ const cyan = '\x1b[36m';
784
+ const reset = '\x1b[0m';
785
+
786
+ if (hasUpdate) {
787
+ console.log(`\n${bold}${yellow}🚀 发现平台新版本: v${latestVer} (当前: v${currentVer})${reset}`);
788
+ console.log(` 建议通过以下方式升级:`);
789
+ console.log(` • NPX 免安装运行: ${cyan}npx openlearn-next@latest${reset}`);
790
+ console.log(` • NPM 全局升级: ${cyan}npm i -g openlearn-next@latest${reset}`);
791
+ console.log(` • 源码部署更新: ${cyan}git pull && pnpm build && pnpm start${reset}\n`);
792
+ } else {
793
+ console.log(`\n ${green}✓ 当前已是最新版本 (v${currentVer}),无须升级。${reset}\n`);
794
+ }
795
+ }
796
+
797
+ return { ok: true, hasUpdate, currentVer, latestVer };
798
+ }
799
+
800
+ /**
801
+ * 平台核心扩展点元数据契约(用于零依赖环境的升级兼容性分析)
802
+ */
803
+ export const CORE_EXTENSION_POINTS = [
804
+ // ── Commands ─────────────────────────────────────────────────────────────
805
+ { id: 'lesson.create', kind: 'command', since: '0.1.0' },
806
+ { id: 'lesson.update', kind: 'command', since: '0.1.0' },
807
+ { id: 'lesson.delete', kind: 'command', since: '0.1.0' },
808
+ { id: 'assignment.grade', kind: 'command', since: '0.2.0' },
809
+ { id: 'assignment.peer_review', kind: 'command', since: '0.3.0' },
810
+ { id: 'whiteboard.draw', kind: 'command', since: '0.1.0' },
811
+ { id: 'whiteboard.clear', kind: 'command', since: '0.1.0' },
812
+ { id: 'vfs.write_file', kind: 'command', since: '0.2.0' },
813
+ { id: 'vfs.read_file', kind: 'command', since: '0.2.0' },
814
+ {
815
+ id: 'vfs.read_path',
816
+ kind: 'command',
817
+ since: '0.1.0',
818
+ deprecated: '0.6.0',
819
+ removedIn: '0.7.0',
820
+ replacement: "Use 'vfs.read_file' instead",
821
+ },
822
+ {
823
+ id: 'classroom.legacy_poll',
824
+ kind: 'command',
825
+ since: '0.1.0',
826
+ deprecated: '0.5.0',
827
+ removedIn: '0.7.0',
828
+ replacement: "Use 'classroom.quick_poll' instead",
829
+ },
830
+
831
+ // ── Events ───────────────────────────────────────────────────────────────
832
+ { id: 'lesson.created', kind: 'event', since: '0.1.0' },
833
+ { id: 'lesson.updated', kind: 'event', since: '0.1.0' },
834
+ { id: 'lesson.deleted', kind: 'event', since: '0.1.0' },
835
+ { id: 'assignment.graded', kind: 'event', since: '0.2.0' },
836
+ { id: 'classroom.pacing_updated', kind: 'event', since: '0.3.0' },
837
+ { id: 'classroom.quick_poll_updated', kind: 'event', since: '0.3.0' },
838
+ { id: 'quiz.answered', kind: 'event', since: '0.6.0' },
839
+ {
840
+ id: 'whiteboard.quiz_answered',
841
+ kind: 'event',
842
+ since: '0.1.0',
843
+ deprecated: '0.6.0',
844
+ removedIn: '0.7.0',
845
+ replacement: "Use 'quiz.answered' instead",
846
+ },
847
+
848
+ // ── UI Slots ─────────────────────────────────────────────────────────────
849
+ { id: 'teacher.tab', kind: 'slot', since: '0.1.0' },
850
+ { id: 'classroom.tool', kind: 'slot', since: '0.1.0' },
851
+ { id: 'teacher.dashboard.widget', kind: 'slot', since: '0.2.0' },
852
+ { id: 'student.view', kind: 'slot', since: '0.2.0' },
853
+ { id: 'student.lesson.tool', kind: 'slot', since: '0.2.0' },
854
+ { id: 'anchor:lesson-header', kind: 'slot', since: '0.2.6' },
855
+ { id: 'anchor:teacher-top-nav', kind: 'slot', since: '0.2.6' },
856
+ {
857
+ id: 'legacy.classroom.tab',
858
+ kind: 'slot',
859
+ since: '0.1.0',
860
+ deprecated: '0.5.0',
861
+ removedIn: '0.7.0',
862
+ replacement: "Use 'teacher.tab' instead",
863
+ },
864
+
865
+ // ── Services ─────────────────────────────────────────────────────────────
866
+ { id: '@openlearn/core:ICommandBusService', kind: 'service', since: '0.1.0' },
867
+ { id: '@openlearn/core:IEventBusService', kind: 'service', since: '0.1.0' },
868
+ { id: '@openlearn/core:IStorageService', kind: 'service', since: '0.1.0' },
869
+ { id: '@openlearn/core:IAIService', kind: 'service', since: '0.2.0' },
870
+ { id: '@openlearn/core:IDatabase', kind: 'service', since: '0.1.0' },
871
+ {
872
+ id: '@openlearn/core:ILegacyAuthService',
873
+ kind: 'service',
874
+ since: '0.1.0',
875
+ deprecated: '0.5.0',
876
+ removedIn: '0.7.0',
877
+ replacement: "Use '@openlearn/core:IAuthService' instead",
878
+ },
879
+ ];
880
+
881
+ /**
882
+ * 分析插件列表相对于目标平台版本的升级兼容性 (Zero External Dependencies)
883
+ * @param {Array} plugins 插件 Manifest 列表
884
+ * @param {string} targetVersion 拟升级的目标版本 (例如 '0.7.0')
885
+ */
886
+ export function generateUpgradeCompatibilityReport(plugins, targetVersion) {
887
+ const extPointMap = new Map(CORE_EXTENSION_POINTS.map((p) => [p.id, p]));
888
+ const pluginResults = [];
889
+
890
+ for (const plugin of plugins) {
891
+ const issues = [];
892
+ const pluginId = plugin.id;
893
+ const pluginName = plugin.name || pluginId;
894
+ const pluginVersion = plugin.version || '0.0.0';
895
+ const engineReq = plugin.engines?.openlearn;
896
+
897
+ // 1. 检查 engines.openlearn 语义化版本约束
898
+ if (engineReq && engineReq !== '*') {
899
+ if (!satisfiesVersionRange(targetVersion, engineReq)) {
900
+ issues.push({
901
+ level: 'error',
902
+ message: `Plugin engines.openlearn constraint "${engineReq}" is not satisfied by target version v${targetVersion}`,
903
+ });
904
+ }
905
+ }
906
+
907
+ // 2. 检查 UI 插槽扩展点贡献 (contributes)
908
+ if (plugin.contributes && typeof plugin.contributes === 'object') {
909
+ for (const slotKey of Object.keys(plugin.contributes)) {
910
+ const meta = extPointMap.get(slotKey);
911
+ if (meta) {
912
+ if (meta.removedIn && compareVersions(targetVersion, meta.removedIn) >= 0) {
913
+ issues.push({
914
+ level: 'error',
915
+ extensionPoint: slotKey,
916
+ replacement: meta.replacement,
917
+ message: `UI Slot "${slotKey}" is removed in v${meta.removedIn}. ${meta.replacement || ''}`,
918
+ });
919
+ } else if (meta.deprecated && compareVersions(targetVersion, meta.deprecated) >= 0) {
920
+ issues.push({
921
+ level: 'warn',
922
+ extensionPoint: slotKey,
923
+ replacement: meta.replacement,
924
+ message: `UI Slot "${slotKey}" is deprecated since v${meta.deprecated} and will be removed in v${meta.removedIn || 'a future minor release'}. ${meta.replacement || ''}`,
925
+ });
926
+ }
927
+ }
928
+ }
929
+ }
930
+
931
+ // 3. 检查依赖注入服务 Token (requires / optional)
932
+ const serviceList = [
933
+ ...(Array.isArray(plugin.requires) ? plugin.requires : []),
934
+ ...(Array.isArray(plugin.optional) ? plugin.optional : []),
935
+ ];
936
+ for (const rawToken of serviceList) {
937
+ const lastAt = rawToken.lastIndexOf('@');
938
+ const tokenName = lastAt > 0 ? rawToken.slice(0, lastAt) : rawToken;
939
+ const meta = extPointMap.get(tokenName);
940
+ if (meta) {
941
+ if (meta.removedIn && compareVersions(targetVersion, meta.removedIn) >= 0) {
942
+ issues.push({
943
+ level: 'error',
944
+ extensionPoint: tokenName,
945
+ replacement: meta.replacement,
946
+ message: `Service Token "${tokenName}" is removed in v${meta.removedIn}. ${meta.replacement || ''}`,
947
+ });
948
+ } else if (meta.deprecated && compareVersions(targetVersion, meta.deprecated) >= 0) {
949
+ issues.push({
950
+ level: 'warn',
951
+ extensionPoint: tokenName,
952
+ replacement: meta.replacement,
953
+ message: `Service Token "${tokenName}" is deprecated since v${meta.deprecated} and will be removed in v${meta.removedIn || 'a future minor release'}. ${meta.replacement || ''}`,
954
+ });
955
+ }
956
+ }
957
+ }
958
+
959
+ // 4. 检查显式声明的 Commands
960
+ const cmdList = Array.isArray(plugin.commands) ? plugin.commands : [];
961
+ for (const cmd of cmdList) {
962
+ const meta = extPointMap.get(cmd);
963
+ if (meta) {
964
+ if (meta.removedIn && compareVersions(targetVersion, meta.removedIn) >= 0) {
965
+ issues.push({
966
+ level: 'error',
967
+ extensionPoint: cmd,
968
+ replacement: meta.replacement,
969
+ message: `Command "${cmd}" is removed in v${meta.removedIn}. ${meta.replacement || ''}`,
970
+ });
971
+ } else if (meta.deprecated && compareVersions(targetVersion, meta.deprecated) >= 0) {
972
+ issues.push({
973
+ level: 'warn',
974
+ extensionPoint: cmd,
975
+ replacement: meta.replacement,
976
+ message: `Command "${cmd}" is deprecated since v${meta.deprecated}. ${meta.replacement || ''}`,
977
+ });
978
+ }
979
+ }
980
+ }
981
+
982
+ let status = 'compatible';
983
+ if (issues.some((i) => i.level === 'error')) {
984
+ status = 'incompatible';
985
+ } else if (issues.some((i) => i.level === 'warn')) {
986
+ status = 'has_warnings';
987
+ }
988
+
989
+ pluginResults.push({
990
+ id: pluginId,
991
+ name: pluginName,
992
+ version: pluginVersion,
993
+ openlearnEngineReq: engineReq,
994
+ status,
995
+ issues,
996
+ });
997
+ }
998
+
999
+ const incompatibleCount = pluginResults.filter((p) => p.status === 'incompatible').length;
1000
+ const warningCount = pluginResults.filter((p) => p.status === 'has_warnings').length;
1001
+ const compatibleCount = pluginResults.filter((p) => p.status === 'compatible').length;
1002
+
1003
+ let overallStatus = 'compatible';
1004
+ if (incompatibleCount > 0) {
1005
+ overallStatus = 'incompatible';
1006
+ } else if (warningCount > 0) {
1007
+ overallStatus = 'has_warnings';
1008
+ }
1009
+
1010
+ return {
1011
+ targetVersion,
1012
+ generatedAt: new Date().toISOString(),
1013
+ overallStatus,
1014
+ summary: {
1015
+ totalPlugins: pluginResults.length,
1016
+ compatibleCount,
1017
+ warningCount,
1018
+ incompatibleCount,
1019
+ },
1020
+ plugins: pluginResults,
1021
+ };
1022
+ }
1023
+
1024
+ /**
1025
+ * 生成并打印插件升级兼容性报告 (Issue #146)
1026
+ * @param {Object} [options]
1027
+ * @param {string} [options.targetVersion] 目标平台版本,默认取当前平台版本
1028
+ * @param {string} [options.dbPath] 数据库路径
1029
+ * @param {string} [options.pluginsDir] 本地插件目录路径
1030
+ * @param {Array} [options.manifests] 直接传入的待检测 manifest 列表(单测/模拟用)
1031
+ * @param {boolean} [options.silent] 是否静默输出
1032
+ * @returns {Promise<any>}
1033
+ */
1034
+ export async function runPluginCompatibilityReport(options = {}) {
1035
+ const pkgPath = path.join(__dirname, 'package.json');
1036
+ const curVer = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version : '0.6.1';
1037
+ const targetVersion = options.targetVersion || process.env.OPENLEARN_UPGRADE_TARGET_VERSION || curVer;
1038
+
1039
+ const manifests = [];
1040
+ if (Array.isArray(options.manifests)) {
1041
+ manifests.push(...options.manifests);
1042
+ } else {
1043
+ // 1. 读取数据库
1044
+ const dbPath = options.dbPath
1045
+ ? path.resolve(options.dbPath)
1046
+ : process.env.OPENLEARN_DB_PATH
1047
+ ? path.resolve(process.env.OPENLEARN_DB_PATH)
1048
+ : path.join(os.homedir(), 'openlearn-next', 'data.db');
1049
+
1050
+ if (fs.existsSync(dbPath)) {
1051
+ try {
1052
+ const Database = (await import('better-sqlite3')).default;
1053
+ const db = new Database(dbPath, { readonly: true });
1054
+ const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='plugins'").get();
1055
+ if (tableCheck) {
1056
+ const rows = db
1057
+ .prepare("SELECT id, name, manifest FROM plugins WHERE id NOT LIKE '@openlearn/plugin-%'")
1058
+ .all();
1059
+ for (const row of rows) {
1060
+ if (row.manifest) {
1061
+ try {
1062
+ manifests.push(JSON.parse(row.manifest));
1063
+ } catch {}
1064
+ }
1065
+ }
1066
+ }
1067
+ db.close();
1068
+ } catch {}
1069
+ }
1070
+
1071
+ // 2. 扫描 plugins 目录
1072
+ const localPluginsDir = options.pluginsDir || path.join(__dirname, 'plugins');
1073
+ if (fs.existsSync(localPluginsDir)) {
1074
+ try {
1075
+ const entries = fs.readdirSync(localPluginsDir, { withFileTypes: true });
1076
+ for (const ent of entries) {
1077
+ if (ent.isDirectory()) {
1078
+ const mfPath = path.join(localPluginsDir, ent.name, 'manifest.json');
1079
+ if (fs.existsSync(mfPath)) {
1080
+ try {
1081
+ const mf = JSON.parse(fs.readFileSync(mfPath, 'utf-8'));
1082
+ if (mf.id && !manifests.some((m) => m.id === mf.id)) {
1083
+ manifests.push(mf);
1084
+ }
1085
+ } catch {}
1086
+ }
1087
+ }
1088
+ }
1089
+ } catch {}
1090
+ }
1091
+ }
1092
+
1093
+ const report = generateUpgradeCompatibilityReport(manifests, targetVersion);
1094
+
1095
+ if (!options.silent) {
1096
+ const bold = '\x1b[1m';
1097
+ const green = '\x1b[32m';
1098
+ const yellow = '\x1b[33m';
1099
+ const red = '\x1b[31m';
1100
+ const cyan = '\x1b[36m';
1101
+ const reset = '\x1b[0m';
1102
+
1103
+ console.log(`\n${bold}${cyan}╔═════════════════════════════════════════════════════════════╗${reset}`);
1104
+ console.log(`${bold}${cyan}║ OpenLearn Plugin Upgrade Compatibility Report (Issue #146) ║${reset}`);
1105
+ console.log(`${bold}${cyan}╚═════════════════════════════════════════════════════════════╝${reset}\n`);
1106
+ console.log(` • 目标版本: ${bold}v${report.targetVersion}${reset}`);
1107
+ console.log(` • 扫描插件: ${report.summary.totalPlugins} 个`);
1108
+ console.log(` • 完全兼容: ${green}${report.summary.compatibleCount} 个${reset}`);
1109
+ console.log(` • 存在弃用警告: ${yellow}${report.summary.warningCount} 个${reset}`);
1110
+ console.log(` • 存在破坏性冲突: ${red}${report.summary.incompatibleCount} 个${reset}\n`);
1111
+
1112
+ for (const p of report.plugins) {
1113
+ const statusIcon =
1114
+ p.status === 'compatible'
1115
+ ? `${green}✓${reset}`
1116
+ : p.status === 'has_warnings'
1117
+ ? `${yellow}⚠${reset}`
1118
+ : `${red}✗${reset}`;
1119
+ console.log(` ${statusIcon} ${bold}${p.id}${reset} (v${p.version})`);
1120
+ for (const issue of p.issues) {
1121
+ const issuePrefix = issue.level === 'error' ? `${red}[ERROR]${reset}` : `${yellow}[WARN]${reset}`;
1122
+ console.log(` ↳ ${issuePrefix} ${issue.message}`);
1123
+ }
1124
+ }
1125
+ console.log('');
1126
+ }
1127
+
1128
+ return report;
1129
+ }