openlearn-next 0.7.4 → 0.7.5

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 (61) hide show
  1. package/README.md +1 -1
  2. package/cli-doctor.mjs +18 -5
  3. package/cli.mjs +49 -0
  4. package/dist/assets/{ActivityWorkspaceWidget-CMN8elwQ.js → ActivityWorkspaceWidget-Dq4kjBS2.js} +1 -1
  5. package/dist/assets/{AdminDirectoryView-Dlz7q-xf.js → AdminDirectoryView-B6OAs5Rj.js} +1 -1
  6. package/dist/assets/AssessmentCenter-B3f1DxSG.js +1 -0
  7. package/dist/assets/AssignmentFilePreview-DQbvbgnd.js +3 -0
  8. package/dist/assets/{BatchPickerModal-CzutdtIz.js → BatchPickerModal-CoOWloSS.js} +1 -1
  9. package/dist/assets/{ClassesView-RkFPpWRu.js → ClassesView-Q56Kf4eL.js} +5 -5
  10. package/dist/assets/{ClassroomCountdownWidget-gs42lIGW.js → ClassroomCountdownWidget-C6luhy8P.js} +1 -1
  11. package/dist/assets/{CloudDriveModal-C6JtK08M.js → CloudDriveModal-DLf4fbKR.js} +1 -1
  12. package/dist/assets/ComputerLabView-D9-Tk7yA.js +1 -0
  13. package/dist/assets/{CourseManagement-DNeaJcT9.js → CourseManagement-DNSfyre-.js} +1 -1
  14. package/dist/assets/{CourseWizardModal-vw876VJr.js → CourseWizardModal-DAueHXcl.js} +1 -1
  15. package/dist/assets/{CoursewareHubPanel-CyxDIJAF.js → CoursewareHubPanel-DLjH-UzF.js} +1 -1
  16. package/dist/assets/{Dashboard-C1iYnfCh.js → Dashboard-qMVadkd2.js} +2 -2
  17. package/dist/assets/{ExportWeightModal-CTZ-O_wi.js → ExportWeightModal-Dkh7OC8U.js} +1 -1
  18. package/dist/assets/{HelpTour-D_q1z_MI.js → HelpTour-BLuiS8rr.js} +1 -1
  19. package/dist/assets/{HelpView-wyKldm_R.js → HelpView-CTF6B9ml.js} +1 -1
  20. package/dist/assets/{ImportLessonsModal-C09Z-qtx.js → ImportLessonsModal-Ra-OF_Vo.js} +1 -1
  21. package/dist/assets/{ImportModal-DqjqNeH4.js → ImportModal-H5s5MyZY.js} +1 -1
  22. package/dist/assets/{InteractiveCoursewareViewer-V97euiJF.js → InteractiveCoursewareViewer-BfHEDrCf.js} +1 -1
  23. package/dist/assets/InteractiveWhiteboard-KBzmiNBG.js +65 -0
  24. package/dist/assets/{LazyCourseware-0iFYlRGj.js → LazyCourseware-BaZS8-i1.js} +2 -2
  25. package/dist/assets/LazyWhiteboard-DGyln3JC.js +2 -0
  26. package/dist/assets/LessonEditorView-B5JXYyup.js +1 -0
  27. package/dist/assets/{LiveClassroomView-CaS1DzSS.js → LiveClassroomView-DezAlve2.js} +7 -7
  28. package/dist/assets/{NotificationDetailModal-4MdNF4tw.js → NotificationDetailModal-DbMMrIHQ.js} +1 -1
  29. package/dist/assets/{PluginView-BqTbAp30.js → PluginView-CvIr-ANj.js} +1 -1
  30. package/dist/assets/{ProcessLogsModal-Cfr-T9Gd.js → ProcessLogsModal-_BwkmJVw.js} +1 -1
  31. package/dist/assets/{QuizGeneratorModal-xTq22vDq.js → QuizGeneratorModal-CTj2moWM.js} +1 -1
  32. package/dist/assets/StudentAssignmentEvalPanel-DrfZT8Lr.js +1 -0
  33. package/dist/assets/{StudentAssignmentView-Cy_JdPYB.js → StudentAssignmentView-D_EhblVQ.js} +1 -1
  34. package/dist/assets/StudentLessonView-BHG50-4M.js +1 -0
  35. package/dist/assets/{StudentView-CpJsG7FT.js → StudentView-DKDhnJQx.js} +2 -2
  36. package/dist/assets/{SystemResourceLibraryModal-YZ85mv_v.js → SystemResourceLibraryModal-C5y8H5Pf.js} +1 -1
  37. package/dist/assets/TeacherView-CWDK3DOe.js +2 -0
  38. package/dist/assets/{TimetableView-D_4XmrRN.js → TimetableView-C8kdJlf7.js} +1 -1
  39. package/dist/assets/{UTIF-FfCsrHhe.js → UTIF-4E-wdh3X.js} +1 -1
  40. package/dist/assets/index-BBznBaN5.js +131 -0
  41. package/dist/assets/index-DPky2leG.css +1 -0
  42. package/dist/assets/{index.es-DyEF0CZ9.js → index.es-WXdI0L-F.js} +1 -1
  43. package/dist/assets/pluginParsers-DkiO-BBu.js +1 -0
  44. package/dist/assets/student-workspace-1yVTbWHC.js +1 -0
  45. package/dist/assets/{vendor-icons-CXrZ3R2V.js → vendor-icons-hc6MjYFv.js} +1 -1
  46. package/dist/assets/{vendor-pdf-C1Vh2Aqq.js → vendor-pdf-By0fofA0.js} +3 -3
  47. package/dist/assets/{vendor-pptx-Bzn6whfK.js → vendor-pptx-EjNUtdws.js} +178 -178
  48. package/dist/index.html +3 -3
  49. package/dist/server.cjs +538 -174
  50. package/package.json +6 -4
  51. package/scripts/assert-node-runtime.mjs +165 -0
  52. package/dist/assets/AssessmentCenter-CaoAI9Ke.js +0 -1
  53. package/dist/assets/ComputerLabView-Dr-g8FrU.js +0 -1
  54. package/dist/assets/InteractiveWhiteboard-COLMh_-w.js +0 -65
  55. package/dist/assets/LazyWhiteboard-CcoBIx6V.js +0 -2
  56. package/dist/assets/LessonEditorView-BWmtKwmP.js +0 -1
  57. package/dist/assets/StudentLessonView-B3fKejCU.js +0 -1
  58. package/dist/assets/TeacherView-B3zhzd5g.js +0 -2
  59. package/dist/assets/index-BzXADCXZ.js +0 -131
  60. package/dist/assets/index-C6hlh8pw.css +0 -1
  61. package/dist/assets/pluginParsers-PzgFzxS9.js +0 -1
package/dist/server.cjs CHANGED
@@ -944,6 +944,9 @@ var init_manifest_schema = __esm({
944
944
  "stage.display.overlay": import_zod.z.array(frontendSlotEntrySchema).optional(),
945
945
  "stage.display.action": import_zod.z.array(frontendSlotEntrySchema).optional(),
946
946
  "assignment.submission.preview": import_zod.z.array(frontendSlotEntrySchema).optional(),
947
+ // 作业**附件**预览:插件声明自己能处理的文件类型(extensions / mimeTypes),
948
+ // 宿主在预览对话框里按类型匹配后给出入口。数据型贡献(远程插件可用)。
949
+ "assignment.file.preview": import_zod.z.array(frontendSlotEntrySchema).optional(),
947
950
  "student.classroom.overlay": import_zod.z.array(frontendSlotEntrySchema).optional(),
948
951
  "student.profile.tab": import_zod.z.array(frontendSlotEntrySchema).optional(),
949
952
  "whiteboard.renderer": import_zod.z.array(frontendSlotEntrySchema).optional(),
@@ -1188,12 +1191,12 @@ var init_manifest_schema = __esm({
1188
1191
  // packages/core/di/api-key-crypto.ts
1189
1192
  function resolveEncryptionKeyStorageDir(env = process.env, cwd = process.cwd()) {
1190
1193
  if (env.OPENLEARN_DATA_DIR?.trim()) {
1191
- return import_node_path4.default.join(import_node_path4.default.resolve(env.OPENLEARN_DATA_DIR.trim()), "storage");
1194
+ return import_node_path5.default.join(import_node_path5.default.resolve(env.OPENLEARN_DATA_DIR.trim()), "storage");
1192
1195
  }
1193
1196
  if (env.OPENLEARN_DB_PATH?.trim()) {
1194
- return import_node_path4.default.join(import_node_path4.default.dirname(import_node_path4.default.resolve(env.OPENLEARN_DB_PATH.trim())), "storage");
1197
+ return import_node_path5.default.join(import_node_path5.default.dirname(import_node_path5.default.resolve(env.OPENLEARN_DB_PATH.trim())), "storage");
1195
1198
  }
1196
- return import_node_path4.default.join(cwd, "storage");
1199
+ return import_node_path5.default.join(cwd, "storage");
1197
1200
  }
1198
1201
  function normalizeKeyHex(raw) {
1199
1202
  let value = raw.trim();
@@ -1214,7 +1217,7 @@ function readProcessEnvKey(env) {
1214
1217
  return normalizeKeyHex(raw);
1215
1218
  }
1216
1219
  function readDotEnvKey(cwd) {
1217
- const envPath = import_node_path4.default.resolve(cwd, ".env");
1220
+ const envPath = import_node_path5.default.resolve(cwd, ".env");
1218
1221
  if (!import_node_fs4.default.existsSync(envPath)) return null;
1219
1222
  let content = "";
1220
1223
  try {
@@ -1227,7 +1230,7 @@ function readDotEnvKey(cwd) {
1227
1230
  return normalizeKeyHex(match[1] ?? "");
1228
1231
  }
1229
1232
  function keyFilePath(storageDir) {
1230
- return import_node_path4.default.join(storageDir, ENCRYPTION_KEY_FILE_NAME);
1233
+ return import_node_path5.default.join(storageDir, ENCRYPTION_KEY_FILE_NAME);
1231
1234
  }
1232
1235
  function chmodKeyFile(filePath) {
1233
1236
  try {
@@ -1253,7 +1256,7 @@ function keysEqual(left, right) {
1253
1256
  return import_node_crypto.default.timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
1254
1257
  }
1255
1258
  function persistGeneratedKey(filePath, generated) {
1256
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(filePath), { recursive: true });
1259
+ import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(filePath), { recursive: true });
1257
1260
  try {
1258
1261
  import_node_fs4.default.writeFileSync(filePath, `${generated}
1259
1262
  `, { mode: 384, flag: "wx" });
@@ -1400,13 +1403,13 @@ function encryptStoredPlaintextApiKeys(db2) {
1400
1403
  }
1401
1404
  return { updated, skipped };
1402
1405
  }
1403
- var import_node_crypto, import_node_fs4, import_node_path4, ENCRYPTION_KEY_FILE_NAME, AUTO_GENERATED_KEY_WARNING, HEX_64, EncryptionKeyInvalidError, EncryptionKeyMismatchError, cached;
1406
+ var import_node_crypto, import_node_fs4, import_node_path5, ENCRYPTION_KEY_FILE_NAME, AUTO_GENERATED_KEY_WARNING, HEX_64, EncryptionKeyInvalidError, EncryptionKeyMismatchError, cached;
1404
1407
  var init_api_key_crypto = __esm({
1405
1408
  "packages/core/di/api-key-crypto.ts"() {
1406
1409
  "use strict";
1407
1410
  import_node_crypto = __toESM(require("node:crypto"), 1);
1408
1411
  import_node_fs4 = __toESM(require("node:fs"), 1);
1409
- import_node_path4 = __toESM(require("node:path"), 1);
1412
+ import_node_path5 = __toESM(require("node:path"), 1);
1410
1413
  ENCRYPTION_KEY_FILE_NAME = "encryption-key";
1411
1414
  AUTO_GENERATED_KEY_WARNING = "\u6B63\u5728\u4F7F\u7528\u81EA\u52A8\u751F\u6210\u7684\u52A0\u5BC6\u5BC6\u94A5\uFF08storage/encryption-key\uFF0C\u4E0E bootstrap-credentials.txt \u540C\u76EE\u5F55\uFF0C\u6743\u9650 600\uFF09\u3002\u5EFA\u8BAE\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ENCRYPTION_KEY \u4E3A\u8BE5\u6587\u4EF6\u4E2D\u7684 64 \u4F4D hex\uFF0C\u5E76\u79BB\u7EBF\u5907\u4EFD\u8BE5\u6587\u4EF6\u3002site-backup \u4E0D\u5305\u542B\u6B64\u6587\u4EF6\u3002\u4E22\u5931\u540E\u65E0\u6CD5\u89E3\u5BC6 AI Provider API Key \u4E0E\u73ED\u7EA7\u4E34\u65F6\u53E3\u4EE4\u3002";
1412
1415
  HEX_64 = /^[0-9a-fA-F]{64}$/;
@@ -1921,10 +1924,10 @@ function getMaxUncompressedSize() {
1921
1924
  }
1922
1925
  function isInsideRoot(root, target) {
1923
1926
  if (!root || !target) return false;
1924
- const rel = path8.relative(path8.resolve(root), path8.resolve(target));
1927
+ const rel = path9.relative(path9.resolve(root), path9.resolve(target));
1925
1928
  if (rel === "") return false;
1926
- if (path8.isAbsolute(rel)) return false;
1927
- return rel !== ".." && !rel.startsWith(".." + path8.sep);
1929
+ if (path9.isAbsolute(rel)) return false;
1930
+ return rel !== ".." && !rel.startsWith(".." + path9.sep);
1928
1931
  }
1929
1932
  function realPathOrNull(target) {
1930
1933
  try {
@@ -1934,7 +1937,7 @@ function realPathOrNull(target) {
1934
1937
  }
1935
1938
  }
1936
1939
  function realPathOrSelf(target) {
1937
- return realPathOrNull(target) ?? path8.resolve(target);
1940
+ return realPathOrNull(target) ?? path9.resolve(target);
1938
1941
  }
1939
1942
  function getMaxUploadBodyBytes() {
1940
1943
  return getMaxUncompressedSize();
@@ -1949,7 +1952,7 @@ function writeBufferToTempZip(buffer, prefix = "plugin-upload-") {
1949
1952
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
1950
1953
  throw new Error("[install-utils] \u7A7A\u7684 ZIP Buffer");
1951
1954
  }
1952
- const zipPath = path8.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
1955
+ const zipPath = path9.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
1953
1956
  fs7.writeFileSync(zipPath, buffer);
1954
1957
  return zipPath;
1955
1958
  }
@@ -2209,13 +2212,13 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
2209
2212
  const esbuild = await import("esbuild");
2210
2213
  const allowedRoots = [];
2211
2214
  const allowRoot = (dir) => {
2212
- const lexical = path8.resolve(dir);
2215
+ const lexical = path9.resolve(dir);
2213
2216
  if (allowedRoots.some((r) => r.lexical === lexical)) return;
2214
2217
  allowedRoots.push({ lexical, real: realPathOrSelf(lexical) });
2215
2218
  };
2216
2219
  allowRoot(resolveDir);
2217
2220
  const isPermittedPath = (target) => {
2218
- const resolved = path8.resolve(target);
2221
+ const resolved = path9.resolve(target);
2219
2222
  if (allowedRoots.some((r) => isInsideRoot(r.lexical, resolved))) return true;
2220
2223
  const real = realPathOrNull(resolved);
2221
2224
  if (!real) return false;
@@ -2242,14 +2245,14 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
2242
2245
  setup(build) {
2243
2246
  build.onResolve({ filter: /.*/ }, (args) => {
2244
2247
  if (args.path.includes("core/di/interfaces")) {
2245
- const tsPath = path8.resolve(process.cwd(), "packages/core/di/interfaces.ts");
2248
+ const tsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.ts");
2246
2249
  if (fs7.existsSync(tsPath)) {
2247
- allowRoot(path8.dirname(tsPath));
2250
+ allowRoot(path9.dirname(tsPath));
2248
2251
  return { path: tsPath };
2249
2252
  }
2250
- const jsPath = path8.resolve(process.cwd(), "packages/core/di/interfaces.js");
2253
+ const jsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.js");
2251
2254
  if (fs7.existsSync(jsPath)) {
2252
- allowRoot(path8.dirname(jsPath));
2255
+ allowRoot(path9.dirname(jsPath));
2253
2256
  return { path: jsPath };
2254
2257
  }
2255
2258
  }
@@ -2263,8 +2266,8 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
2263
2266
  };
2264
2267
  }
2265
2268
  if (args.path.startsWith(".")) {
2266
- const importerDir = args.resolveDir || path8.resolve(resolveDir);
2267
- const candidate = path8.resolve(importerDir, args.path);
2269
+ const importerDir = args.resolveDir || path9.resolve(resolveDir);
2270
+ const candidate = path9.resolve(importerDir, args.path);
2268
2271
  if (!isPermittedPath(candidate)) {
2269
2272
  return {
2270
2273
  errors: [
@@ -2316,8 +2319,8 @@ async function extractZipEntries(zip, destDir, maxTotalBytes) {
2316
2319
  let totalBytes = 0;
2317
2320
  for (const [name, file] of Object.entries(zip.files)) {
2318
2321
  if (file.dir) continue;
2319
- const filePath = path8.join(destDir, name);
2320
- fs7.mkdirSync(path8.dirname(filePath), { recursive: true });
2322
+ const filePath = path9.join(destDir, name);
2323
+ fs7.mkdirSync(path9.dirname(filePath), { recursive: true });
2321
2324
  const content = await file.async("nodebuffer");
2322
2325
  totalBytes += content.length;
2323
2326
  if (totalBytes > maxTotalBytes) {
@@ -2373,11 +2376,11 @@ async function validateAndBundleZip(zipPath) {
2373
2376
  throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
2374
2377
  }
2375
2378
  const entryCode = await entryFile.async("string");
2376
- const tmpDir = path8.join(os2.tmpdir(), `plugin-build-${(0, import_uuid6.v7)()}`);
2379
+ const tmpDir = path9.join(os2.tmpdir(), `plugin-build-${(0, import_uuid6.v7)()}`);
2377
2380
  try {
2378
2381
  fs7.mkdirSync(tmpDir, { recursive: true });
2379
2382
  await extractZipEntries(zip, tmpDir, getMaxUncompressedSize());
2380
- const bundledPath = path8.join(os2.tmpdir(), `plugin-bundle-${(0, import_uuid6.v7)()}.mjs`);
2383
+ const bundledPath = path9.join(os2.tmpdir(), `plugin-bundle-${(0, import_uuid6.v7)()}.mjs`);
2381
2384
  await bundlePlugin(entryCode, tmpDir, bundledPath);
2382
2385
  return { manifest, bundledPath, entryFileName: resolvedMain, package: zip };
2383
2386
  } finally {
@@ -2390,14 +2393,14 @@ async function validateAndBundleZip(zipPath) {
2390
2393
  async function extractManifestFromBundle(_bundledCode) {
2391
2394
  throw new Error("extractManifestFromBundle is not yet implemented");
2392
2395
  }
2393
- var import_jszip, fs7, os2, path8, import_uuid6, PluginCodeSecurityError, LEX_SCAN_LIMIT;
2396
+ var import_jszip, fs7, os2, path9, import_uuid6, PluginCodeSecurityError, LEX_SCAN_LIMIT;
2394
2397
  var init_install_utils = __esm({
2395
2398
  "packages/core/esm-loader/install-utils.ts"() {
2396
2399
  "use strict";
2397
2400
  import_jszip = __toESM(require("jszip"), 1);
2398
2401
  fs7 = __toESM(require("fs"), 1);
2399
2402
  os2 = __toESM(require("os"), 1);
2400
- path8 = __toESM(require("path"), 1);
2403
+ path9 = __toESM(require("path"), 1);
2401
2404
  import_uuid6 = require("uuid");
2402
2405
  init_manifest_schema();
2403
2406
  PluginCodeSecurityError = class extends Error {
@@ -2516,12 +2519,12 @@ function resolveAuditRotationConfig(env = process.env) {
2516
2519
  const maxFiles = Math.floor(readPositiveInt(env.AUDIT_LOG_MAX_FILES, 20, "AUDIT_LOG_MAX_FILES"));
2517
2520
  return { maxBytes: Math.floor(maxSizeMb * 1024 * 1024), maxFiles };
2518
2521
  }
2519
- var import_node_fs5, import_node_path5, import_node_stream, activeStreams, exitHookInstalled, RotatingFileStream;
2522
+ var import_node_fs5, import_node_path6, import_node_stream, activeStreams, exitHookInstalled, RotatingFileStream;
2520
2523
  var init_rotating_file_stream = __esm({
2521
2524
  "packages/core/observability/rotating-file-stream.ts"() {
2522
2525
  "use strict";
2523
2526
  import_node_fs5 = __toESM(require("node:fs"), 1);
2524
- import_node_path5 = __toESM(require("node:path"), 1);
2527
+ import_node_path6 = __toESM(require("node:path"), 1);
2525
2528
  import_node_stream = require("node:stream");
2526
2529
  activeStreams = /* @__PURE__ */ new Set();
2527
2530
  exitHookInstalled = false;
@@ -2535,7 +2538,7 @@ var init_rotating_file_stream = __esm({
2535
2538
  this.filePath = options.filePath;
2536
2539
  this.maxBytes = options.maxBytes;
2537
2540
  this.maxFiles = options.maxFiles;
2538
- this.dir = import_node_path5.default.dirname(this.filePath);
2541
+ this.dir = import_node_path6.default.dirname(this.filePath);
2539
2542
  }
2540
2543
  /** 当前活动文件路径(测试与排障用) */
2541
2544
  get activeFilePath() {
@@ -2640,21 +2643,21 @@ function consoleStream() {
2640
2643
  function createLogger(component) {
2641
2644
  return logger.child({ component });
2642
2645
  }
2643
- var import_pino, import_node_fs6, import_node_path6, defaultLevel, consoleLevel, fileLevel, rootLevel, logDir, logFile, streams, fileLogging, logger;
2646
+ var import_pino, import_node_fs6, import_node_path7, defaultLevel, consoleLevel, fileLevel, rootLevel, logDir, logFile, streams, fileLogging, logger;
2644
2647
  var init_logger = __esm({
2645
2648
  "packages/core/observability/logger.ts"() {
2646
2649
  "use strict";
2647
2650
  import_pino = __toESM(require("pino"), 1);
2648
2651
  import_node_fs6 = __toESM(require("node:fs"), 1);
2649
- import_node_path6 = __toESM(require("node:path"), 1);
2652
+ import_node_path7 = __toESM(require("node:path"), 1);
2650
2653
  init_redaction();
2651
2654
  init_rotating_file_stream();
2652
2655
  defaultLevel = process.env.LOG_LEVEL || (process.env.HTTP_ACCESS_LOG === "all" ? "debug" : process.env.NODE_ENV === "production" ? "info" : "debug");
2653
2656
  consoleLevel = process.env.LOG_CONSOLE_LEVEL || defaultLevel;
2654
2657
  fileLevel = defaultLevel;
2655
2658
  rootLevel = minPinoLevel(consoleLevel, fileLevel);
2656
- logDir = import_node_path6.default.resolve(process.env.LOG_DIR || import_node_path6.default.join(process.cwd(), "logs"));
2657
- logFile = import_node_path6.default.join(logDir, "openlearn.log");
2659
+ logDir = import_node_path7.default.resolve(process.env.LOG_DIR || import_node_path7.default.join(process.cwd(), "logs"));
2660
+ logFile = import_node_path7.default.join(logDir, "openlearn.log");
2658
2661
  streams = [
2659
2662
  { level: consoleLevel, stream: consoleStream() }
2660
2663
  ];
@@ -2787,7 +2790,7 @@ function buildAuditLogger() {
2787
2790
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
2788
2791
  return (0, import_pino2.default)({ level: "silent" }, import_pino2.default.destination(1));
2789
2792
  }
2790
- const logDir2 = import_node_path7.default.resolve(process.env.LOG_DIR || import_node_path7.default.join(process.cwd(), "logs"));
2793
+ const logDir2 = import_node_path8.default.resolve(process.env.LOG_DIR || import_node_path8.default.join(process.cwd(), "logs"));
2791
2794
  try {
2792
2795
  if (!import_node_fs7.default.existsSync(logDir2)) import_node_fs7.default.mkdirSync(logDir2, { recursive: true });
2793
2796
  import_node_fs7.default.accessSync(logDir2, import_node_fs7.default.constants.W_OK);
@@ -2795,7 +2798,7 @@ function buildAuditLogger() {
2795
2798
  streams2.push({
2796
2799
  level: "info",
2797
2800
  stream: new RotatingFileStream({
2798
- filePath: import_node_path7.default.join(logDir2, "audit.log"),
2801
+ filePath: import_node_path8.default.join(logDir2, "audit.log"),
2799
2802
  maxBytes: rotation.maxBytes,
2800
2803
  maxFiles: rotation.maxFiles
2801
2804
  })
@@ -2820,12 +2823,12 @@ function recordSecurityAudit(event, fields = {}) {
2820
2823
  }
2821
2824
  return record;
2822
2825
  }
2823
- var import_node_fs7, import_node_path7, import_pino2, testSink, auditLogger;
2826
+ var import_node_fs7, import_node_path8, import_pino2, testSink, auditLogger;
2824
2827
  var init_audit_log = __esm({
2825
2828
  "packages/core/observability/audit-log.ts"() {
2826
2829
  "use strict";
2827
2830
  import_node_fs7 = __toESM(require("node:fs"), 1);
2828
- import_node_path7 = __toESM(require("node:path"), 1);
2831
+ import_node_path8 = __toESM(require("node:path"), 1);
2829
2832
  import_pino2 = __toESM(require("pino"), 1);
2830
2833
  init_logger();
2831
2834
  init_request_context();
@@ -3875,9 +3878,9 @@ var init_http_router = __esm({
3875
3878
  /**
3876
3879
  * 注册指定动词与路径的 Handler
3877
3880
  */
3878
- route(method, path46, handler, options = {}) {
3881
+ route(method, path47, handler, options = {}) {
3879
3882
  const upperMethod = method.toUpperCase();
3880
- const normalized = path46.startsWith("/") ? path46 : "/" + path46;
3883
+ const normalized = path47.startsWith("/") ? path47 : "/" + path47;
3881
3884
  const { regex, paramNames } = compileRoutePattern(normalized);
3882
3885
  this.warnIfUnscoped(upperMethod, normalized, options);
3883
3886
  this.routes.push({
@@ -3890,20 +3893,20 @@ var init_http_router = __esm({
3890
3893
  scope: options.scope
3891
3894
  });
3892
3895
  }
3893
- get(path46, handler, options) {
3894
- this.route("GET", path46, handler, options);
3896
+ get(path47, handler, options) {
3897
+ this.route("GET", path47, handler, options);
3895
3898
  }
3896
- post(path46, handler, options) {
3897
- this.route("POST", path46, handler, options);
3899
+ post(path47, handler, options) {
3900
+ this.route("POST", path47, handler, options);
3898
3901
  }
3899
- put(path46, handler, options) {
3900
- this.route("PUT", path46, handler, options);
3902
+ put(path47, handler, options) {
3903
+ this.route("PUT", path47, handler, options);
3901
3904
  }
3902
- patch(path46, handler, options) {
3903
- this.route("PATCH", path46, handler, options);
3905
+ patch(path47, handler, options) {
3906
+ this.route("PATCH", path47, handler, options);
3904
3907
  }
3905
- delete(path46, handler, options) {
3906
- this.route("DELETE", path46, handler, options);
3908
+ delete(path47, handler, options) {
3909
+ this.route("DELETE", path47, handler, options);
3907
3910
  }
3908
3911
  warnIfUnscoped(method, pattern, options) {
3909
3912
  if (options.scope) return;
@@ -3918,19 +3921,19 @@ var init_http_router = __esm({
3918
3921
  }
3919
3922
  stream(methodOrPath, pathOrHandler, maybeHandler) {
3920
3923
  if (typeof pathOrHandler === "function") {
3921
- const path46 = methodOrPath;
3924
+ const path47 = methodOrPath;
3922
3925
  const handler = pathOrHandler;
3923
- this.registerStream("GET", path46, handler);
3924
- this.registerStream("POST", path46, handler);
3926
+ this.registerStream("GET", path47, handler);
3927
+ this.registerStream("POST", path47, handler);
3925
3928
  } else {
3926
3929
  const method = methodOrPath.toUpperCase();
3927
- const path46 = pathOrHandler;
3930
+ const path47 = pathOrHandler;
3928
3931
  const handler = maybeHandler;
3929
- this.registerStream(method, path46, handler);
3932
+ this.registerStream(method, path47, handler);
3930
3933
  }
3931
3934
  }
3932
- registerStream(method, path46, handler) {
3933
- const normalized = path46.startsWith("/") ? path46 : "/" + path46;
3935
+ registerStream(method, path47, handler) {
3936
+ const normalized = path47.startsWith("/") ? path47 : "/" + path47;
3934
3937
  const { regex, paramNames } = compileRoutePattern(normalized);
3935
3938
  this.routes.push({
3936
3939
  method: method.toUpperCase(),
@@ -3944,9 +3947,9 @@ var init_http_router = __esm({
3944
3947
  /**
3945
3948
  * 检查指定动词与路径是否为流式路由
3946
3949
  */
3947
- isStream(method, path46) {
3950
+ isStream(method, path47) {
3948
3951
  const upperMethod = method.toUpperCase();
3949
- const normalized = path46.startsWith("/") ? path46 : "/" + path46;
3952
+ const normalized = path47.startsWith("/") ? path47 : "/" + path47;
3950
3953
  for (const entry of this.routes) {
3951
3954
  if (entry.method !== upperMethod) continue;
3952
3955
  if (entry.regex.test(normalized)) {
@@ -3958,9 +3961,9 @@ var init_http_router = __esm({
3958
3961
  /**
3959
3962
  * 匹配路由并提取路由参数
3960
3963
  */
3961
- match(method, path46) {
3964
+ match(method, path47) {
3962
3965
  const upperMethod = method.toUpperCase();
3963
- const normalized = path46.startsWith("/") ? path46 : "/" + path46;
3966
+ const normalized = path47.startsWith("/") ? path47 : "/" + path47;
3964
3967
  for (const entry of this.routes) {
3965
3968
  if (entry.method !== upperMethod) continue;
3966
3969
  const match = normalized.match(entry.regex);
@@ -4103,13 +4106,13 @@ function checkMethodPolicy(input) {
4103
4106
  if (tokens.frontendApi && token === tokens.frontendApi) {
4104
4107
  if (isReadOnlyMethod(method)) return null;
4105
4108
  if (method === "request" && !Array.isArray(args)) return null;
4106
- const path46 = extractPathArg(args);
4107
- const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path46 === p || path46.startsWith(`${p}/`));
4108
- const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path46.endsWith(s));
4109
- const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path46);
4109
+ const path47 = extractPathArg(args);
4110
+ const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path47 === p || path47.startsWith(`${p}/`));
4111
+ const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path47.endsWith(s));
4112
+ const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path47);
4110
4113
  if (isHighRisk || isIrreversible || isEntityDelete) {
4111
4114
  if (hasCapability(caps, CAP_API_ADMIN)) return null;
4112
- const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${path46}'` : isEntityDelete ? `\u5B9E\u4F53\u7EA7\u5220\u9664 '${path46}'` : `\u5199\u5165\u9AD8\u5371\u8DEF\u5F84 '${path46 || "<\u672A\u63D0\u4F9B\u8DEF\u5F84>"}'`;
4115
+ const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${path47}'` : isEntityDelete ? `\u5B9E\u4F53\u7EA7\u5220\u9664 '${path47}'` : `\u5199\u5165\u9AD8\u5371\u8DEF\u5F84 '${path47 || "<\u672A\u63D0\u4F9B\u8DEF\u5F84>"}'`;
4113
4116
  return `${why} \u9700\u8981 manifest \u58F0\u660E '${CAP_API_ADMIN}' \u80FD\u529B`;
4114
4117
  }
4115
4118
  return hasCapability(caps, CAP_API_WRITE) ? null : `\u5199\u64CD\u4F5C\uFF08${method}\uFF09\u9700\u8981 manifest \u58F0\u660E '${CAP_API_WRITE}' \u80FD\u529B\uFF08\u666E\u901A\u53EA\u8BFB\u8C03\u7528\u8BF7\u7528 get\uFF09`;
@@ -4609,6 +4612,53 @@ function wrapPointsDimensionRegistry(registry4, tracker, pluginId) {
4609
4612
  })
4610
4613
  };
4611
4614
  }
4615
+ function wrapClassroomLifecycleService(service, tracker, pluginId) {
4616
+ return {
4617
+ getStage: createSafeFunction((lessonId) => service.getStage(lessonId)),
4618
+ transitionStage: createSafeFunction(
4619
+ (lessonId, toStage, actorId, classId) => service.transitionStage(lessonId, toStage, actorId, classId)
4620
+ ),
4621
+ registerStageGuard: createSafeFunction((owner, guard) => {
4622
+ service.registerStageGuard(owner, guard);
4623
+ tracker.track(pluginId, {
4624
+ dispose: () => {
4625
+ service.unregisterStageGuard(owner);
4626
+ }
4627
+ });
4628
+ }),
4629
+ unregisterStageGuard: createSafeFunction((owner) => {
4630
+ service.unregisterStageGuard(owner);
4631
+ })
4632
+ };
4633
+ }
4634
+ function wrapInteractionRuntimeService(service, tracker, pluginId) {
4635
+ return {
4636
+ registerActivityProvider: createSafeFunction((owner, descriptor) => {
4637
+ service.registerActivityProvider(owner, descriptor);
4638
+ const providerId = descriptor?.id;
4639
+ if (providerId) {
4640
+ tracker.track(pluginId, {
4641
+ dispose: () => {
4642
+ service.unregisterActivityProvider(owner, providerId);
4643
+ }
4644
+ });
4645
+ }
4646
+ }),
4647
+ unregisterActivityProvider: createSafeFunction((owner, id) => {
4648
+ service.unregisterActivityProvider(owner, id);
4649
+ }),
4650
+ listActivityProviders: createSafeFunction(() => service.listActivityProviders())
4651
+ };
4652
+ }
4653
+ function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId) {
4654
+ if (tokenName === IClassroomLifecycleServiceToken.name) {
4655
+ return wrapClassroomLifecycleService(raw, tracker, pluginId);
4656
+ }
4657
+ if (tokenName === IInteractionRuntimeServiceToken.name) {
4658
+ return wrapInteractionRuntimeService(raw, tracker, pluginId);
4659
+ }
4660
+ return raw;
4661
+ }
4612
4662
  function wrapPointsLedgerService(service, pluginId, capabilities) {
4613
4663
  const auditAccess = (studentId, method, operation, requiredCap) => {
4614
4664
  const hasCap = hasCapability(capabilities ?? [], requiredCap);
@@ -4903,9 +4953,9 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4903
4953
  `[PluginHost] Plugin "${manifest.id}" cannot resolve "${token}" by name. Use the Token object imported from @openlearn/plugin-sdk, or one of the SDK-exported Token names.`
4904
4954
  );
4905
4955
  }
4906
- return serviceRegistry.resolveByName(token);
4956
+ return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId));
4907
4957
  }
4908
- return serviceRegistry.resolve(token);
4958
+ return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId));
4909
4959
  },
4910
4960
  // V3.2: 插件向 DI 容器提供服务(对应 manifest.provides)
4911
4961
  provide: async (token, instance) => {
@@ -5740,7 +5790,7 @@ function isPidAlive(pid) {
5740
5790
  }
5741
5791
  }
5742
5792
  function writeBootstrapTempFile(code) {
5743
- const file = import_node_path10.default.join(
5793
+ const file = import_node_path11.default.join(
5744
5794
  import_node_os.default.tmpdir(),
5745
5795
  `${BOOTSTRAP_PREFIX}${process.pid}-${import_node_crypto7.default.randomBytes(8).toString("hex")}${BOOTSTRAP_SUFFIX}`
5746
5796
  );
@@ -5754,7 +5804,7 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
5754
5804
  let cleaned = 0;
5755
5805
  for (const file of files) {
5756
5806
  if (!file.startsWith(BOOTSTRAP_PREFIX) || !file.endsWith(BOOTSTRAP_SUFFIX)) continue;
5757
- const full = import_node_path10.default.join(tmpDir, file);
5807
+ const full = import_node_path11.default.join(tmpDir, file);
5758
5808
  const ownerPid = bootstrapOwnerPid(file);
5759
5809
  if (ownerPid === null) {
5760
5810
  try {
@@ -5779,9 +5829,9 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
5779
5829
  function defaultNetPreloadStartDirs() {
5780
5830
  const dirs = [];
5781
5831
  try {
5782
- const url = import_meta5.url;
5832
+ const url = import_meta6.url;
5783
5833
  if (typeof url === "string" && url.startsWith("file:")) {
5784
- dirs.push(import_node_path10.default.dirname((0, import_node_url3.fileURLToPath)(url)));
5834
+ dirs.push(import_node_path11.default.dirname((0, import_node_url3.fileURLToPath)(url)));
5785
5835
  }
5786
5836
  } catch {
5787
5837
  }
@@ -5793,11 +5843,11 @@ function defaultNetPreloadStartDirs() {
5793
5843
  function resolveNetPreloadPath(options = {}) {
5794
5844
  const cwd = options.cwd ?? process.cwd();
5795
5845
  const candidates = [
5796
- import_node_path10.default.resolve(cwd, "packages/core/worker-runtime/plugin-net-preload.cjs"),
5797
- import_node_path10.default.resolve(cwd, "dist/plugin-net-preload.cjs")
5846
+ import_node_path11.default.resolve(cwd, "packages/core/worker-runtime/plugin-net-preload.cjs"),
5847
+ import_node_path11.default.resolve(cwd, "dist/plugin-net-preload.cjs")
5798
5848
  ];
5799
5849
  for (const dir of options.startDirs ?? defaultNetPreloadStartDirs()) {
5800
- candidates.unshift(import_node_path10.default.join(dir, "plugin-net-preload.cjs"));
5850
+ candidates.unshift(import_node_path11.default.join(dir, "plugin-net-preload.cjs"));
5801
5851
  }
5802
5852
  const found = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
5803
5853
  if (!found) {
@@ -5805,7 +5855,7 @@ function resolveNetPreloadPath(options = {}) {
5805
5855
  }
5806
5856
  return found;
5807
5857
  }
5808
- var import_node_child_process, import_node_crypto7, import_node_fs9, import_node_os, import_node_path10, import_node_url3, import_meta5, ENV_ALLOWLIST, ENV_DENYLIST, WORKER_DATA_ENV, BOOTSTRAP_PREFIX, BOOTSTRAP_SUFFIX, LEGACY_BOOTSTRAP_STALE_MS;
5858
+ var import_node_child_process, import_node_crypto7, import_node_fs9, import_node_os, import_node_path11, import_node_url3, import_meta6, ENV_ALLOWLIST, ENV_DENYLIST, WORKER_DATA_ENV, BOOTSTRAP_PREFIX, BOOTSTRAP_SUFFIX, LEGACY_BOOTSTRAP_STALE_MS;
5809
5859
  var init_child_spawn = __esm({
5810
5860
  "packages/core/worker-runtime/child-spawn.ts"() {
5811
5861
  "use strict";
@@ -5813,13 +5863,13 @@ var init_child_spawn = __esm({
5813
5863
  import_node_crypto7 = __toESM(require("node:crypto"), 1);
5814
5864
  import_node_fs9 = __toESM(require("node:fs"), 1);
5815
5865
  import_node_os = __toESM(require("node:os"), 1);
5816
- import_node_path10 = __toESM(require("node:path"), 1);
5866
+ import_node_path11 = __toESM(require("node:path"), 1);
5817
5867
  import_node_url3 = require("node:url");
5818
5868
  init_child_transport();
5819
5869
  init_child_isolate();
5820
5870
  init_plugin_permission();
5821
5871
  init_plugin_limits();
5822
- import_meta5 = {};
5872
+ import_meta6 = {};
5823
5873
  ENV_ALLOWLIST = [
5824
5874
  "PATH",
5825
5875
  "LANG",
@@ -8923,9 +8973,9 @@ __export(worker_manager_exports, {
8923
8973
  function collectSharedReadPaths(pluginDir) {
8924
8974
  if (!pluginDir) return [];
8925
8975
  const paths = /* @__PURE__ */ new Set();
8926
- const nodeModulesDir = import_node_path11.default.join(import_node_path11.default.dirname(pluginDir), "node_modules");
8976
+ const nodeModulesDir = import_node_path12.default.join(import_node_path12.default.dirname(pluginDir), "node_modules");
8927
8977
  paths.add(nodeModulesDir);
8928
- const sdkLink = import_node_path11.default.join(nodeModulesDir, "@openlearn", "plugin-sdk");
8978
+ const sdkLink = import_node_path12.default.join(nodeModulesDir, "@openlearn", "plugin-sdk");
8929
8979
  try {
8930
8980
  if (import_fs7.default.existsSync(sdkLink)) paths.add(import_fs7.default.realpathSync(sdkLink));
8931
8981
  } catch {
@@ -8964,7 +9014,7 @@ function generateBootstrapCode() {
8964
9014
  const rootPath = process.cwd().replace(/\\/g, "/");
8965
9015
  return composeBootstrapCode(`${rootPath}/package.json`);
8966
9016
  }
8967
- var import_node_worker_threads, import_fs7, import_node_path11, import_uuid12, BASE_WORKER_SERVICE_TOKENS, ALL_SERVICE_TOKENS, POINTS_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
9017
+ var import_node_worker_threads, import_fs7, import_node_path12, import_uuid12, BASE_WORKER_SERVICE_TOKENS, ALL_SERVICE_TOKENS, POINTS_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
8968
9018
  var init_worker_manager = __esm({
8969
9019
  "packages/core/worker-runtime/worker-manager.ts"() {
8970
9020
  "use strict";
@@ -8978,7 +9028,7 @@ var init_worker_manager = __esm({
8978
9028
  init_plugin_limits();
8979
9029
  init_thread_isolate();
8980
9030
  import_fs7 = __toESM(require("fs"), 1);
8981
- import_node_path11 = __toESM(require("node:path"), 1);
9031
+ import_node_path12 = __toESM(require("node:path"), 1);
8982
9032
  init_transport();
8983
9033
  init_plugin_net_proxy();
8984
9034
  init_service_host();
@@ -9383,7 +9433,7 @@ var init_worker_manager = __esm({
9383
9433
  const encodedBootstrap = Buffer.from(bootstrapCode, "utf-8").toString("base64");
9384
9434
  const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
9385
9435
  const allowedTokens = computeAllowedWorkerTokens(manifest, serviceTokens);
9386
- const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(import_node_path11.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
9436
+ const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(import_node_path12.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
9387
9437
  let approvedNetHosts = [];
9388
9438
  try {
9389
9439
  approvedNetHosts = listApprovedNetHosts(this.db, [pluginId, manifest.id]);
@@ -9755,6 +9805,193 @@ __export(server_exports, {
9755
9805
  startServer: () => startServer
9756
9806
  });
9757
9807
  module.exports = __toCommonJS(server_exports);
9808
+
9809
+ // server/security/runtime-check.ts
9810
+ var import_node_path = __toESM(require("node:path"), 1);
9811
+ var import_node_module = require("node:module");
9812
+
9813
+ // scripts/assert-node-runtime.mjs
9814
+ var SUPPORTED_NODE_MAJORS = Object.freeze([20, 22, 24]);
9815
+ var SUPPORTED_NODE_TEXT = "Node.js 20.x / 22.x / 24.x";
9816
+ var PINNED_NODE_MAJOR = 22;
9817
+ function parseNodeMajor(version) {
9818
+ if (typeof version !== "string") return null;
9819
+ const matched = /^v?(\d+)\./.exec(version.trim());
9820
+ if (!matched) return null;
9821
+ const major = Number(matched[1]);
9822
+ return Number.isFinite(major) ? major : null;
9823
+ }
9824
+ function checkNodeVersion(version, supported = SUPPORTED_NODE_MAJORS) {
9825
+ const major = parseNodeMajor(version);
9826
+ if (major === null) {
9827
+ return {
9828
+ ok: false,
9829
+ code: "unparseable",
9830
+ major: null,
9831
+ message: `\u65E0\u6CD5\u89E3\u6790 Node \u7248\u672C\u300C${version}\u300D\u3002`
9832
+ };
9833
+ }
9834
+ if (supported.includes(major)) {
9835
+ return { ok: true, code: "supported", major, message: "" };
9836
+ }
9837
+ return {
9838
+ ok: false,
9839
+ code: "unsupported",
9840
+ major,
9841
+ message: `\u5F53\u524D Node \u7248\u672C v${major}.x \u4E0D\u53D7\u652F\u6301\uFF08\u652F\u6301\u8303\u56F4\uFF1A${SUPPORTED_NODE_TEXT}\uFF09\u3002\u672C\u9879\u76EE\u7684\u539F\u751F\u4F9D\u8D56\uFF08better-sqlite3\uFF09\u6309 Node ABI \u7F16\u8BD1\uFF0C\u590D\u7528\u65E7 node_modules \u4F1A\u5728\u8FD0\u884C\u671F\u62A5 ERR_DLOPEN_FAILED / NODE_MODULE_VERSION \u4E0D\u4E00\u81F4\u3002`
9842
+ };
9843
+ }
9844
+ function buildFixGuidance(major) {
9845
+ const pinned = PINNED_NODE_MAJOR;
9846
+ const lines = [
9847
+ "\u600E\u4E48\u4FEE\uFF08\u4EFB\u9009\u5176\u4E00\uFF09\uFF1A",
9848
+ ` 1) \u5207\u5230\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08\u672C\u9879\u76EE\u5B9E\u6D4B ${SUPPORTED_NODE_MAJORS.join(" / ")}\uFF09\uFF1A`,
9849
+ ` nvm install ${pinned} && nvm use ${pinned}`,
9850
+ ` fnm install ${pinned} && fnm use ${pinned}`,
9851
+ ` asdf install nodejs ${pinned}.16.0 && asdf local nodejs ${pinned}.16.0`,
9852
+ " \u4ED3\u5E93\u6839\u7684 .nvmrc / .node-version \u5DF2\u9489\u4F4F " + pinned + "\uFF0Cnvm / fnm \u4F1A\u81EA\u52A8\u91C7\u7528\u3002",
9853
+ " 2) \u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5185\u542B Node " + pinned + "\uFF0C\u5BBF\u4E3B\u673A\u65E0\u9700\u88C5 Node\uFF09\uFF1A",
9854
+ " docker compose up -d",
9855
+ " 3) \u786E\u5B9E\u8981\u7528\u522B\u7684 Node \u7248\u672C\uFF1A\u5FC5\u987B\u8BA9\u539F\u751F\u6A21\u5757\u6309\u65B0 ABI \u91CD\u65B0\u7F16\u8BD1 \u2014\u2014",
9856
+ " \u4FDD\u7559\u65E7 node_modules \u53EA\u4F1A\u7EE7\u7EED\u590D\u7528 ABI \u4E0D\u5339\u914D\u7684\u4E8C\u8FDB\u5236\uFF1A",
9857
+ " rm -rf node_modules && pnpm install"
9858
+ ];
9859
+ if (major !== null && major === 21) {
9860
+ lines.push(
9861
+ " \u6CE8\uFF1ANode 21 \u4E0D\u662F LTS\uFF0C\u4E14 better-sqlite3 \u5B98\u65B9\u4E0D\u63D0\u4F9B 21.x \u9884\u7F16\u8BD1\u4EA7\u7269\uFF0C",
9862
+ " \u91CD\u88C5\u540E\u4ECD\u9700\u8981\u672C\u5730\u7F16\u8BD1\u5DE5\u5177\u94FE\uFF08python3 + make + g++\uFF09\uFF0C\u4E0D\u5EFA\u8BAE\u7528\u4E8E\u751F\u4EA7\u3002"
9863
+ );
9864
+ }
9865
+ return lines.join("\n");
9866
+ }
9867
+ function formatUnsupportedMessage(version, check = checkNodeVersion(version)) {
9868
+ if (check.ok) return "";
9869
+ const head = `[NodeRuntime] FATAL: ${check.message}`;
9870
+ return `${head}
9871
+
9872
+ ${buildFixGuidance(check.major)}
9873
+ `;
9874
+ }
9875
+ function runCliGuard() {
9876
+ const result = checkNodeVersion(process.version);
9877
+ if (result.ok) return;
9878
+ console.error(formatUnsupportedMessage(process.version, result));
9879
+ process.exit(1);
9880
+ }
9881
+ var invokedDirectly = process.argv[1] && (process.argv[1].endsWith("assert-node-runtime.mjs") || process.argv[1].endsWith("assert-node-runtime") || process.argv[1].includes("assert-node-runtime"));
9882
+ if (invokedDirectly) {
9883
+ runCliGuard();
9884
+ }
9885
+
9886
+ // server/security/runtime-check.ts
9887
+ var import_meta = {};
9888
+ var ABI_PATTERN = /NODE_MODULE_VERSION\s+(\d+)/i;
9889
+ var MISSING_BINDINGS_MARKERS = ["Could not locate the bindings file", "bindings file"];
9890
+ function detectNativeModuleFailure(error) {
9891
+ const runtimeAbi = Number(process.versions.modules);
9892
+ const message = error instanceof Error ? error.message : String(error);
9893
+ const abiMatch = ABI_PATTERN.exec(message);
9894
+ if (abiMatch) {
9895
+ return {
9896
+ kind: "abi-mismatch",
9897
+ // better-sqlite3 的报错里第一个 NODE_MODULE_VERSION 是「模块编译时的 ABI」,
9898
+ // 第二个是「当前运行时要求的 ABI」。
9899
+ moduleAbi: Number(abiMatch[1]),
9900
+ runtimeAbi,
9901
+ detail: message
9902
+ };
9903
+ }
9904
+ if (MISSING_BINDINGS_MARKERS.some((marker) => message.includes(marker))) {
9905
+ return { kind: "missing-bindings", moduleAbi: null, runtimeAbi, detail: message };
9906
+ }
9907
+ return { kind: "unknown", moduleAbi: null, runtimeAbi, detail: message };
9908
+ }
9909
+ function resolveRequire() {
9910
+ if (typeof import_meta !== "undefined" && import_meta.url) {
9911
+ return (0, import_node_module.createRequire)(import_meta.url);
9912
+ }
9913
+ if (typeof __filename !== "undefined") {
9914
+ return (0, import_node_module.createRequire)(__filename);
9915
+ }
9916
+ return (0, import_node_module.createRequire)(import_node_path.default.join(process.cwd(), "package.json"));
9917
+ }
9918
+ function loadBetterSqlite3Binding(loadModule) {
9919
+ const load = loadModule ?? resolveRequire();
9920
+ const Database3 = load("better-sqlite3");
9921
+ const probe = new Database3(":memory:");
9922
+ probe.close();
9923
+ }
9924
+ function checkNativeModule(load = loadBetterSqlite3Binding) {
9925
+ try {
9926
+ load();
9927
+ return null;
9928
+ } catch (error) {
9929
+ const failure = detectNativeModuleFailure(error);
9930
+ if (failure.kind === "abi-mismatch") {
9931
+ return {
9932
+ key: "native-module-abi",
9933
+ severity: "error",
9934
+ message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u4E0E\u5F53\u524D Node \u8FD0\u884C\u65F6\u4E0D\u517C\u5BB9\uFF1A\u4E8C\u8FDB\u5236\u662F\u6309 ABI ${failure.moduleAbi} \u7F16\u8BD1\u7684\uFF0C\u5F53\u524D\u8FD0\u884C\u65F6\uFF08Node ${process.versions.node}\uFF09\u8981\u6C42 ABI ${failure.runtimeAbi}\u3002
9935
+ \u5E38\u89C1\u539F\u56E0\uFF1Anode_modules \u662F\u5728\u53E6\u4E00\u4E2A Node \u7248\u672C\u4E0B\u5B89\u88C5/\u62F7\u8D1D\u7684\u3002
9936
+ \u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08\u6309\u5F53\u524D Node \u91CD\u65B0\u7F16\u8BD1\u539F\u751F\u6A21\u5757\uFF09\uFF0C
9937
+ \u6216\u5207\u56DE\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08${SUPPORTED_NODE_TEXT}\uFF09\u3002`
9938
+ };
9939
+ }
9940
+ if (failure.kind === "missing-bindings") {
9941
+ return {
9942
+ key: "native-module-missing",
9943
+ severity: "error",
9944
+ message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u7684\u4E8C\u8FDB\u5236\u4E0D\u5B58\u5728\uFF08\u672A\u7F16\u8BD1\u6216\u4F9D\u8D56\u88AB\u88C1\u526A\uFF09\u3002
9945
+ \u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08pnpm \u9700\u8981 allowBuilds \u653E\u884C better-sqlite3 \u7684\u6784\u5EFA\u811A\u672C\uFF09\uFF0C
9946
+ \u6216\u6539\u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5DF2\u5185\u7F6E\u7F16\u8BD1\u597D\u7684\u4E8C\u8FDB\u5236\uFF09\u3002`
9947
+ };
9948
+ }
9949
+ return {
9950
+ key: "native-module-load",
9951
+ severity: "error",
9952
+ message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u52A0\u8F7D\u5931\u8D25\uFF1A${failure.detail}`
9953
+ };
9954
+ }
9955
+ }
9956
+ function checkRuntimeEnvironment(input = {}) {
9957
+ const errors = [];
9958
+ const warnings = [];
9959
+ const nodeVersion = input.nodeVersion ?? process.version;
9960
+ const nodeCheck = checkNodeVersion(nodeVersion);
9961
+ if (!nodeCheck.ok) {
9962
+ errors.push({
9963
+ key: "node-version",
9964
+ severity: "error",
9965
+ message: `${nodeCheck.message}
9966
+ ${buildFixGuidance(nodeCheck.major)}`
9967
+ });
9968
+ return { errors, warnings };
9969
+ }
9970
+ const nativeIssue = checkNativeModule(input.loadNativeModule);
9971
+ if (nativeIssue) {
9972
+ errors.push(nativeIssue);
9973
+ }
9974
+ return { errors, warnings };
9975
+ }
9976
+ function assertRuntimeEnvironment() {
9977
+ const { errors, warnings } = checkRuntimeEnvironment();
9978
+ for (const warning of warnings) {
9979
+ console.warn(`[NodeRuntime] WARN: ${warning.message}`);
9980
+ }
9981
+ if (errors.length === 0) return;
9982
+ console.error("[NodeRuntime] FATAL: \u8FD0\u884C\u65F6\u73AF\u5883\u4E0D\u6EE1\u8DB3\u8981\u6C42\uFF0C\u62D2\u7EDD\u542F\u52A8\u3002");
9983
+ for (const issue of errors) {
9984
+ console.error(`
9985
+ [NodeRuntime] ${issue.key}
9986
+ ${issue.message}`);
9987
+ }
9988
+ process.exit(1);
9989
+ }
9990
+
9991
+ // server/security/runtime-preflight.ts
9992
+ assertRuntimeEnvironment();
9993
+
9994
+ // server.ts
9758
9995
  var import_express4 = __toESM(require("express"), 1);
9759
9996
  var import_path23 = __toESM(require("path"), 1);
9760
9997
  var import_dotenv = __toESM(require("dotenv"), 1);
@@ -10786,12 +11023,12 @@ var import_url = require("url");
10786
11023
 
10787
11024
  // packages/core/db/backup-manager.ts
10788
11025
  var import_node_fs2 = __toESM(require("node:fs"), 1);
10789
- var import_node_path2 = __toESM(require("node:path"), 1);
11026
+ var import_node_path3 = __toESM(require("node:path"), 1);
10790
11027
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
10791
11028
 
10792
11029
  // packages/core/db/secure-files.ts
10793
11030
  var import_node_fs = __toESM(require("node:fs"), 1);
10794
- var import_node_path = __toESM(require("node:path"), 1);
11031
+ var import_node_path2 = __toESM(require("node:path"), 1);
10795
11032
  var SECURE_FILE_MODE = 384;
10796
11033
  var SECURE_DIR_MODE = 448;
10797
11034
  var GROUP_OTHER_BITS = 63;
@@ -10835,7 +11072,7 @@ function ensureSecureFile(filePath) {
10835
11072
  hardenFile(filePath);
10836
11073
  return;
10837
11074
  }
10838
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(filePath), { recursive: true });
11075
+ import_node_fs.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true });
10839
11076
  const fd = import_node_fs.default.openSync(filePath, "a", SECURE_FILE_MODE);
10840
11077
  import_node_fs.default.closeSync(fd);
10841
11078
  } catch {
@@ -10853,15 +11090,15 @@ function secureDatabaseFiles(dbPath2) {
10853
11090
  // packages/core/db/backup-manager.ts
10854
11091
  function getDefaultDbPath() {
10855
11092
  if (process.env.OPENLEARN_DB_PATH) {
10856
- return import_node_path2.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
11093
+ return import_node_path3.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
10857
11094
  }
10858
- return import_node_path2.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
11095
+ return import_node_path3.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
10859
11096
  }
10860
11097
  function getDefaultBackupDir() {
10861
11098
  if (process.env.BACKUP_DIR) {
10862
- return import_node_path2.default.resolve(process.cwd(), process.env.BACKUP_DIR);
11099
+ return import_node_path3.default.resolve(process.cwd(), process.env.BACKUP_DIR);
10863
11100
  }
10864
- return import_node_path2.default.resolve(process.cwd(), "backups");
11101
+ return import_node_path3.default.resolve(process.cwd(), "backups");
10865
11102
  }
10866
11103
  function validateBackup(filePath) {
10867
11104
  if (!import_node_fs2.default.existsSync(filePath)) {
@@ -10915,7 +11152,7 @@ function pruneByPrefix(backupDir, prefix, keepCount) {
10915
11152
  let pruned = 0;
10916
11153
  for (const file of files.slice(keepCount)) {
10917
11154
  try {
10918
- import_node_fs2.default.unlinkSync(import_node_path2.default.join(backupDir, file));
11155
+ import_node_fs2.default.unlinkSync(import_node_path3.default.join(backupDir, file));
10919
11156
  pruned++;
10920
11157
  } catch {
10921
11158
  }
@@ -10934,15 +11171,15 @@ function pruneMigratorSnapshots(backupDir = getDefaultBackupDir(), keepCount) {
10934
11171
  return pruneByPrefix(backupDir, "pre_migration_", Math.max(1, keepCount ?? resolvePreMigrationKeep()));
10935
11172
  }
10936
11173
  function performBackup(options = {}) {
10937
- const dbPath2 = options.dbPath ? import_node_path2.default.resolve(options.dbPath) : getDefaultDbPath();
10938
- const backupDir = options.backupDir ? import_node_path2.default.resolve(options.backupDir) : getDefaultBackupDir();
11174
+ const dbPath2 = options.dbPath ? import_node_path3.default.resolve(options.dbPath) : getDefaultDbPath();
11175
+ const backupDir = options.backupDir ? import_node_path3.default.resolve(options.backupDir) : getDefaultBackupDir();
10939
11176
  const keep = typeof options.keep === "number" ? options.keep : 10;
10940
11177
  if (!import_node_fs2.default.existsSync(backupDir)) {
10941
11178
  ensureSecureDir(backupDir);
10942
11179
  }
10943
11180
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10944
11181
  const filename = options.customName || `backup_${timestamp}.db`;
10945
- const backupPath = import_node_path2.default.join(backupDir, filename);
11182
+ const backupPath = import_node_path3.default.join(backupDir, filename);
10946
11183
  if (import_node_fs2.default.existsSync(backupPath)) {
10947
11184
  import_node_fs2.default.unlinkSync(backupPath);
10948
11185
  }
@@ -10979,7 +11216,7 @@ function performBackup(options = {}) {
10979
11216
  }
10980
11217
 
10981
11218
  // packages/core/db/migrator.ts
10982
- var import_meta = {};
11219
+ var import_meta2 = {};
10983
11220
  function splitSqlStatements(sql) {
10984
11221
  return sql.split(";").map((s) => s.trim()).filter((s) => {
10985
11222
  const content = s.replace(/--.*$/gm, "").trim();
@@ -11135,8 +11372,8 @@ function getDefaultMigrationsDir(options = {}) {
11135
11372
  const starts = options.startDirs ? [...options.startDirs] : [];
11136
11373
  if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
11137
11374
  try {
11138
- if (!options.startDirs && typeof import_meta !== "undefined" && import_meta.url) {
11139
- starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta.url)));
11375
+ if (!options.startDirs && typeof import_meta2 !== "undefined" && import_meta2.url) {
11376
+ starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta2.url)));
11140
11377
  }
11141
11378
  } catch {
11142
11379
  }
@@ -11164,7 +11401,7 @@ function getDefaultMigrationsDir(options = {}) {
11164
11401
 
11165
11402
  // packages/core/db/schema-golden.ts
11166
11403
  var import_node_fs3 = __toESM(require("node:fs"), 1);
11167
- var import_node_path3 = __toESM(require("node:path"), 1);
11404
+ var import_node_path4 = __toESM(require("node:path"), 1);
11168
11405
  var SCHEMA_GOLDEN_FILENAME = "schema-golden.json";
11169
11406
  function extractSchemaSnapshot(db2) {
11170
11407
  const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
@@ -11200,7 +11437,7 @@ function formatSchemaGoldenDrifts(drifts) {
11200
11437
  ).join("\n");
11201
11438
  }
11202
11439
  function loadSchemaGolden(migrationsDir) {
11203
- const filePath = import_node_path3.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
11440
+ const filePath = import_node_path4.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
11204
11441
  if (!import_node_fs3.default.existsSync(filePath)) {
11205
11442
  throw new Error(
11206
11443
  `[SchemaGolden] FATAL: ${filePath} \u4E0D\u5B58\u5728\u3002\u91D1\u6837\u4E0E\u8FC1\u79FB\u6587\u4EF6\u540C\u76EE\u5F55\u53D1\u5E03\uFF0C\u7F3A\u5931\u8BF4\u660E\u5B89\u88C5\u5305\u4E0D\u5B8C\u6574\uFF1B\u6E90\u7801\u4ED3\u5E93\u91CC\u8BF7\u8FD0\u884C pnpm db:gen-golden \u751F\u6210\u3002`
@@ -11745,7 +11982,7 @@ function purgeAllExpiredData(db2, options) {
11745
11982
 
11746
11983
  // packages/core/db/index.ts
11747
11984
  init_api_key_crypto();
11748
- var import_meta2 = {};
11985
+ var import_meta3 = {};
11749
11986
  var BCRYPT_ROUNDS = 10;
11750
11987
  function hashPassword(pwd) {
11751
11988
  return import_bcryptjs.default.hashSync(pwd, BCRYPT_ROUNDS);
@@ -11820,8 +12057,8 @@ if (process.env.VITEST) {
11820
12057
  dbPath = import_path2.default.join(__dirname, "educational_os.db");
11821
12058
  } else {
11822
12059
  try {
11823
- if (typeof import_meta2 !== "undefined" && import_meta2.url) {
11824
- const __filename2 = (0, import_url2.fileURLToPath)(import_meta2.url);
12060
+ if (typeof import_meta3 !== "undefined" && import_meta3.url) {
12061
+ const __filename2 = (0, import_url2.fileURLToPath)(import_meta3.url);
11825
12062
  const __dir = import_path2.default.dirname(__filename2);
11826
12063
  dbPath = import_path2.default.join(__dir, "educational_os.db");
11827
12064
  } else {
@@ -14149,10 +14386,10 @@ function parseScoreFields(fields) {
14149
14386
  if (typeof fields !== "string" || !fields.trim()) return [];
14150
14387
  return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
14151
14388
  }
14152
- function getNested(raw, path46) {
14389
+ function getNested(raw, path47) {
14153
14390
  if (raw === null || raw === void 0) return void 0;
14154
- if (!path46) return void 0;
14155
- const parts = String(path46).split(".").map((p) => p.trim()).filter(Boolean);
14391
+ if (!path47) return void 0;
14392
+ const parts = String(path47).split(".").map((p) => p.trim()).filter(Boolean);
14156
14393
  let cursor = raw;
14157
14394
  for (let i = 0; i < parts.length; i += 1) {
14158
14395
  if (cursor === null || cursor === void 0) return void 0;
@@ -18152,7 +18389,7 @@ var import_path11 = __toESM(require("path"), 1);
18152
18389
  // packages/core/plugin-host/base.ts
18153
18390
  var import_path5 = __toESM(require("path"), 1);
18154
18391
  var import_express = __toESM(require("express"), 1);
18155
- var import_node_module = require("node:module");
18392
+ var import_node_module2 = require("node:module");
18156
18393
  var import_node_url = require("node:url");
18157
18394
 
18158
18395
  // packages/core/plugin-host/resource-tracker.ts
@@ -18438,7 +18675,7 @@ var ContributionRegistry = class {
18438
18675
  // packages/core/plugin-host/base.ts
18439
18676
  init_types();
18440
18677
  init_errors2();
18441
- var import_meta3 = {};
18678
+ var import_meta4 = {};
18442
18679
  var VALID_TRANSITIONS = {
18443
18680
  ["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
18444
18681
  ["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
@@ -18473,12 +18710,12 @@ function resolveHostSdkDir() {
18473
18710
  const bases = [];
18474
18711
  if (typeof __dirname !== "undefined") bases.push(__dirname);
18475
18712
  try {
18476
- bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta3.url)));
18713
+ bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta4.url)));
18477
18714
  } catch {
18478
18715
  }
18479
18716
  for (const base of bases) {
18480
18717
  try {
18481
- const req = (0, import_node_module.createRequire)(import_path5.default.join(base, "__openlearn_resolve__.js"));
18718
+ const req = (0, import_node_module2.createRequire)(import_path5.default.join(base, "__openlearn_resolve__.js"));
18482
18719
  return import_path5.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
18483
18720
  } catch {
18484
18721
  }
@@ -18743,10 +18980,10 @@ var import_semver2 = __toESM(require("semver"), 1);
18743
18980
  init_types();
18744
18981
 
18745
18982
  // packages/core/plugin-host/execution-policy.ts
18746
- var import_node_path8 = __toESM(require("node:path"), 1);
18983
+ var import_node_path9 = __toESM(require("node:path"), 1);
18747
18984
  var import_node_url2 = require("node:url");
18748
18985
  init_types();
18749
- var import_meta4 = {};
18986
+ var import_meta5 = {};
18750
18987
  function trustedInlineAllowlist(env = process.env) {
18751
18988
  return new Set(
18752
18989
  (env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
@@ -18754,23 +18991,23 @@ function trustedInlineAllowlist(env = process.env) {
18754
18991
  }
18755
18992
  function builtinPluginsDir() {
18756
18993
  try {
18757
- const url = import_meta4.url;
18994
+ const url = import_meta5.url;
18758
18995
  if (typeof url === "string" && url.startsWith("file:")) {
18759
- return import_node_path8.default.resolve(import_node_path8.default.dirname((0, import_node_url2.fileURLToPath)(url)), "../../plugins");
18996
+ return import_node_path9.default.resolve(import_node_path9.default.dirname((0, import_node_url2.fileURLToPath)(url)), "../../plugins");
18760
18997
  }
18761
18998
  } catch {
18762
18999
  }
18763
19000
  if (typeof __dirname !== "undefined") {
18764
- return import_node_path8.default.resolve(__dirname, "../packages/plugins");
19001
+ return import_node_path9.default.resolve(__dirname, "../packages/plugins");
18765
19002
  }
18766
- return import_node_path8.default.resolve(process.cwd(), "packages/plugins");
19003
+ return import_node_path9.default.resolve(process.cwd(), "packages/plugins");
18767
19004
  }
18768
19005
  function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir()) {
18769
19006
  if (!sourcePath) return false;
18770
- const root = import_node_path8.default.resolve(builtinDir);
18771
- const resolved = import_node_path8.default.resolve(sourcePath);
18772
- const relative2 = import_node_path8.default.relative(root, resolved);
18773
- return relative2 === "" || !relative2.startsWith("..") && !import_node_path8.default.isAbsolute(relative2);
19007
+ const root = import_node_path9.default.resolve(builtinDir);
19008
+ const resolved = import_node_path9.default.resolve(sourcePath);
19009
+ const relative2 = import_node_path9.default.relative(root, resolved);
19010
+ return relative2 === "" || !relative2.startsWith("..") && !import_node_path9.default.isAbsolute(relative2);
18774
19011
  }
18775
19012
  function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
18776
19013
  if (isInsideBuiltinPluginsDir(sourcePath)) return true;
@@ -19733,6 +19970,37 @@ var PluginHostCore = class _PluginHostCore extends PluginHostBase {
19733
19970
  console.warn(`[PluginHost] Failed to revoke stage guards for "${pluginId}":`, e);
19734
19971
  }
19735
19972
  }
19973
+ /**
19974
+ * 回收该插件注册的服务端题型(Issue #320)。
19975
+ *
19976
+ * 题型注册表(`IAssessmentGraderRegistry`)是插件用 `ctx.resolve(token)` 直取的
19977
+ * **裸注册表**,不像 `ctx.services.*` 那样经过 `ResourceTracker` 包装 —— 所以
19978
+ * `disposeAll()` 拿它没办法。不清理的后果已验证为真:
19979
+ *
19980
+ * 1. 停用插件后题型仍留在注册表里 —— 教师继续能出题、继续按旧闭包判分(“幽灵题型”),
19981
+ * `GET /api/assessment-question-types` 也继续把它列给前端;
19982
+ * 2. 重装/升级插件时 `registerQuestionType` 抛 `duplicate question type`,
19983
+ * 表现为「升级后插件激活失败」,必须重启宿主才能恢复。
19984
+ *
19985
+ * 与 `revokePluginStageGuards` 同构:注册方可能传安装行 UUID,也可能传
19986
+ * manifest.id(文档约定的就是后者),两侧都清一遍(幂等)。
19987
+ */
19988
+ async revokePluginQuestionTypes(pluginId, manifestId) {
19989
+ try {
19990
+ const registry4 = await this.serviceRegistry.tryResolve(IAssessmentGraderRegistryToken);
19991
+ if (!registry4 || typeof registry4.clearOwnedQuestionTypes !== "function") return;
19992
+ let removed = 0;
19993
+ for (const owner of /* @__PURE__ */ new Set([pluginId, manifestId])) {
19994
+ if (!owner) continue;
19995
+ removed += registry4.clearOwnedQuestionTypes(owner) ?? 0;
19996
+ }
19997
+ if (removed > 0) {
19998
+ console.log(`[PluginHost] Revoked ${removed} question type(s) for plugin "${pluginId}"`);
19999
+ }
20000
+ } catch (e) {
20001
+ console.warn(`[PluginHost] Failed to revoke question types for "${pluginId}":`, e);
20002
+ }
20003
+ }
19736
20004
  setPluginState(pluginId, state, opts = {}) {
19737
20005
  this.pluginStates.set(pluginId, state);
19738
20006
  if (!opts.persistDb) return;
@@ -19830,7 +20098,7 @@ var PluginHostHttp = class extends PluginHostCore {
19830
20098
  /**
19831
20099
  * 判断指定插件的某个请求路径是否为流式路由 (SSE)
19832
20100
  */
19833
- isStreamRoute(pluginIdOrManifestId, method, path46) {
20101
+ isStreamRoute(pluginIdOrManifestId, method, path47) {
19834
20102
  const upperMethod = method.toUpperCase();
19835
20103
  const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
19836
20104
  const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
@@ -19839,7 +20107,7 @@ var PluginHostHttp = class extends PluginHostCore {
19839
20107
  for (const r of manifest.api.routes) {
19840
20108
  if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
19841
20109
  const compiled = compileRoutePattern(r.path);
19842
- if (compiled.regex.test(path46)) {
20110
+ if (compiled.regex.test(path47)) {
19843
20111
  return true;
19844
20112
  }
19845
20113
  }
@@ -19847,14 +20115,14 @@ var PluginHostHttp = class extends PluginHostCore {
19847
20115
  }
19848
20116
  if (!instance) return false;
19849
20117
  if (instance.context?.http) {
19850
- return instance.context.http.isStream(upperMethod, path46);
20118
+ return instance.context.http.isStream(upperMethod, path47);
19851
20119
  }
19852
20120
  if (instance.workerRef) {
19853
20121
  const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
19854
20122
  for (const r of routes) {
19855
20123
  if (r.method === upperMethod && r.isStream) {
19856
20124
  const compiled = compileRoutePattern(r.pattern);
19857
- if (compiled.regex.test(path46)) {
20125
+ if (compiled.regex.test(path47)) {
19858
20126
  return true;
19859
20127
  }
19860
20128
  }
@@ -19928,7 +20196,7 @@ init_context_builder();
19928
20196
 
19929
20197
  // packages/core/plugin-host/declarative-migrator.ts
19930
20198
  var import_node_fs8 = __toESM(require("node:fs"), 1);
19931
- var import_node_path9 = __toESM(require("node:path"), 1);
20199
+ var import_node_path10 = __toESM(require("node:path"), 1);
19932
20200
  init_errors2();
19933
20201
  init_plugin_tables();
19934
20202
  function ensurePluginDeclaredMigrationsTable(db2) {
@@ -19944,7 +20212,7 @@ function ensurePluginDeclaredMigrationsTable(db2) {
19944
20212
  `);
19945
20213
  }
19946
20214
  function parseMigrationFileName(fileName) {
19947
- const baseName = import_node_path9.default.basename(fileName, ".sql");
20215
+ const baseName = import_node_path10.default.basename(fileName, ".sql");
19948
20216
  const match = baseName.match(/^(\d+)[_-]?(.*)$/);
19949
20217
  if (match) {
19950
20218
  const version = parseInt(match[1], 10);
@@ -19967,7 +20235,7 @@ function loadPluginMigrations(migrationsDir) {
19967
20235
  return a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
19968
20236
  });
19969
20237
  return files.map((file) => {
19970
- const fullPath = import_node_path9.default.join(migrationsDir, file);
20238
+ const fullPath = import_node_path10.default.join(migrationsDir, file);
19971
20239
  const content = import_node_fs8.default.readFileSync(fullPath, "utf8");
19972
20240
  const { version, baseName } = parseMigrationFileName(file);
19973
20241
  const parsed = parseMigrationSql(baseName, content);
@@ -20609,6 +20877,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20609
20877
  this.resourceTracker.disposeAll(pluginId);
20610
20878
  this.pluginInstances.delete(pluginId);
20611
20879
  this.revokePluginContributions(pluginId);
20880
+ await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
20612
20881
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20613
20882
  if (this._hotReloadController) {
20614
20883
  this._hotReloadController.unregisterPlugin(pluginId);
@@ -20627,6 +20896,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20627
20896
  this.resourceTracker.disposeAll(pluginId);
20628
20897
  this.pluginInstances.delete(pluginId);
20629
20898
  this.revokePluginContributions(pluginId);
20899
+ await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
20630
20900
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20631
20901
  if (actorId) {
20632
20902
  await this.revokeCapabilities(actorId);
@@ -20662,6 +20932,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20662
20932
  this.resourceTracker.disposeAll(pluginId);
20663
20933
  this.pluginInstances.delete(pluginId);
20664
20934
  this.revokePluginContributions(pluginId);
20935
+ await this.revokePluginQuestionTypes(pluginId, instance?.manifest?.id ?? pluginId);
20665
20936
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20666
20937
  if (actorId) {
20667
20938
  await this.revokeCapabilities(actorId);
@@ -20725,26 +20996,26 @@ var import_semver3 = __toESM(require("semver"), 1);
20725
20996
 
20726
20997
  // packages/core/version.ts
20727
20998
  var import_node_fs11 = __toESM(require("node:fs"), 1);
20728
- var import_node_path12 = __toESM(require("node:path"), 1);
20999
+ var import_node_path13 = __toESM(require("node:path"), 1);
20729
21000
  var import_node_url5 = require("node:url");
20730
- var import_meta6 = {};
20731
- var FALLBACK_VERSION = "0.7.4";
21001
+ var import_meta7 = {};
21002
+ var FALLBACK_VERSION = "0.7.5";
20732
21003
  function readPlatformVersion() {
20733
21004
  const bases = [];
20734
21005
  if (typeof __dirname !== "undefined") bases.push(__dirname);
20735
21006
  try {
20736
- bases.push(import_node_path12.default.dirname((0, import_node_url5.fileURLToPath)(import_meta6.url)));
21007
+ bases.push(import_node_path13.default.dirname((0, import_node_url5.fileURLToPath)(import_meta7.url)));
20737
21008
  } catch {
20738
21009
  }
20739
21010
  for (const base of bases) {
20740
21011
  let dir = base;
20741
21012
  for (; ; ) {
20742
21013
  try {
20743
- const pkg = JSON.parse(import_node_fs11.default.readFileSync(import_node_path12.default.join(dir, "package.json"), "utf-8"));
21014
+ const pkg = JSON.parse(import_node_fs11.default.readFileSync(import_node_path13.default.join(dir, "package.json"), "utf-8"));
20744
21015
  if (pkg.name === "openlearn-next" && typeof pkg.version === "string") return pkg.version;
20745
21016
  } catch {
20746
21017
  }
20747
- const parent = import_node_path12.default.dirname(dir);
21018
+ const parent = import_node_path13.default.dirname(dir);
20748
21019
  if (parent === dir) break;
20749
21020
  dir = parent;
20750
21021
  }
@@ -21464,7 +21735,7 @@ init_audit_log();
21464
21735
  // packages/core/plugin-host/dependency-install.ts
21465
21736
  var import_node_child_process2 = require("node:child_process");
21466
21737
  var import_node_fs12 = __toESM(require("node:fs"), 1);
21467
- var import_node_path13 = __toESM(require("node:path"), 1);
21738
+ var import_node_path14 = __toESM(require("node:path"), 1);
21468
21739
  var DependencyInstallError = class extends Error {
21469
21740
  constructor(message, pluginId, cause) {
21470
21741
  super(message);
@@ -21499,7 +21770,7 @@ function resolveRegistry() {
21499
21770
  function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
21500
21771
  const names = dependencyNames(dependencies);
21501
21772
  if (names.length === 0) return;
21502
- const lockPath = import_node_path13.default.join(pluginDir, "package-lock.json");
21773
+ const lockPath = import_node_path14.default.join(pluginDir, "package-lock.json");
21503
21774
  if (!import_node_fs12.default.existsSync(lockPath)) {
21504
21775
  throw new DependencyInstallError(
21505
21776
  `\u4F9D\u8D56\u5B89\u88C5\u672A\u4EA7\u51FA package-lock.json \u2014\u2014 \u65E0\u6CD5\u786E\u8BA4\u4F9D\u8D56\u5DF2\u843D\u76D8\u3002pluginId=${pluginId}\uFF0C\u58F0\u660E\u4F9D\u8D56\uFF1A${names.join(", ")}`,
@@ -21581,7 +21852,7 @@ function installPluginDependencies(pluginDir, opts, runNpm = import_node_child_p
21581
21852
  const { pluginId, dependencies, operation = "install" } = opts;
21582
21853
  const depMap = toNpmDependencyMap(dependencies);
21583
21854
  if (Object.keys(depMap).length === 0) return;
21584
- const pkgJsonPath = import_node_path13.default.join(pluginDir, "package.json");
21855
+ const pkgJsonPath = import_node_path14.default.join(pluginDir, "package.json");
21585
21856
  import_node_fs12.default.writeFileSync(
21586
21857
  pkgJsonPath,
21587
21858
  JSON.stringify({ name: pluginId, version: "0.0.0", dependencies: depMap }, null, 2),
@@ -22411,6 +22682,7 @@ var PluginHostUninstall = class extends PluginHostInstall {
22411
22682
  return pluginId;
22412
22683
  }
22413
22684
  })();
22685
+ await this.revokePluginQuestionTypes(pluginId, manifestId);
22414
22686
  if (state === "uninstalled" /* UNINSTALLED */ && !row) {
22415
22687
  this.pluginStates.delete(pluginId);
22416
22688
  return {
@@ -32668,7 +32940,7 @@ ${formatEnvIssues(warnings)}`);
32668
32940
 
32669
32941
  // server/security/file-permissions.ts
32670
32942
  var import_node_fs14 = __toESM(require("node:fs"), 1);
32671
- var import_node_path14 = __toESM(require("node:path"), 1);
32943
+ var import_node_path15 = __toESM(require("node:path"), 1);
32672
32944
  var FILE_PERMISSION_BASELINE_DOC = "\u4FEE\u590D\uFF1Achmod 600 <file>\uFF08\u76EE\u5F55 chmod 700\uFF09\uFF1B\u8BE6\u89C1 docs/administrator-guide/admin-manual.md \u6587\u4EF6\u6743\u9650\u57FA\u7EBF\u4E00\u8282\u3002";
32673
32945
  function modeOf(p) {
32674
32946
  try {
@@ -32678,13 +32950,13 @@ function modeOf(p) {
32678
32950
  }
32679
32951
  }
32680
32952
  function defaultPaths() {
32681
- const dbPath2 = process.env.OPENLEARN_DB_PATH ? import_node_path14.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH) : import_node_path14.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
32682
- const backupDir = process.env.BACKUP_DIR ? import_node_path14.default.resolve(process.cwd(), process.env.BACKUP_DIR) : import_node_path14.default.join(import_node_path14.default.dirname(dbPath2), "backups");
32953
+ const dbPath2 = process.env.OPENLEARN_DB_PATH ? import_node_path15.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH) : import_node_path15.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
32954
+ const backupDir = process.env.BACKUP_DIR ? import_node_path15.default.resolve(process.cwd(), process.env.BACKUP_DIR) : import_node_path15.default.join(import_node_path15.default.dirname(dbPath2), "backups");
32683
32955
  return {
32684
- envPath: import_node_path14.default.resolve(process.cwd(), ".env"),
32956
+ envPath: import_node_path15.default.resolve(process.cwd(), ".env"),
32685
32957
  dbPath: dbPath2,
32686
32958
  backupDir,
32687
- encryptionKeyPath: import_node_path14.default.resolve(process.cwd(), "storage", "encryption-key")
32959
+ encryptionKeyPath: import_node_path15.default.resolve(process.cwd(), "storage", "encryption-key")
32688
32960
  };
32689
32961
  }
32690
32962
  function checkSensitiveFilePermissions(options = {}) {
@@ -32719,7 +32991,7 @@ function checkSensitiveFilePermissions(options = {}) {
32719
32991
  if (scanned >= maxBackupFiles) break;
32720
32992
  if (!entry.endsWith(".db")) continue;
32721
32993
  scanned++;
32722
- checkFile(import_node_path14.default.join(backupDir, entry), "600", "backup-file");
32994
+ checkFile(import_node_path15.default.join(backupDir, entry), "600", "backup-file");
32723
32995
  }
32724
32996
  }
32725
32997
  return issues;
@@ -33620,7 +33892,7 @@ var runOpenAIAgentChat = async (provider, request) => {
33620
33892
 
33621
33893
  // server/backup-scheduler.ts
33622
33894
  var import_node_fs15 = __toESM(require("node:fs"), 1);
33623
- var import_node_path15 = __toESM(require("node:path"), 1);
33895
+ var import_node_path16 = __toESM(require("node:path"), 1);
33624
33896
  var HOUR_MS = 60 * 60 * 1e3;
33625
33897
  var DAY_MS2 = 24 * HOUR_MS;
33626
33898
  var WAL_FULL_THRESHOLD_BYTES = 50 * 1024 * 1024;
@@ -33637,8 +33909,8 @@ function shouldFullCheckpoint(walBytes, threshold = WAL_FULL_THRESHOLD_BYTES) {
33637
33909
  }
33638
33910
  function resolveDbPath(explicit) {
33639
33911
  if (explicit) return explicit;
33640
- if (process.env.OPENLEARN_DB_PATH) return import_node_path15.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
33641
- return import_node_path15.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
33912
+ if (process.env.OPENLEARN_DB_PATH) return import_node_path16.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
33913
+ return import_node_path16.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
33642
33914
  }
33643
33915
  function startBackupScheduler(deps) {
33644
33916
  const noop = { stop: () => {
@@ -33757,9 +34029,9 @@ function isWhitelisted(route, whitelist) {
33757
34029
  (entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
33758
34030
  );
33759
34031
  }
33760
- function normalizePath2(path46) {
33761
- if (!path46) return "/";
33762
- return path46.endsWith("/") && path46.length > 1 ? path46.slice(0, -1) : path46;
34032
+ function normalizePath2(path47) {
34033
+ if (!path47) return "/";
34034
+ return path47.endsWith("/") && path47.length > 1 ? path47.slice(0, -1) : path47;
33763
34035
  }
33764
34036
  function auditRouteScopes(app, whitelist = []) {
33765
34037
  const declared = [];
@@ -33937,11 +34209,11 @@ function enforcePasswordChanged(req, res, next) {
33937
34209
  if (isPasswordChangeGateSkipped()) {
33938
34210
  return next();
33939
34211
  }
33940
- const path46 = req.path || "";
33941
- if (!path46.startsWith("/api/")) {
34212
+ const path47 = req.path || "";
34213
+ if (!path47.startsWith("/api/")) {
33942
34214
  return next();
33943
34215
  }
33944
- if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path46)) {
34216
+ if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path47)) {
33945
34217
  return next();
33946
34218
  }
33947
34219
  const token = getCookieToken(req);
@@ -34016,8 +34288,8 @@ function csrfGuard(req, res, next) {
34016
34288
  next();
34017
34289
  return;
34018
34290
  }
34019
- const path47 = req.path;
34020
- if (EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
34291
+ const path48 = req.path;
34292
+ if (EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
34021
34293
  next();
34022
34294
  return;
34023
34295
  }
@@ -34027,10 +34299,10 @@ function csrfGuard(req, res, next) {
34027
34299
  const origin = req.headers.origin;
34028
34300
  const referer = req.headers.referer;
34029
34301
  const host = req.headers.host;
34030
- const path46 = req.path;
34302
+ const path47 = req.path;
34031
34303
  if (typeof origin === "string" && origin !== "") {
34032
34304
  if (origin === "null") {
34033
- if (!EXEMPT_PREFIXES.some((p) => path46 === p || path46.startsWith(p))) {
34305
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
34034
34306
  res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
34035
34307
  return;
34036
34308
  }
@@ -34038,7 +34310,7 @@ function csrfGuard(req, res, next) {
34038
34310
  try {
34039
34311
  const originHost = new URL(origin).host;
34040
34312
  if (host && originHost.toLowerCase() !== host.toLowerCase()) {
34041
- if (!EXEMPT_PREFIXES.some((p) => path46 === p || path46.startsWith(p))) {
34313
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
34042
34314
  res.status(403).json({
34043
34315
  success: false,
34044
34316
  code: "FORBIDDEN_CROSS_SITE",
@@ -34056,7 +34328,7 @@ function csrfGuard(req, res, next) {
34056
34328
  try {
34057
34329
  const refererHost = new URL(referer).host;
34058
34330
  if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
34059
- if (!EXEMPT_PREFIXES.some((p) => path46 === p || path46.startsWith(p))) {
34331
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
34060
34332
  res.status(403).json({
34061
34333
  success: false,
34062
34334
  code: "FORBIDDEN_CROSS_SITE",
@@ -34109,8 +34381,8 @@ function parseSentryDsn(dsn) {
34109
34381
  const projectId = url.pathname.split("/").filter(Boolean).pop();
34110
34382
  if (!projectId || !/^\d+$/.test(projectId)) return null;
34111
34383
  const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
34112
- const path46 = prefix ? `/${prefix}` : "";
34113
- const envelopeUrl = `${url.protocol}//${url.host}${path46}/api/${projectId}/envelope/`;
34384
+ const path47 = prefix ? `/${prefix}` : "";
34385
+ const envelopeUrl = `${url.protocol}//${url.host}${path47}/api/${projectId}/envelope/`;
34114
34386
  return { envelopeUrl, publicKey };
34115
34387
  } catch {
34116
34388
  return null;
@@ -34551,14 +34823,14 @@ function createSocketIoCors(options) {
34551
34823
 
34552
34824
  // server/routes/health.ts
34553
34825
  var import_node_fs17 = __toESM(require("node:fs"), 1);
34554
- var import_node_path17 = __toESM(require("node:path"), 1);
34826
+ var import_node_path18 = __toESM(require("node:path"), 1);
34555
34827
 
34556
34828
  // packages/core/observability/health.ts
34557
34829
  var import_node_fs16 = __toESM(require("node:fs"), 1);
34558
- var import_node_path16 = __toESM(require("node:path"), 1);
34830
+ var import_node_path17 = __toESM(require("node:path"), 1);
34559
34831
  var import_node_crypto13 = require("node:crypto");
34560
34832
  function probeStorageWritable(dir) {
34561
- const probe = import_node_path16.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
34833
+ const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
34562
34834
  try {
34563
34835
  import_node_fs16.default.mkdirSync(dir, { recursive: true });
34564
34836
  import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
@@ -34575,14 +34847,14 @@ function probeStorageWritable(dir) {
34575
34847
  function listMigrationNames(migrationsDir) {
34576
34848
  try {
34577
34849
  if (!import_node_fs16.default.existsSync(migrationsDir)) return null;
34578
- const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path16.default.basename(file, ".sql"));
34850
+ const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path17.default.basename(file, ".sql"));
34579
34851
  return names.length > 0 ? names : null;
34580
34852
  } catch {
34581
34853
  return null;
34582
34854
  }
34583
34855
  }
34584
34856
  function migrationRecordName(name) {
34585
- return import_node_path16.default.basename(name.replace(/\r/g, "").trim(), ".sql");
34857
+ return import_node_path17.default.basename(name.replace(/\r/g, "").trim(), ".sql");
34586
34858
  }
34587
34859
  function migrationsComplete(expected, applied) {
34588
34860
  if (!expected) return { ok: false, reason: "migrations" };
@@ -34682,7 +34954,7 @@ function newestBackupAgeSeconds(backupDir, now) {
34682
34954
  let newestMs = Number.NEGATIVE_INFINITY;
34683
34955
  for (const name of names) {
34684
34956
  try {
34685
- const stat = import_node_fs17.default.statSync(import_node_path17.default.join(backupDir, name));
34957
+ const stat = import_node_fs17.default.statSync(import_node_path18.default.join(backupDir, name));
34686
34958
  if (!stat.isFile()) continue;
34687
34959
  if (stat.mtimeMs > newestMs) newestMs = stat.mtimeMs;
34688
34960
  } catch {
@@ -34714,7 +34986,7 @@ function defaultCountEvents() {
34714
34986
  function registerHealthRoutes(app, options) {
34715
34987
  const startTime = options?.startTime ?? Date.now();
34716
34988
  const platformVersion = options?.platformVersion ?? PLATFORM_VERSION;
34717
- const migrationsDir = options?.migrationsDir ?? import_node_path17.default.join(process.cwd(), "migrations");
34989
+ const migrationsDir = options?.migrationsDir ?? import_node_path18.default.join(process.cwd(), "migrations");
34718
34990
  const storageDir = options?.storageDir ?? getStorageDir();
34719
34991
  const backupDir = options?.backupDir ?? getBackupsDir();
34720
34992
  const countEvents = options?.countEvents ?? defaultCountEvents;
@@ -34835,7 +35107,7 @@ var import_zod2 = require("zod");
34835
35107
 
34836
35108
  // server/services/course-package-assets.ts
34837
35109
  var import_node_fs18 = __toESM(require("node:fs"), 1);
34838
- var import_node_path18 = __toESM(require("node:path"), 1);
35110
+ var import_node_path19 = __toESM(require("node:path"), 1);
34839
35111
  var import_node_crypto14 = __toESM(require("node:crypto"), 1);
34840
35112
  var import_jszip3 = __toESM(require("jszip"), 1);
34841
35113
  var import_jspdf = require("jspdf");
@@ -35102,7 +35374,7 @@ async function materializePackageAssets(assets) {
35102
35374
  throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
35103
35375
  }
35104
35376
  const uniqueName = `${Date.now()}-${import_node_crypto14.default.randomBytes(4).toString("hex")}${ext}`;
35105
- const filePath = import_node_path18.default.join(uploadsDir, uniqueName);
35377
+ const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
35106
35378
  import_node_fs18.default.writeFileSync(filePath, buffer);
35107
35379
  writtenPaths.push(filePath);
35108
35380
  const slideCount = isEmbeddableAssetType(asset.contentType) ? asset.contentType === "pdf" ? countPdfPages(buffer) : await countPptxSlides(buffer) : 0;
@@ -42066,7 +42338,7 @@ function registerLessonsRoutes(ctx) {
42066
42338
 
42067
42339
  // server/services/course-package-import.ts
42068
42340
  var import_node_fs20 = __toESM(require("node:fs"), 1);
42069
- var import_node_path19 = __toESM(require("node:path"), 1);
42341
+ var import_node_path20 = __toESM(require("node:path"), 1);
42070
42342
  var import_uuid18 = require("uuid");
42071
42343
 
42072
42344
  // server/services/course-package-identity.ts
@@ -42341,8 +42613,8 @@ function findReferencedAsset(rawData, assets) {
42341
42613
  function cleanupMaterializedAssets(materialized) {
42342
42614
  const uploadsDir = getUploadsDir();
42343
42615
  for (const asset of materialized.values()) {
42344
- const fileName = import_node_path19.default.basename(asset.fileUrl);
42345
- const target = import_node_path19.default.join(uploadsDir, fileName);
42616
+ const fileName = import_node_path20.default.basename(asset.fileUrl);
42617
+ const target = import_node_path20.default.join(uploadsDir, fileName);
42346
42618
  try {
42347
42619
  if (import_node_fs20.default.existsSync(target)) {
42348
42620
  import_node_fs20.default.unlinkSync(target);
@@ -46478,6 +46750,85 @@ function registerAssignmentsRoutes(ctx) {
46478
46750
 
46479
46751
  // server/routes/assignment-hub.ts
46480
46752
  var import_express3 = __toESM(require("express"), 1);
46753
+
46754
+ // packages/core/assignment/file-preview-policy.ts
46755
+ function extensionOf(fileName) {
46756
+ if (typeof fileName !== "string") return "";
46757
+ const base = fileName.split(/[\\/]/).pop() || "";
46758
+ const dot = base.lastIndexOf(".");
46759
+ if (dot <= 0 || dot === base.length - 1) return "";
46760
+ return base.slice(dot).toLowerCase();
46761
+ }
46762
+ var PREVIEW_CONTENT_TYPES = Object.freeze({
46763
+ // 文档
46764
+ ".pdf": "application/pdf",
46765
+ ".rtf": "application/rtf",
46766
+ ".odt": "application/vnd.oasis.opendocument.text",
46767
+ ".odp": "application/vnd.oasis.opendocument.presentation",
46768
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
46769
+ ".doc": "application/msword",
46770
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
46771
+ ".ppt": "application/vnd.ms-powerpoint",
46772
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
46773
+ ".xls": "application/vnd.ms-excel",
46774
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
46775
+ // 纯文本 / 代码 / 数据 —— 宿主按文本渲染
46776
+ ".txt": "text/plain",
46777
+ ".md": "text/plain",
46778
+ ".csv": "text/plain",
46779
+ ".json": "text/plain",
46780
+ ".xml": "text/plain",
46781
+ ".html": "text/plain",
46782
+ ".css": "text/plain",
46783
+ ".py": "text/plain",
46784
+ ".js": "text/plain",
46785
+ ".mjs": "text/plain",
46786
+ ".cjs": "text/plain",
46787
+ ".ts": "text/plain",
46788
+ ".tsx": "text/plain",
46789
+ ".jsx": "text/plain",
46790
+ ".java": "text/plain",
46791
+ ".c": "text/plain",
46792
+ ".h": "text/plain",
46793
+ ".cpp": "text/plain",
46794
+ ".cs": "text/plain",
46795
+ ".go": "text/plain",
46796
+ ".rs": "text/plain",
46797
+ ".rb": "text/plain",
46798
+ ".php": "text/plain",
46799
+ ".sql": "text/plain",
46800
+ ".ino": "text/plain",
46801
+ // 图片
46802
+ ".png": "image/png",
46803
+ ".jpg": "image/jpeg",
46804
+ ".jpeg": "image/jpeg",
46805
+ ".gif": "image/gif",
46806
+ ".webp": "image/webp",
46807
+ ".bmp": "image/bmp",
46808
+ ".svg": "image/svg+xml",
46809
+ ".heic": "image/heic",
46810
+ // 压缩包
46811
+ ".zip": "application/zip"
46812
+ });
46813
+ var INLINE_SAFE_CONTENT_TYPES = /* @__PURE__ */ new Set([
46814
+ "application/pdf",
46815
+ "text/plain",
46816
+ "image/png",
46817
+ "image/jpeg",
46818
+ "image/gif",
46819
+ "image/webp",
46820
+ "image/bmp"
46821
+ ]);
46822
+ var TEXT_PREVIEW_MAX_BYTES = 512 * 1024;
46823
+ function resolveContentType(fileName) {
46824
+ const extension = extensionOf(fileName);
46825
+ return PREVIEW_CONTENT_TYPES[extension] ?? "application/octet-stream";
46826
+ }
46827
+ function isInlineSafeContentType(contentType) {
46828
+ return INLINE_SAFE_CONTENT_TYPES.has((contentType || "").toLowerCase());
46829
+ }
46830
+
46831
+ // server/routes/assignment-hub.ts
46481
46832
  function decodeFileName(raw) {
46482
46833
  const text = String(raw ?? "").trim();
46483
46834
  if (!text) return "";
@@ -46657,6 +47008,19 @@ function registerAssignmentHubRoutes(ctx) {
46657
47008
  );
46658
47009
  res.setHeader("X-Content-Type-Options", "nosniff");
46659
47010
  res.setHeader("Cache-Control", "private, no-store");
47011
+ const wantsInline = String(req.query.disposition || "").toLowerCase() === "inline";
47012
+ const contentType = resolveContentType(originalName);
47013
+ if (wantsInline && isInlineSafeContentType(contentType)) {
47014
+ res.setHeader("Content-Type", contentType);
47015
+ res.setHeader(
47016
+ "Content-Security-Policy",
47017
+ contentType === "application/pdf" ? "sandbox allow-scripts" : "sandbox"
47018
+ );
47019
+ const safeName = encodeURIComponent(originalName).replace(/['()]/g, escape);
47020
+ res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${safeName}`);
47021
+ res.sendFile(absPath);
47022
+ return;
47023
+ }
46660
47024
  res.download(absPath, originalName);
46661
47025
  } catch (e) {
46662
47026
  sendHubError(res, e);
@@ -48422,7 +48786,7 @@ async function checkVersion(source, installedVersion) {
48422
48786
  init_audit_log();
48423
48787
 
48424
48788
  // server/routes/plugin-api-gateway.ts
48425
- var import_node_path20 = __toESM(require("node:path"), 1);
48789
+ var import_node_path21 = __toESM(require("node:path"), 1);
48426
48790
  init_http_router();
48427
48791
  init_interfaces();
48428
48792
  var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
@@ -48596,7 +48960,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
48596
48960
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
48597
48961
  return;
48598
48962
  }
48599
- const normalizedSubPath = import_node_path20.default.posix.normalize("/" + rawSubPath);
48963
+ const normalizedSubPath = import_node_path21.default.posix.normalize("/" + rawSubPath);
48600
48964
  if (normalizedSubPath.includes("..")) {
48601
48965
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
48602
48966
  return;
@@ -49034,7 +49398,7 @@ init_fetch_with_retry();
49034
49398
  // server/services/community-registry.ts
49035
49399
  var import_semver7 = __toESM(require("semver"), 1);
49036
49400
  var import_node_fs21 = __toESM(require("node:fs"), 1);
49037
- var import_node_path21 = __toESM(require("node:path"), 1);
49401
+ var import_node_path22 = __toESM(require("node:path"), 1);
49038
49402
  var import_node_os3 = __toESM(require("node:os"), 1);
49039
49403
  var import_node_crypto16 = require("node:crypto");
49040
49404
  var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
@@ -49256,7 +49620,7 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
49256
49620
  if (!response.body) {
49257
49621
  throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
49258
49622
  }
49259
- const zipPath = import_node_path21.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
49623
+ const zipPath = import_node_path22.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
49260
49624
  const out = import_node_fs21.default.createWriteStream(zipPath);
49261
49625
  let received = 0;
49262
49626
  try {