openlearn-next 0.7.3 → 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 (62) 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-5PzyxF6a.js → ActivityWorkspaceWidget-Dq4kjBS2.js} +1 -1
  5. package/dist/assets/{AdminDirectoryView-BCtmQTEK.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-CbmjInp7.js → BatchPickerModal-CoOWloSS.js} +1 -1
  9. package/dist/assets/{ClassesView-DEnMaDOt.js → ClassesView-Q56Kf4eL.js} +5 -5
  10. package/dist/assets/{ClassroomCountdownWidget-CHq4983w.js → ClassroomCountdownWidget-C6luhy8P.js} +1 -1
  11. package/dist/assets/{CloudDriveModal-D2t3Uyzr.js → CloudDriveModal-DLf4fbKR.js} +1 -1
  12. package/dist/assets/ComputerLabView-D9-Tk7yA.js +1 -0
  13. package/dist/assets/{CourseManagement-DOXyJ_j-.js → CourseManagement-DNSfyre-.js} +1 -1
  14. package/dist/assets/{CourseWizardModal-Dbi6Caw2.js → CourseWizardModal-DAueHXcl.js} +1 -1
  15. package/dist/assets/{CoursewareHubPanel-CyxDIJAF.js → CoursewareHubPanel-DLjH-UzF.js} +1 -1
  16. package/dist/assets/{Dashboard-2rLu_ZBy.js → Dashboard-qMVadkd2.js} +2 -2
  17. package/dist/assets/{ExportWeightModal-BrmAh6VX.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-CVBrKDrd.js → HelpView-CTF6B9ml.js} +1 -1
  20. package/dist/assets/{ImportLessonsModal-PhHtX31R.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-Vi6lLz7a.js → InteractiveCoursewareViewer-BfHEDrCf.js} +1 -1
  23. package/dist/assets/InteractiveWhiteboard-KBzmiNBG.js +65 -0
  24. package/dist/assets/{LazyCourseware-KIt0rGtW.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-BRsZ6QEq.js → LiveClassroomView-DezAlve2.js} +7 -7
  28. package/dist/assets/{NotificationDetailModal-tY5rxYCN.js → NotificationDetailModal-DbMMrIHQ.js} +1 -1
  29. package/dist/assets/{PluginView-CedpAW6d.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-bUA002kP.js → QuizGeneratorModal-CTj2moWM.js} +1 -1
  32. package/dist/assets/StudentAssignmentEvalPanel-DrfZT8Lr.js +1 -0
  33. package/dist/assets/{StudentAssignmentView-BhAz6Hfk.js → StudentAssignmentView-D_EhblVQ.js} +1 -1
  34. package/dist/assets/StudentLessonView-BHG50-4M.js +1 -0
  35. package/dist/assets/{StudentView-PHKxIV9T.js → StudentView-DKDhnJQx.js} +2 -2
  36. package/dist/assets/{SystemResourceLibraryModal-DgXYe6_x.js → SystemResourceLibraryModal-C5y8H5Pf.js} +1 -1
  37. package/dist/assets/TeacherView-CWDK3DOe.js +2 -0
  38. package/dist/assets/{TimetableView-D2ozoj_D.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/cli-migrator.cjs +18 -1
  49. package/dist/index.html +3 -3
  50. package/dist/server.cjs +731 -277
  51. package/package.json +7 -4
  52. package/scripts/assert-node-runtime.mjs +165 -0
  53. package/dist/assets/AssessmentCenter-B43ojHG-.js +0 -1
  54. package/dist/assets/ComputerLabView-YtnoT5Rb.js +0 -1
  55. package/dist/assets/InteractiveWhiteboard-lTzeayX5.js +0 -65
  56. package/dist/assets/LazyWhiteboard-CCN3pYcW.js +0 -2
  57. package/dist/assets/LessonEditorView-D_GVhHuM.js +0 -1
  58. package/dist/assets/StudentLessonView-DC1yX4kG.js +0 -1
  59. package/dist/assets/TeacherView-DscJtOJ9.js +0 -2
  60. package/dist/assets/index-2676X8YB.js +0 -131
  61. package/dist/assets/index-C6hlh8pw.css +0 -1
  62. package/dist/assets/pluginParsers-e4PSm2Iy.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_path3.default.join(import_node_path3.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_path3.default.join(import_node_path3.default.dirname(import_node_path3.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_path3.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,11 +1217,11 @@ function readProcessEnvKey(env) {
1214
1217
  return normalizeKeyHex(raw);
1215
1218
  }
1216
1219
  function readDotEnvKey(cwd) {
1217
- const envPath = import_node_path3.default.resolve(cwd, ".env");
1218
- if (!import_node_fs3.default.existsSync(envPath)) return null;
1220
+ const envPath = import_node_path5.default.resolve(cwd, ".env");
1221
+ if (!import_node_fs4.default.existsSync(envPath)) return null;
1219
1222
  let content = "";
1220
1223
  try {
1221
- content = import_node_fs3.default.readFileSync(envPath, "utf8");
1224
+ content = import_node_fs4.default.readFileSync(envPath, "utf8");
1222
1225
  } catch {
1223
1226
  return null;
1224
1227
  }
@@ -1227,11 +1230,11 @@ function readDotEnvKey(cwd) {
1227
1230
  return normalizeKeyHex(match[1] ?? "");
1228
1231
  }
1229
1232
  function keyFilePath(storageDir) {
1230
- return import_node_path3.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 {
1234
- import_node_fs3.default.chmodSync(filePath, 384);
1237
+ import_node_fs4.default.chmodSync(filePath, 384);
1235
1238
  } catch (err) {
1236
1239
  throw new Error(
1237
1240
  `\u65E0\u6CD5\u5C06 ${filePath} \u7684\u6743\u9650\u8BBE\u4E3A 0600\uFF08${err instanceof Error ? err.message : err}\uFF09\u3002\u5DF2\u62D2\u7EDD\u7EE7\u7EED\u4F7F\u7528\u8BE5\u5BC6\u94A5\u6587\u4EF6\u3002`
@@ -1239,8 +1242,8 @@ function chmodKeyFile(filePath) {
1239
1242
  }
1240
1243
  }
1241
1244
  function readKeyFile(filePath) {
1242
- if (!import_node_fs3.default.existsSync(filePath)) return null;
1243
- const normalized = normalizeKeyHex(import_node_fs3.default.readFileSync(filePath, "utf8"));
1245
+ if (!import_node_fs4.default.existsSync(filePath)) return null;
1246
+ const normalized = normalizeKeyHex(import_node_fs4.default.readFileSync(filePath, "utf8"));
1244
1247
  if (!normalized) {
1245
1248
  throw new EncryptionKeyInvalidError(
1246
1249
  `\u5BC6\u94A5\u6587\u4EF6 ${filePath} \u662F\u7A7A\u7684\u3002\u8BF7\u5220\u6389\u5B83\uFF0C\u6216\u5199\u5165\u539F\u6765\u7684 64 \u4F4D hex\u3002\u4E0D\u8981\u8BA9\u7CFB\u7EDF\u53E6\u9020\u4E00\u628A\u94A5\u5319\u3002`
@@ -1253,9 +1256,9 @@ 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_fs3.default.mkdirSync(import_node_path3.default.dirname(filePath), { recursive: true });
1259
+ import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(filePath), { recursive: true });
1257
1260
  try {
1258
- import_node_fs3.default.writeFileSync(filePath, `${generated}
1261
+ import_node_fs4.default.writeFileSync(filePath, `${generated}
1259
1262
  `, { mode: 384, flag: "wx" });
1260
1263
  } catch (err) {
1261
1264
  const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
@@ -1291,7 +1294,7 @@ function resolveEncryptionKey(options = {}) {
1291
1294
  const fromDotEnv = fromProcess ? null : readDotEnvKey(cwd);
1292
1295
  const configured = fromProcess ?? fromDotEnv;
1293
1296
  const configuredSource = fromProcess ? "env" : fromDotEnv ? "env-file" : null;
1294
- const fileKey = import_node_fs3.default.existsSync(filePath) ? readKeyFile(filePath) : null;
1297
+ const fileKey = import_node_fs4.default.existsSync(filePath) ? readKeyFile(filePath) : null;
1295
1298
  if (configured && fileKey && !keysEqual(configured, fileKey)) {
1296
1299
  throw new EncryptionKeyMismatchError(filePath);
1297
1300
  }
@@ -1400,13 +1403,13 @@ function encryptStoredPlaintextApiKeys(db2) {
1400
1403
  }
1401
1404
  return { updated, skipped };
1402
1405
  }
1403
- var import_node_crypto, import_node_fs3, import_node_path3, 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
- import_node_fs3 = __toESM(require("node:fs"), 1);
1409
- import_node_path3 = __toESM(require("node:path"), 1);
1411
+ import_node_fs4 = __toESM(require("node:fs"), 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,49 +1924,49 @@ function getMaxUncompressedSize() {
1921
1924
  }
1922
1925
  function isInsideRoot(root, target) {
1923
1926
  if (!root || !target) return false;
1924
- const rel = path7.relative(path7.resolve(root), path7.resolve(target));
1927
+ const rel = path9.relative(path9.resolve(root), path9.resolve(target));
1925
1928
  if (rel === "") return false;
1926
- if (path7.isAbsolute(rel)) return false;
1927
- return rel !== ".." && !rel.startsWith(".." + path7.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 {
1931
- return fs6.realpathSync(target);
1934
+ return fs7.realpathSync(target);
1932
1935
  } catch {
1933
1936
  return null;
1934
1937
  }
1935
1938
  }
1936
1939
  function realPathOrSelf(target) {
1937
- return realPathOrNull(target) ?? path7.resolve(target);
1940
+ return realPathOrNull(target) ?? path9.resolve(target);
1938
1941
  }
1939
1942
  function getMaxUploadBodyBytes() {
1940
1943
  return getMaxUncompressedSize();
1941
1944
  }
1942
1945
  function readZipBufferFromPath(zipPath) {
1943
- if (!fs6.existsSync(zipPath)) {
1946
+ if (!fs7.existsSync(zipPath)) {
1944
1947
  throw new Error(`[install-utils] ZIP \u6587\u4EF6\u4E0D\u5B58\u5728: ${zipPath}`);
1945
1948
  }
1946
- return fs6.readFileSync(zipPath);
1949
+ return fs7.readFileSync(zipPath);
1947
1950
  }
1948
1951
  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 = path7.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
1953
- fs6.writeFileSync(zipPath, buffer);
1955
+ const zipPath = path9.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
1956
+ fs7.writeFileSync(zipPath, buffer);
1954
1957
  return zipPath;
1955
1958
  }
1956
1959
  function removeTempZip(zipPath) {
1957
1960
  if (!zipPath) return;
1958
1961
  try {
1959
- fs6.rmSync(zipPath, { force: true });
1962
+ fs7.rmSync(zipPath, { force: true });
1960
1963
  } catch {
1961
1964
  }
1962
1965
  }
1963
1966
  function removeTempFile(filePath) {
1964
1967
  if (!filePath) return;
1965
1968
  try {
1966
- fs6.rmSync(filePath, { force: true });
1969
+ fs7.rmSync(filePath, { force: true });
1967
1970
  } catch {
1968
1971
  }
1969
1972
  }
@@ -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 = path7.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 = path7.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 = path7.resolve(process.cwd(), "packages/core/di/interfaces.ts");
2246
- if (fs6.existsSync(tsPath)) {
2247
- allowRoot(path7.dirname(tsPath));
2248
+ const tsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.ts");
2249
+ if (fs7.existsSync(tsPath)) {
2250
+ allowRoot(path9.dirname(tsPath));
2248
2251
  return { path: tsPath };
2249
2252
  }
2250
- const jsPath = path7.resolve(process.cwd(), "packages/core/di/interfaces.js");
2251
- if (fs6.existsSync(jsPath)) {
2252
- allowRoot(path7.dirname(jsPath));
2253
+ const jsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.js");
2254
+ if (fs7.existsSync(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 || path7.resolve(resolveDir);
2267
- const candidate = path7.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: [
@@ -2302,7 +2305,7 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
2302
2305
  ]
2303
2306
  });
2304
2307
  if (outfile !== void 0) {
2305
- assertPluginCodeSafe(fs6.readFileSync(outfile, "utf-8"));
2308
+ assertPluginCodeSafe(fs7.readFileSync(outfile, "utf-8"));
2306
2309
  return void 0;
2307
2310
  }
2308
2311
  const bundled = result.outputFiles?.[0]?.text;
@@ -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 = path7.join(destDir, name);
2320
- fs6.mkdirSync(path7.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) {
@@ -2325,7 +2328,7 @@ async function extractZipEntries(zip, destDir, maxTotalBytes) {
2325
2328
  `ZIP bomb prevention: actual uncompressed size ${totalBytes} bytes exceeds limit of ${maxTotalBytes} bytes`
2326
2329
  );
2327
2330
  }
2328
- fs6.writeFileSync(filePath, content);
2331
+ fs7.writeFileSync(filePath, content);
2329
2332
  }
2330
2333
  }
2331
2334
  async function validateAndBundleZip(zipPath) {
@@ -2373,16 +2376,16 @@ 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 = path7.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
- fs6.mkdirSync(tmpDir, { recursive: true });
2381
+ fs7.mkdirSync(tmpDir, { recursive: true });
2379
2382
  await extractZipEntries(zip, tmpDir, getMaxUncompressedSize());
2380
- const bundledPath = path7.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 {
2384
2387
  try {
2385
- fs6.rmSync(tmpDir, { recursive: true, force: true });
2388
+ fs7.rmSync(tmpDir, { recursive: true, force: true });
2386
2389
  } catch {
2387
2390
  }
2388
2391
  }
@@ -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, fs6, os2, path7, 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
- fs6 = __toESM(require("fs"), 1);
2401
+ fs7 = __toESM(require("fs"), 1);
2399
2402
  os2 = __toESM(require("os"), 1);
2400
- path7 = __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 {
@@ -2472,7 +2475,7 @@ var init_redaction = __esm({
2472
2475
  // packages/core/observability/rotating-file-stream.ts
2473
2476
  function removeIfExists(filePath) {
2474
2477
  try {
2475
- import_node_fs4.default.unlinkSync(filePath);
2478
+ import_node_fs5.default.unlinkSync(filePath);
2476
2479
  } catch {
2477
2480
  }
2478
2481
  }
@@ -2491,7 +2494,7 @@ function unregisterStream(stream) {
2491
2494
  function safeRename(from, to) {
2492
2495
  try {
2493
2496
  removeIfExists(to);
2494
- import_node_fs4.default.renameSync(from, to);
2497
+ import_node_fs5.default.renameSync(from, to);
2495
2498
  return true;
2496
2499
  } catch {
2497
2500
  return false;
@@ -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_fs4, import_node_path4, 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
- import_node_fs4 = __toESM(require("node:fs"), 1);
2524
- import_node_path4 = __toESM(require("node:path"), 1);
2526
+ import_node_fs5 = __toESM(require("node:fs"), 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_path4.default.dirname(this.filePath);
2541
+ this.dir = import_node_path6.default.dirname(this.filePath);
2539
2542
  }
2540
2543
  /** 当前活动文件路径(测试与排障用) */
2541
2544
  get activeFilePath() {
@@ -2556,7 +2559,7 @@ var init_rotating_file_stream = __esm({
2556
2559
  if (this.currentSize > 0 && this.currentSize + buf.length > this.maxBytes) {
2557
2560
  this.rotate();
2558
2561
  }
2559
- const written = import_node_fs4.default.writeSync(this.fd, buf, 0, buf.length, null);
2562
+ const written = import_node_fs5.default.writeSync(this.fd, buf, 0, buf.length, null);
2560
2563
  this.currentSize += written;
2561
2564
  callback();
2562
2565
  } catch (err) {
@@ -2576,7 +2579,7 @@ var init_rotating_file_stream = __esm({
2576
2579
  closeFd() {
2577
2580
  if (this.fd !== null) {
2578
2581
  try {
2579
- import_node_fs4.default.closeSync(this.fd);
2582
+ import_node_fs5.default.closeSync(this.fd);
2580
2583
  } catch {
2581
2584
  }
2582
2585
  this.fd = null;
@@ -2585,11 +2588,11 @@ var init_rotating_file_stream = __esm({
2585
2588
  }
2586
2589
  ensureOpen() {
2587
2590
  if (this.fd !== null) return;
2588
- import_node_fs4.default.mkdirSync(this.dir, { recursive: true });
2589
- this.fd = import_node_fs4.default.openSync(this.filePath, "a");
2591
+ import_node_fs5.default.mkdirSync(this.dir, { recursive: true });
2592
+ this.fd = import_node_fs5.default.openSync(this.filePath, "a");
2590
2593
  registerStream(this);
2591
2594
  try {
2592
- this.currentSize = import_node_fs4.default.statSync(this.filePath).size;
2595
+ this.currentSize = import_node_fs5.default.statSync(this.filePath).size;
2593
2596
  } catch {
2594
2597
  this.currentSize = 0;
2595
2598
  }
@@ -2603,13 +2606,13 @@ var init_rotating_file_stream = __esm({
2603
2606
  this.closeFd();
2604
2607
  for (let i = this.maxFiles - 1; i >= 1; i -= 1) {
2605
2608
  const from = this.historyFilePath(i);
2606
- if (import_node_fs4.default.existsSync(from)) {
2609
+ if (import_node_fs5.default.existsSync(from)) {
2607
2610
  safeRename(from, this.historyFilePath(i + 1));
2608
2611
  }
2609
2612
  }
2610
2613
  safeRename(this.filePath, this.historyFilePath(1));
2611
2614
  this.currentSize = 0;
2612
- this.fd = import_node_fs4.default.openSync(this.filePath, "a");
2615
+ this.fd = import_node_fs5.default.openSync(this.filePath, "a");
2613
2616
  }
2614
2617
  };
2615
2618
  }
@@ -2640,28 +2643,28 @@ function consoleStream() {
2640
2643
  function createLogger(component) {
2641
2644
  return logger.child({ component });
2642
2645
  }
2643
- var import_pino, import_node_fs5, import_node_path5, 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
- import_node_fs5 = __toESM(require("node:fs"), 1);
2649
- import_node_path5 = __toESM(require("node:path"), 1);
2651
+ import_node_fs6 = __toESM(require("node:fs"), 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_path5.default.resolve(process.env.LOG_DIR || import_node_path5.default.join(process.cwd(), "logs"));
2657
- logFile = import_node_path5.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
  ];
2661
2664
  fileLogging = false;
2662
2665
  try {
2663
- if (!import_node_fs5.default.existsSync(logDir)) import_node_fs5.default.mkdirSync(logDir, { recursive: true });
2664
- import_node_fs5.default.accessSync(logDir, import_node_fs5.default.constants.W_OK);
2666
+ if (!import_node_fs6.default.existsSync(logDir)) import_node_fs6.default.mkdirSync(logDir, { recursive: true });
2667
+ import_node_fs6.default.accessSync(logDir, import_node_fs6.default.constants.W_OK);
2665
2668
  fileLogging = true;
2666
2669
  } catch (err) {
2667
2670
  console.warn(`[logger] \u65E0\u6CD5\u5199\u5165\u65E5\u5FD7\u76EE\u5F55 ${logDir}\uFF0C\u4EC5\u8F93\u51FA\u5230 stdout\u3002`, err instanceof Error ? err.message : err);
@@ -2787,15 +2790,15 @@ 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_path6.default.resolve(process.env.LOG_DIR || import_node_path6.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
- if (!import_node_fs6.default.existsSync(logDir2)) import_node_fs6.default.mkdirSync(logDir2, { recursive: true });
2793
- import_node_fs6.default.accessSync(logDir2, import_node_fs6.default.constants.W_OK);
2795
+ if (!import_node_fs7.default.existsSync(logDir2)) import_node_fs7.default.mkdirSync(logDir2, { recursive: true });
2796
+ import_node_fs7.default.accessSync(logDir2, import_node_fs7.default.constants.W_OK);
2794
2797
  const rotation = resolveAuditRotationConfig();
2795
2798
  streams2.push({
2796
2799
  level: "info",
2797
2800
  stream: new RotatingFileStream({
2798
- filePath: import_node_path6.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_fs6, import_node_path6, 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
- import_node_fs6 = __toESM(require("node:fs"), 1);
2828
- import_node_path6 = __toESM(require("node:path"), 1);
2830
+ import_node_fs7 = __toESM(require("node:fs"), 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, path45, handler, options = {}) {
3881
+ route(method, path47, handler, options = {}) {
3879
3882
  const upperMethod = method.toUpperCase();
3880
- const normalized = path45.startsWith("/") ? path45 : "/" + path45;
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(path45, handler, options) {
3894
- this.route("GET", path45, handler, options);
3896
+ get(path47, handler, options) {
3897
+ this.route("GET", path47, handler, options);
3895
3898
  }
3896
- post(path45, handler, options) {
3897
- this.route("POST", path45, handler, options);
3899
+ post(path47, handler, options) {
3900
+ this.route("POST", path47, handler, options);
3898
3901
  }
3899
- put(path45, handler, options) {
3900
- this.route("PUT", path45, handler, options);
3902
+ put(path47, handler, options) {
3903
+ this.route("PUT", path47, handler, options);
3901
3904
  }
3902
- patch(path45, handler, options) {
3903
- this.route("PATCH", path45, handler, options);
3905
+ patch(path47, handler, options) {
3906
+ this.route("PATCH", path47, handler, options);
3904
3907
  }
3905
- delete(path45, handler, options) {
3906
- this.route("DELETE", path45, 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 path45 = methodOrPath;
3924
+ const path47 = methodOrPath;
3922
3925
  const handler = pathOrHandler;
3923
- this.registerStream("GET", path45, handler);
3924
- this.registerStream("POST", path45, handler);
3926
+ this.registerStream("GET", path47, handler);
3927
+ this.registerStream("POST", path47, handler);
3925
3928
  } else {
3926
3929
  const method = methodOrPath.toUpperCase();
3927
- const path45 = pathOrHandler;
3930
+ const path47 = pathOrHandler;
3928
3931
  const handler = maybeHandler;
3929
- this.registerStream(method, path45, handler);
3932
+ this.registerStream(method, path47, handler);
3930
3933
  }
3931
3934
  }
3932
- registerStream(method, path45, handler) {
3933
- const normalized = path45.startsWith("/") ? path45 : "/" + path45;
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, path45) {
3950
+ isStream(method, path47) {
3948
3951
  const upperMethod = method.toUpperCase();
3949
- const normalized = path45.startsWith("/") ? path45 : "/" + path45;
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, path45) {
3964
+ match(method, path47) {
3962
3965
  const upperMethod = method.toUpperCase();
3963
- const normalized = path45.startsWith("/") ? path45 : "/" + path45;
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 path45 = extractPathArg(args);
4107
- const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path45 === p || path45.startsWith(`${p}/`));
4108
- const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path45.endsWith(s));
4109
- const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path45);
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 '${path45}'` : isEntityDelete ? `\u5B9E\u4F53\u7EA7\u5220\u9664 '${path45}'` : `\u5199\u5165\u9AD8\u5371\u8DEF\u5F84 '${path45 || "<\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) => {
@@ -5714,7 +5764,7 @@ function spawnPluginChild(opts) {
5714
5764
  });
5715
5765
  const removeBootstrapFile = () => {
5716
5766
  try {
5717
- import_node_fs8.default.unlinkSync(bootstrapFile);
5767
+ import_node_fs9.default.unlinkSync(bootstrapFile);
5718
5768
  } catch {
5719
5769
  }
5720
5770
  };
@@ -5740,25 +5790,25 @@ function isPidAlive(pid) {
5740
5790
  }
5741
5791
  }
5742
5792
  function writeBootstrapTempFile(code) {
5743
- const file = import_node_path9.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
  );
5747
- import_node_fs8.default.writeFileSync(file, code, { encoding: "utf8", mode: 384 });
5797
+ import_node_fs9.default.writeFileSync(file, code, { encoding: "utf8", mode: 384 });
5748
5798
  return file;
5749
5799
  }
5750
5800
  function cleanupStaleBootstrapFiles(now = Date.now()) {
5751
5801
  try {
5752
5802
  const tmpDir = import_node_os.default.tmpdir();
5753
- const files = import_node_fs8.default.readdirSync(tmpDir);
5803
+ const files = import_node_fs9.default.readdirSync(tmpDir);
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_path9.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 {
5761
- if (now - import_node_fs8.default.statSync(full).mtimeMs < LEGACY_BOOTSTRAP_STALE_MS) continue;
5811
+ if (now - import_node_fs9.default.statSync(full).mtimeMs < LEGACY_BOOTSTRAP_STALE_MS) continue;
5762
5812
  } catch {
5763
5813
  continue;
5764
5814
  }
@@ -5766,7 +5816,7 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
5766
5816
  continue;
5767
5817
  }
5768
5818
  try {
5769
- import_node_fs8.default.unlinkSync(full);
5819
+ import_node_fs9.default.unlinkSync(full);
5770
5820
  cleaned++;
5771
5821
  } catch {
5772
5822
  }
@@ -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_path9.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,33 +5843,33 @@ function defaultNetPreloadStartDirs() {
5793
5843
  function resolveNetPreloadPath(options = {}) {
5794
5844
  const cwd = options.cwd ?? process.cwd();
5795
5845
  const candidates = [
5796
- import_node_path9.default.resolve(cwd, "packages/core/worker-runtime/plugin-net-preload.cjs"),
5797
- import_node_path9.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_path9.default.join(dir, "plugin-net-preload.cjs"));
5850
+ candidates.unshift(import_node_path11.default.join(dir, "plugin-net-preload.cjs"));
5801
5851
  }
5802
- const found = candidates.find((candidate) => import_node_fs8.default.existsSync(candidate));
5852
+ const found = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
5803
5853
  if (!found) {
5804
5854
  throw new Error(`[PluginNet] preload script not found. Looked in: ${candidates.join(", ")}`);
5805
5855
  }
5806
5856
  return found;
5807
5857
  }
5808
- var import_node_child_process, import_node_crypto7, import_node_fs8, import_node_os, import_node_path9, 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";
5812
5862
  import_node_child_process = require("node:child_process");
5813
5863
  import_node_crypto7 = __toESM(require("node:crypto"), 1);
5814
- import_node_fs8 = __toESM(require("node:fs"), 1);
5864
+ import_node_fs9 = __toESM(require("node:fs"), 1);
5815
5865
  import_node_os = __toESM(require("node:os"), 1);
5816
- import_node_path9 = __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",
@@ -5869,12 +5919,12 @@ function quotaExceeded(input) {
5869
5919
  }
5870
5920
  function readProcSample(pid) {
5871
5921
  try {
5872
- const stat = import_node_fs9.default.readFileSync(`/proc/${pid}/stat`, "utf8");
5922
+ const stat = import_node_fs10.default.readFileSync(`/proc/${pid}/stat`, "utf8");
5873
5923
  const rest = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
5874
5924
  const utime = Number(rest[11]);
5875
5925
  const stime = Number(rest[12]);
5876
5926
  if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
5877
- const status = import_node_fs9.default.readFileSync(`/proc/${pid}/status`, "utf8");
5927
+ const status = import_node_fs10.default.readFileSync(`/proc/${pid}/status`, "utf8");
5878
5928
  const rss = /^VmRSS:\s+(\d+)\s+kB/m.exec(status);
5879
5929
  return { cpuMs: (utime + stime) / 100 * 1e3, rssKb: rss ? Number(rss[1]) : 0 };
5880
5930
  } catch {
@@ -5917,11 +5967,11 @@ function startProcessQuota(pid, onBreach, env = process.env) {
5917
5967
  }
5918
5968
  };
5919
5969
  }
5920
- var import_node_fs9, DEFAULT_MAX_RSS_MB, DEFAULT_CPU_QUOTA_MS, DEFAULT_CPU_WINDOW_MS;
5970
+ var import_node_fs10, DEFAULT_MAX_RSS_MB, DEFAULT_CPU_QUOTA_MS, DEFAULT_CPU_WINDOW_MS;
5921
5971
  var init_plugin_quota = __esm({
5922
5972
  "packages/core/worker-runtime/plugin-quota.ts"() {
5923
5973
  "use strict";
5924
- import_node_fs9 = __toESM(require("node:fs"), 1);
5974
+ import_node_fs10 = __toESM(require("node:fs"), 1);
5925
5975
  DEFAULT_MAX_RSS_MB = 512;
5926
5976
  DEFAULT_CPU_QUOTA_MS = 8e3;
5927
5977
  DEFAULT_CPU_WINDOW_MS = 1e4;
@@ -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_path10.default.join(import_node_path10.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_path10.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_path10, 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_path10 = __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_path10.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();
@@ -11027,7 +11264,7 @@ function simpleChecksum(text) {
11027
11264
  }
11028
11265
  return hash.toString(16);
11029
11266
  }
11030
- function runMigrations(db2, migrations) {
11267
+ function runMigrations(db2, migrations, options = {}) {
11031
11268
  db2.exec(`
11032
11269
  CREATE TABLE IF NOT EXISTS _migrations (
11033
11270
  name TEXT PRIMARY KEY,
@@ -11038,6 +11275,23 @@ function runMigrations(db2, migrations) {
11038
11275
  const appliedRows = db2.prepare("SELECT name, checksum FROM _migrations").all();
11039
11276
  const applied = new Map(appliedRows.map((r) => [r.name, r.checksum]));
11040
11277
  const byName = new Map(migrations.map((m) => [m.name, m]));
11278
+ const unknown = [...applied.keys()].filter((name) => !byName.has(name));
11279
+ if (unknown.length > 0) {
11280
+ const detail = unknown.map((n) => ` - ${n}`).join("\n");
11281
+ const msg = `[Migration] \u53F0\u8D26\u91CC\u6709 ${unknown.length} \u4E2A\u672C\u6B21\u8FC1\u79FB\u5217\u8868\u672A\u5305\u542B\u7684\u6761\u76EE\uFF1A
11282
+ ${detail}
11283
+ ` + (options.strictLedger ? ` \u542F\u52A8\u94FE\u8DEF\u5224\u5B9A\u4E3A FATAL\uFF1A
11284
+ \u6210\u56E0\u4E00\uFF1A\u7528\u66F4\u65B0\u7248\u672C\u7684\u5907\u4EFD\u6062\u590D\u4E86\u65E7\u4EE3\u7801\uFF08\u4EE3\u7801\u964D\u7EA7\uFF09\u2014\u2014\u8BF7\u5148\u5347\u7EA7\u4EE3\u7801\u518D\u542F\u52A8\uFF1B
11285
+ \u6210\u56E0\u4E8C\uFF1A\u5DF2\u5E94\u7528\u7684\u8FC1\u79FB\u6587\u4EF6\u88AB\u6539\u540D\u6216\u5220\u9664\u2014\u2014\u8FC1\u79FB\u4E00\u65E6\u5E94\u7528\u4E0D\u53EF\u6539\u540D\uFF0C\u8BF7\u6062\u590D\u6587\u4EF6\u6216\u8865\u4E00\u6761\u65B0\u8FC1\u79FB\u3002
11286
+ \u786E\u5C5E\u6709\u610F\u7684\u964D\u7EA7\u8FD0\u7EF4\uFF1A\u8BBE\u7F6E MIGRATION_ALLOW_UNKNOWN_LEDGER=true \u653E\u884C\uFF08\u4E0D\u63A8\u8350\uFF09\u3002` : ` \uFF08\u975E\u4E25\u683C\u53E3\u5F84\uFF1A\u8FC1\u79FB\u5B50\u96C6\u8C03\u7528\u5C5E\u5408\u6CD5\u7528\u6CD5\uFF0C\u4EC5\u63D0\u793A\uFF09`);
11287
+ if (process.env.MIGRATION_ALLOW_UNKNOWN_LEDGER === "true") {
11288
+ console.warn("[Migration] WARNING: \u672A\u77E5\u53F0\u8D26\u6761\u76EE\u5DF2\u6309 MIGRATION_ALLOW_UNKNOWN_LEDGER \u653E\u884C\uFF1A\n" + msg);
11289
+ } else if (options.strictLedger) {
11290
+ throw new Error(msg);
11291
+ } else {
11292
+ console.warn("[Migration] " + msg);
11293
+ }
11294
+ }
11041
11295
  const drift = [];
11042
11296
  for (const [name, stored] of applied) {
11043
11297
  const migration = byName.get(name);
@@ -11118,8 +11372,8 @@ function getDefaultMigrationsDir(options = {}) {
11118
11372
  const starts = options.startDirs ? [...options.startDirs] : [];
11119
11373
  if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
11120
11374
  try {
11121
- if (!options.startDirs && typeof import_meta !== "undefined" && import_meta.url) {
11122
- 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)));
11123
11377
  }
11124
11378
  } catch {
11125
11379
  }
@@ -11145,6 +11399,68 @@ function getDefaultMigrationsDir(options = {}) {
11145
11399
  return import_path.default.resolve(cwd, "migrations");
11146
11400
  }
11147
11401
 
11402
+ // packages/core/db/schema-golden.ts
11403
+ var import_node_fs3 = __toESM(require("node:fs"), 1);
11404
+ var import_node_path4 = __toESM(require("node:path"), 1);
11405
+ var SCHEMA_GOLDEN_FILENAME = "schema-golden.json";
11406
+ function extractSchemaSnapshot(db2) {
11407
+ const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
11408
+ const snapshot = {};
11409
+ for (const { name } of tables) {
11410
+ const columns = db2.prepare(`PRAGMA table_info(${JSON.stringify(name)})`).all().map(
11411
+ (c) => c.name
11412
+ );
11413
+ snapshot[name] = columns.sort();
11414
+ }
11415
+ return snapshot;
11416
+ }
11417
+ function verifySchemaAgainstGolden(db2, golden) {
11418
+ const actual = extractSchemaSnapshot(db2);
11419
+ const drifts = [];
11420
+ for (const [table, expectedColumns] of Object.entries(golden.tables)) {
11421
+ const actualColumns = actual[table];
11422
+ if (!actualColumns) {
11423
+ drifts.push({ table, missingColumns: expectedColumns, tableMissing: true });
11424
+ continue;
11425
+ }
11426
+ const actualSet = new Set(actualColumns);
11427
+ const missing = expectedColumns.filter((c) => !actualSet.has(c));
11428
+ if (missing.length > 0) {
11429
+ drifts.push({ table, missingColumns: missing, tableMissing: false });
11430
+ }
11431
+ }
11432
+ return drifts;
11433
+ }
11434
+ function formatSchemaGoldenDrifts(drifts) {
11435
+ return drifts.map(
11436
+ (d) => d.tableMissing ? ` - \u8868 ${d.table} \u6574\u8868\u7F3A\u5931\uFF08\u671F\u671B\u5217\uFF1A${d.missingColumns.join(", ")}\uFF09` : ` - \u8868 ${d.table} \u7F3A\u5217\uFF1A${d.missingColumns.join(", ")}`
11437
+ ).join("\n");
11438
+ }
11439
+ function loadSchemaGolden(migrationsDir) {
11440
+ const filePath = import_node_path4.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
11441
+ if (!import_node_fs3.default.existsSync(filePath)) {
11442
+ throw new Error(
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`
11444
+ );
11445
+ }
11446
+ try {
11447
+ return JSON.parse(import_node_fs3.default.readFileSync(filePath, "utf8"));
11448
+ } catch (err) {
11449
+ throw new Error(`[SchemaGolden] FATAL: \u91D1\u6837\u6587\u4EF6\u89E3\u6790\u5931\u8D25\uFF08${filePath}\uFF09\uFF1A${String(err)}`, { cause: err });
11450
+ }
11451
+ }
11452
+ function assertSchemaMatchesGolden(db2, migrationsDir) {
11453
+ const golden = loadSchemaGolden(migrationsDir);
11454
+ const drifts = verifySchemaAgainstGolden(db2, golden);
11455
+ if (drifts.length > 0) {
11456
+ throw new Error(
11457
+ `[SchemaGolden] FATAL: \u6570\u636E\u5E93\u7ED3\u6784\u4F4E\u4E8E\u4EE3\u7801\u671F\u671B\uFF08${drifts.length} \u5904\u7F3A\u5931\uFF09\u2014\u2014\u8FC1\u79FB\u53F0\u8D26\u867D\u5DF2\u8FFD\u5E73\uFF0C\u4F46\u5E93\u88AB\u5916\u90E8\u4FEE\u6539\u8FC7\u6216\u4ECE\u65E7\u5907\u4EFD\u505A\u4E86\u90E8\u5206\u6062\u590D\u3002\u7F3A\u5931\u660E\u7EC6\uFF1A
11458
+ ${formatSchemaGoldenDrifts(drifts)}
11459
+ \u6062\u590D\u624B\u6BB5\uFF1A\u7528 backups/ \u91CC\u6700\u65B0\u7684 pre_migration_*.db \u5FEB\u7167\u6062\u590D\uFF0C\u6216\u6838\u5BF9\u6700\u8FD1\u4E00\u6B21\u6062\u590D\u64CD\u4F5C\u7684\u6765\u6E90\u3002`
11460
+ );
11461
+ }
11462
+ }
11463
+
11148
11464
  // packages/core/db/class-passcode-crypto.ts
11149
11465
  var import_node_crypto2 = __toESM(require("node:crypto"), 1);
11150
11466
  init_api_key_crypto();
@@ -11666,7 +11982,7 @@ function purgeAllExpiredData(db2, options) {
11666
11982
 
11667
11983
  // packages/core/db/index.ts
11668
11984
  init_api_key_crypto();
11669
- var import_meta2 = {};
11985
+ var import_meta3 = {};
11670
11986
  var BCRYPT_ROUNDS = 10;
11671
11987
  function hashPassword(pwd) {
11672
11988
  return import_bcryptjs.default.hashSync(pwd, BCRYPT_ROUNDS);
@@ -11741,8 +12057,8 @@ if (process.env.VITEST) {
11741
12057
  dbPath = import_path2.default.join(__dirname, "educational_os.db");
11742
12058
  } else {
11743
12059
  try {
11744
- if (typeof import_meta2 !== "undefined" && import_meta2.url) {
11745
- 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);
11746
12062
  const __dir = import_path2.default.dirname(__filename2);
11747
12063
  dbPath = import_path2.default.join(__dir, "educational_os.db");
11748
12064
  } else {
@@ -11802,7 +12118,16 @@ function initializeDatabase(instance) {
11802
12118
  if (migrations.length === 0) {
11803
12119
  throw new Error(`[DB SSOT] FATAL: \u8FC1\u79FB\u76EE\u5F55 ${migrationsDir} \u91CC\u6CA1\u6709 .sql \u6587\u4EF6\u3002\u62D2\u7EDD\u5728\u6CA1\u6709 schema \u7684\u5E93\u4E0A\u542F\u52A8\u3002`);
11804
12120
  }
11805
- runMigrations(instance, migrations);
12121
+ runMigrations(instance, migrations, { strictLedger: true });
12122
+ if (process.env.VITEST) {
12123
+ try {
12124
+ assertSchemaMatchesGolden(instance, migrationsDir);
12125
+ } catch (err) {
12126
+ console.warn("[SchemaGolden] \u6D4B\u8BD5\u73AF\u5883\u7ED3\u6784\u7EC8\u68C0\u672A\u901A\u8FC7\uFF08\u5FFD\u7565\uFF09\uFF1A", err instanceof Error ? err.message : err);
12127
+ }
12128
+ } else {
12129
+ assertSchemaMatchesGolden(instance, migrationsDir);
12130
+ }
11806
12131
  applyStartupDataMigrations(instance);
11807
12132
  const apiKeyMigration = encryptStoredPlaintextApiKeys(instance);
11808
12133
  if (apiKeyMigration.updated > 0) {
@@ -14061,10 +14386,10 @@ function parseScoreFields(fields) {
14061
14386
  if (typeof fields !== "string" || !fields.trim()) return [];
14062
14387
  return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
14063
14388
  }
14064
- function getNested(raw, path45) {
14389
+ function getNested(raw, path47) {
14065
14390
  if (raw === null || raw === void 0) return void 0;
14066
- if (!path45) return void 0;
14067
- const parts = String(path45).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);
14068
14393
  let cursor = raw;
14069
14394
  for (let i = 0; i < parts.length; i += 1) {
14070
14395
  if (cursor === null || cursor === void 0) return void 0;
@@ -18064,7 +18389,7 @@ var import_path11 = __toESM(require("path"), 1);
18064
18389
  // packages/core/plugin-host/base.ts
18065
18390
  var import_path5 = __toESM(require("path"), 1);
18066
18391
  var import_express = __toESM(require("express"), 1);
18067
- var import_node_module = require("node:module");
18392
+ var import_node_module2 = require("node:module");
18068
18393
  var import_node_url = require("node:url");
18069
18394
 
18070
18395
  // packages/core/plugin-host/resource-tracker.ts
@@ -18350,7 +18675,7 @@ var ContributionRegistry = class {
18350
18675
  // packages/core/plugin-host/base.ts
18351
18676
  init_types();
18352
18677
  init_errors2();
18353
- var import_meta3 = {};
18678
+ var import_meta4 = {};
18354
18679
  var VALID_TRANSITIONS = {
18355
18680
  ["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
18356
18681
  ["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
@@ -18385,12 +18710,12 @@ function resolveHostSdkDir() {
18385
18710
  const bases = [];
18386
18711
  if (typeof __dirname !== "undefined") bases.push(__dirname);
18387
18712
  try {
18388
- 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)));
18389
18714
  } catch {
18390
18715
  }
18391
18716
  for (const base of bases) {
18392
18717
  try {
18393
- 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"));
18394
18719
  return import_path5.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
18395
18720
  } catch {
18396
18721
  }
@@ -18655,10 +18980,10 @@ var import_semver2 = __toESM(require("semver"), 1);
18655
18980
  init_types();
18656
18981
 
18657
18982
  // packages/core/plugin-host/execution-policy.ts
18658
- var import_node_path7 = __toESM(require("node:path"), 1);
18983
+ var import_node_path9 = __toESM(require("node:path"), 1);
18659
18984
  var import_node_url2 = require("node:url");
18660
18985
  init_types();
18661
- var import_meta4 = {};
18986
+ var import_meta5 = {};
18662
18987
  function trustedInlineAllowlist(env = process.env) {
18663
18988
  return new Set(
18664
18989
  (env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
@@ -18666,23 +18991,23 @@ function trustedInlineAllowlist(env = process.env) {
18666
18991
  }
18667
18992
  function builtinPluginsDir() {
18668
18993
  try {
18669
- const url = import_meta4.url;
18994
+ const url = import_meta5.url;
18670
18995
  if (typeof url === "string" && url.startsWith("file:")) {
18671
- return import_node_path7.default.resolve(import_node_path7.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");
18672
18997
  }
18673
18998
  } catch {
18674
18999
  }
18675
19000
  if (typeof __dirname !== "undefined") {
18676
- return import_node_path7.default.resolve(__dirname, "../packages/plugins");
19001
+ return import_node_path9.default.resolve(__dirname, "../packages/plugins");
18677
19002
  }
18678
- return import_node_path7.default.resolve(process.cwd(), "packages/plugins");
19003
+ return import_node_path9.default.resolve(process.cwd(), "packages/plugins");
18679
19004
  }
18680
19005
  function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir()) {
18681
19006
  if (!sourcePath) return false;
18682
- const root = import_node_path7.default.resolve(builtinDir);
18683
- const resolved = import_node_path7.default.resolve(sourcePath);
18684
- const relative2 = import_node_path7.default.relative(root, resolved);
18685
- return relative2 === "" || !relative2.startsWith("..") && !import_node_path7.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);
18686
19011
  }
18687
19012
  function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
18688
19013
  if (isInsideBuiltinPluginsDir(sourcePath)) return true;
@@ -19645,6 +19970,37 @@ var PluginHostCore = class _PluginHostCore extends PluginHostBase {
19645
19970
  console.warn(`[PluginHost] Failed to revoke stage guards for "${pluginId}":`, e);
19646
19971
  }
19647
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
+ }
19648
20004
  setPluginState(pluginId, state, opts = {}) {
19649
20005
  this.pluginStates.set(pluginId, state);
19650
20006
  if (!opts.persistDb) return;
@@ -19742,7 +20098,7 @@ var PluginHostHttp = class extends PluginHostCore {
19742
20098
  /**
19743
20099
  * 判断指定插件的某个请求路径是否为流式路由 (SSE)
19744
20100
  */
19745
- isStreamRoute(pluginIdOrManifestId, method, path45) {
20101
+ isStreamRoute(pluginIdOrManifestId, method, path47) {
19746
20102
  const upperMethod = method.toUpperCase();
19747
20103
  const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
19748
20104
  const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
@@ -19751,7 +20107,7 @@ var PluginHostHttp = class extends PluginHostCore {
19751
20107
  for (const r of manifest.api.routes) {
19752
20108
  if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
19753
20109
  const compiled = compileRoutePattern(r.path);
19754
- if (compiled.regex.test(path45)) {
20110
+ if (compiled.regex.test(path47)) {
19755
20111
  return true;
19756
20112
  }
19757
20113
  }
@@ -19759,14 +20115,14 @@ var PluginHostHttp = class extends PluginHostCore {
19759
20115
  }
19760
20116
  if (!instance) return false;
19761
20117
  if (instance.context?.http) {
19762
- return instance.context.http.isStream(upperMethod, path45);
20118
+ return instance.context.http.isStream(upperMethod, path47);
19763
20119
  }
19764
20120
  if (instance.workerRef) {
19765
20121
  const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
19766
20122
  for (const r of routes) {
19767
20123
  if (r.method === upperMethod && r.isStream) {
19768
20124
  const compiled = compileRoutePattern(r.pattern);
19769
- if (compiled.regex.test(path45)) {
20125
+ if (compiled.regex.test(path47)) {
19770
20126
  return true;
19771
20127
  }
19772
20128
  }
@@ -19839,8 +20195,8 @@ init_manifest_schema();
19839
20195
  init_context_builder();
19840
20196
 
19841
20197
  // packages/core/plugin-host/declarative-migrator.ts
19842
- var import_node_fs7 = __toESM(require("node:fs"), 1);
19843
- var import_node_path8 = __toESM(require("node:path"), 1);
20198
+ var import_node_fs8 = __toESM(require("node:fs"), 1);
20199
+ var import_node_path10 = __toESM(require("node:path"), 1);
19844
20200
  init_errors2();
19845
20201
  init_plugin_tables();
19846
20202
  function ensurePluginDeclaredMigrationsTable(db2) {
@@ -19856,7 +20212,7 @@ function ensurePluginDeclaredMigrationsTable(db2) {
19856
20212
  `);
19857
20213
  }
19858
20214
  function parseMigrationFileName(fileName) {
19859
- const baseName = import_node_path8.default.basename(fileName, ".sql");
20215
+ const baseName = import_node_path10.default.basename(fileName, ".sql");
19860
20216
  const match = baseName.match(/^(\d+)[_-]?(.*)$/);
19861
20217
  if (match) {
19862
20218
  const version = parseInt(match[1], 10);
@@ -19865,22 +20221,22 @@ function parseMigrationFileName(fileName) {
19865
20221
  return { version: 0, baseName };
19866
20222
  }
19867
20223
  function loadPluginMigrations(migrationsDir) {
19868
- if (!import_node_fs7.default.existsSync(migrationsDir)) {
20224
+ if (!import_node_fs8.default.existsSync(migrationsDir)) {
19869
20225
  return [];
19870
20226
  }
19871
- const stat = import_node_fs7.default.statSync(migrationsDir);
20227
+ const stat = import_node_fs8.default.statSync(migrationsDir);
19872
20228
  if (!stat.isDirectory()) {
19873
20229
  return [];
19874
20230
  }
19875
- const files = import_node_fs7.default.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort((a, b) => {
20231
+ const files = import_node_fs8.default.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort((a, b) => {
19876
20232
  const vA = parseMigrationFileName(a).version;
19877
20233
  const vB = parseMigrationFileName(b).version;
19878
20234
  if (vA !== vB) return vA - vB;
19879
20235
  return a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
19880
20236
  });
19881
20237
  return files.map((file) => {
19882
- const fullPath = import_node_path8.default.join(migrationsDir, file);
19883
- const content = import_node_fs7.default.readFileSync(fullPath, "utf8");
20238
+ const fullPath = import_node_path10.default.join(migrationsDir, file);
20239
+ const content = import_node_fs8.default.readFileSync(fullPath, "utf8");
19884
20240
  const { version, baseName } = parseMigrationFileName(file);
19885
20241
  const parsed = parseMigrationSql(baseName, content);
19886
20242
  return {
@@ -20521,6 +20877,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20521
20877
  this.resourceTracker.disposeAll(pluginId);
20522
20878
  this.pluginInstances.delete(pluginId);
20523
20879
  this.revokePluginContributions(pluginId);
20880
+ await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
20524
20881
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20525
20882
  if (this._hotReloadController) {
20526
20883
  this._hotReloadController.unregisterPlugin(pluginId);
@@ -20539,6 +20896,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20539
20896
  this.resourceTracker.disposeAll(pluginId);
20540
20897
  this.pluginInstances.delete(pluginId);
20541
20898
  this.revokePluginContributions(pluginId);
20899
+ await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
20542
20900
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20543
20901
  if (actorId) {
20544
20902
  await this.revokeCapabilities(actorId);
@@ -20574,6 +20932,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20574
20932
  this.resourceTracker.disposeAll(pluginId);
20575
20933
  this.pluginInstances.delete(pluginId);
20576
20934
  this.revokePluginContributions(pluginId);
20935
+ await this.revokePluginQuestionTypes(pluginId, instance?.manifest?.id ?? pluginId);
20577
20936
  this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
20578
20937
  if (actorId) {
20579
20938
  await this.revokeCapabilities(actorId);
@@ -20636,27 +20995,27 @@ var import_path8 = __toESM(require("path"), 1);
20636
20995
  var import_semver3 = __toESM(require("semver"), 1);
20637
20996
 
20638
20997
  // packages/core/version.ts
20639
- var import_node_fs10 = __toESM(require("node:fs"), 1);
20640
- var import_node_path11 = __toESM(require("node:path"), 1);
20998
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
20999
+ var import_node_path13 = __toESM(require("node:path"), 1);
20641
21000
  var import_node_url5 = require("node:url");
20642
- var import_meta6 = {};
20643
- var FALLBACK_VERSION = "0.7.2";
21001
+ var import_meta7 = {};
21002
+ var FALLBACK_VERSION = "0.7.5";
20644
21003
  function readPlatformVersion() {
20645
21004
  const bases = [];
20646
21005
  if (typeof __dirname !== "undefined") bases.push(__dirname);
20647
21006
  try {
20648
- bases.push(import_node_path11.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)));
20649
21008
  } catch {
20650
21009
  }
20651
21010
  for (const base of bases) {
20652
21011
  let dir = base;
20653
21012
  for (; ; ) {
20654
21013
  try {
20655
- const pkg = JSON.parse(import_node_fs10.default.readFileSync(import_node_path11.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"));
20656
21015
  if (pkg.name === "openlearn-next" && typeof pkg.version === "string") return pkg.version;
20657
21016
  } catch {
20658
21017
  }
20659
- const parent = import_node_path11.default.dirname(dir);
21018
+ const parent = import_node_path13.default.dirname(dir);
20660
21019
  if (parent === dir) break;
20661
21020
  dir = parent;
20662
21021
  }
@@ -21375,8 +21734,8 @@ init_audit_log();
21375
21734
 
21376
21735
  // packages/core/plugin-host/dependency-install.ts
21377
21736
  var import_node_child_process2 = require("node:child_process");
21378
- var import_node_fs11 = __toESM(require("node:fs"), 1);
21379
- var import_node_path12 = __toESM(require("node:path"), 1);
21737
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
21738
+ var import_node_path14 = __toESM(require("node:path"), 1);
21380
21739
  var DependencyInstallError = class extends Error {
21381
21740
  constructor(message, pluginId, cause) {
21382
21741
  super(message);
@@ -21411,8 +21770,8 @@ function resolveRegistry() {
21411
21770
  function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
21412
21771
  const names = dependencyNames(dependencies);
21413
21772
  if (names.length === 0) return;
21414
- const lockPath = import_node_path12.default.join(pluginDir, "package-lock.json");
21415
- if (!import_node_fs11.default.existsSync(lockPath)) {
21773
+ const lockPath = import_node_path14.default.join(pluginDir, "package-lock.json");
21774
+ if (!import_node_fs12.default.existsSync(lockPath)) {
21416
21775
  throw new DependencyInstallError(
21417
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(", ")}`,
21418
21777
  pluginId
@@ -21420,7 +21779,7 @@ function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
21420
21779
  }
21421
21780
  let lock;
21422
21781
  try {
21423
- lock = JSON.parse(import_node_fs11.default.readFileSync(lockPath, "utf-8"));
21782
+ lock = JSON.parse(import_node_fs12.default.readFileSync(lockPath, "utf-8"));
21424
21783
  } catch (err) {
21425
21784
  throw new DependencyInstallError(
21426
21785
  `package-lock.json \u4E0D\u662F\u5408\u6CD5 JSON \u2014\u2014 \u4F9D\u8D56\u5B89\u88C5\u7ED3\u679C\u4E0D\u53EF\u4FE1\u3002pluginId=${pluginId}`,
@@ -21493,8 +21852,8 @@ function installPluginDependencies(pluginDir, opts, runNpm = import_node_child_p
21493
21852
  const { pluginId, dependencies, operation = "install" } = opts;
21494
21853
  const depMap = toNpmDependencyMap(dependencies);
21495
21854
  if (Object.keys(depMap).length === 0) return;
21496
- const pkgJsonPath = import_node_path12.default.join(pluginDir, "package.json");
21497
- import_node_fs11.default.writeFileSync(
21855
+ const pkgJsonPath = import_node_path14.default.join(pluginDir, "package.json");
21856
+ import_node_fs12.default.writeFileSync(
21498
21857
  pkgJsonPath,
21499
21858
  JSON.stringify({ name: pluginId, version: "0.0.0", dependencies: depMap }, null, 2),
21500
21859
  "utf-8"
@@ -22323,6 +22682,7 @@ var PluginHostUninstall = class extends PluginHostInstall {
22323
22682
  return pluginId;
22324
22683
  }
22325
22684
  })();
22685
+ await this.revokePluginQuestionTypes(pluginId, manifestId);
22326
22686
  if (state === "uninstalled" /* UNINSTALLED */ && !row) {
22327
22687
  this.pluginStates.delete(pluginId);
22328
22688
  return {
@@ -22763,20 +23123,20 @@ init_install_utils();
22763
23123
 
22764
23124
  // packages/core/security/plugin-signature.ts
22765
23125
  var import_node_crypto9 = __toESM(require("node:crypto"), 1);
22766
- var import_node_fs12 = __toESM(require("node:fs"), 1);
23126
+ var import_node_fs13 = __toESM(require("node:fs"), 1);
22767
23127
  function computeSha256(input) {
22768
23128
  const hash = import_node_crypto9.default.createHash("sha256");
22769
23129
  if (typeof input === "string") {
22770
- if (import_node_fs12.default.existsSync(input)) {
22771
- const fd = import_node_fs12.default.openSync(input, "r");
23130
+ if (import_node_fs13.default.existsSync(input)) {
23131
+ const fd = import_node_fs13.default.openSync(input, "r");
22772
23132
  const buf = Buffer.alloc(64 * 1024);
22773
23133
  let bytesRead = 0;
22774
23134
  try {
22775
- while ((bytesRead = import_node_fs12.default.readSync(fd, buf, 0, buf.length, null)) > 0) {
23135
+ while ((bytesRead = import_node_fs13.default.readSync(fd, buf, 0, buf.length, null)) > 0) {
22776
23136
  hash.update(buf.subarray(0, bytesRead));
22777
23137
  }
22778
23138
  } finally {
22779
- import_node_fs12.default.closeSync(fd);
23139
+ import_node_fs13.default.closeSync(fd);
22780
23140
  }
22781
23141
  const digest4 = hash.digest();
22782
23142
  return { hex: digest4.toString("hex"), base64: digest4.toString("base64") };
@@ -22845,8 +23205,8 @@ async function extractEmbeddedSignature(zipPathOrBuffer) {
22845
23205
  const JSZip5 = (await import("jszip")).default;
22846
23206
  let buffer;
22847
23207
  if (typeof zipPathOrBuffer === "string") {
22848
- if (!import_node_fs12.default.existsSync(zipPathOrBuffer)) return null;
22849
- buffer = import_node_fs12.default.readFileSync(zipPathOrBuffer);
23208
+ if (!import_node_fs13.default.existsSync(zipPathOrBuffer)) return null;
23209
+ buffer = import_node_fs13.default.readFileSync(zipPathOrBuffer);
22850
23210
  } else {
22851
23211
  buffer = zipPathOrBuffer;
22852
23212
  }
@@ -22891,9 +23251,9 @@ function resolveTrustedPublicKeys(explicitKeys, env = process.env) {
22891
23251
  for (const part of remaining.split(/[,;\n]/)) {
22892
23252
  const p = part.trim();
22893
23253
  if (!p) continue;
22894
- if (import_node_fs12.default.existsSync(p)) {
23254
+ if (import_node_fs13.default.existsSync(p)) {
22895
23255
  try {
22896
- const fileContent = import_node_fs12.default.readFileSync(p, "utf8").trim();
23256
+ const fileContent = import_node_fs13.default.readFileSync(p, "utf8").trim();
22897
23257
  const filePems = fileContent.match(/-----BEGIN [A-Z0-9 ]+-----[\s\S]+?-----END [A-Z0-9 ]+-----/g);
22898
23258
  if (filePems) {
22899
23259
  for (const fpem of filePems) {
@@ -22946,7 +23306,7 @@ async function verifyPluginPackage(options) {
22946
23306
  const JSZip5 = (await import("jszip")).default;
22947
23307
  let buffer;
22948
23308
  if (typeof packageSource === "string") {
22949
- buffer = import_node_fs12.default.readFileSync(packageSource);
23309
+ buffer = import_node_fs13.default.readFileSync(packageSource);
22950
23310
  } else {
22951
23311
  buffer = packageSource;
22952
23312
  }
@@ -23152,11 +23512,11 @@ var PluginDistributionManager = class {
23152
23512
  return;
23153
23513
  }
23154
23514
  const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
23155
- const fs39 = await import("node:fs");
23156
- if (!fs39.existsSync(filePath)) {
23515
+ const fs40 = await import("node:fs");
23516
+ if (!fs40.existsSync(filePath)) {
23157
23517
  throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
23158
23518
  }
23159
- const code = fs39.readFileSync(filePath, "utf-8");
23519
+ const code = fs40.readFileSync(filePath, "utf-8");
23160
23520
  await this.pluginHost.reloadPlugin(pluginId, code);
23161
23521
  }
23162
23522
  async updateFromZip(zipPath, options = {}) {
@@ -23345,8 +23705,8 @@ var HotReloadController = class {
23345
23705
  const timer = setTimeout(async () => {
23346
23706
  this.debounceTimers.delete(pluginId);
23347
23707
  try {
23348
- const fs39 = await import("fs/promises");
23349
- const newSourceCode = await fs39.readFile(filePath, "utf-8");
23708
+ const fs40 = await import("fs/promises");
23709
+ const newSourceCode = await fs40.readFile(filePath, "utf-8");
23350
23710
  console.log(`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`);
23351
23711
  await this.pluginHost.reloadPlugin(pluginId, newSourceCode);
23352
23712
  } catch (err) {
@@ -32579,24 +32939,24 @@ ${formatEnvIssues(warnings)}`);
32579
32939
  }
32580
32940
 
32581
32941
  // server/security/file-permissions.ts
32582
- var import_node_fs13 = __toESM(require("node:fs"), 1);
32583
- var import_node_path13 = __toESM(require("node:path"), 1);
32942
+ var import_node_fs14 = __toESM(require("node:fs"), 1);
32943
+ var import_node_path15 = __toESM(require("node:path"), 1);
32584
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";
32585
32945
  function modeOf(p) {
32586
32946
  try {
32587
- return import_node_fs13.default.statSync(p).mode & 511;
32947
+ return import_node_fs14.default.statSync(p).mode & 511;
32588
32948
  } catch {
32589
32949
  return null;
32590
32950
  }
32591
32951
  }
32592
32952
  function defaultPaths() {
32593
- const dbPath2 = process.env.OPENLEARN_DB_PATH ? import_node_path13.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH) : import_node_path13.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
32594
- const backupDir = process.env.BACKUP_DIR ? import_node_path13.default.resolve(process.cwd(), process.env.BACKUP_DIR) : import_node_path13.default.join(import_node_path13.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");
32595
32955
  return {
32596
- envPath: import_node_path13.default.resolve(process.cwd(), ".env"),
32956
+ envPath: import_node_path15.default.resolve(process.cwd(), ".env"),
32597
32957
  dbPath: dbPath2,
32598
32958
  backupDir,
32599
- encryptionKeyPath: import_node_path13.default.resolve(process.cwd(), "storage", "encryption-key")
32959
+ encryptionKeyPath: import_node_path15.default.resolve(process.cwd(), "storage", "encryption-key")
32600
32960
  };
32601
32961
  }
32602
32962
  function checkSensitiveFilePermissions(options = {}) {
@@ -32627,11 +32987,11 @@ function checkSensitiveFilePermissions(options = {}) {
32627
32987
  }
32628
32988
  if (dirMode !== null) {
32629
32989
  let scanned = 0;
32630
- for (const entry of import_node_fs13.default.readdirSync(backupDir)) {
32990
+ for (const entry of import_node_fs14.default.readdirSync(backupDir)) {
32631
32991
  if (scanned >= maxBackupFiles) break;
32632
32992
  if (!entry.endsWith(".db")) continue;
32633
32993
  scanned++;
32634
- checkFile(import_node_path13.default.join(backupDir, entry), "600", "backup-file");
32994
+ checkFile(import_node_path15.default.join(backupDir, entry), "600", "backup-file");
32635
32995
  }
32636
32996
  }
32637
32997
  return issues;
@@ -33531,8 +33891,8 @@ var runOpenAIAgentChat = async (provider, request) => {
33531
33891
  };
33532
33892
 
33533
33893
  // server/backup-scheduler.ts
33534
- var import_node_fs14 = __toESM(require("node:fs"), 1);
33535
- var import_node_path14 = __toESM(require("node:path"), 1);
33894
+ var import_node_fs15 = __toESM(require("node:fs"), 1);
33895
+ var import_node_path16 = __toESM(require("node:path"), 1);
33536
33896
  var HOUR_MS = 60 * 60 * 1e3;
33537
33897
  var DAY_MS2 = 24 * HOUR_MS;
33538
33898
  var WAL_FULL_THRESHOLD_BYTES = 50 * 1024 * 1024;
@@ -33549,8 +33909,8 @@ function shouldFullCheckpoint(walBytes, threshold = WAL_FULL_THRESHOLD_BYTES) {
33549
33909
  }
33550
33910
  function resolveDbPath(explicit) {
33551
33911
  if (explicit) return explicit;
33552
- if (process.env.OPENLEARN_DB_PATH) return import_node_path14.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
33553
- return import_node_path14.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");
33554
33914
  }
33555
33915
  function startBackupScheduler(deps) {
33556
33916
  const noop = { stop: () => {
@@ -33578,7 +33938,7 @@ function startBackupScheduler(deps) {
33578
33938
  try {
33579
33939
  let walBytes = 0;
33580
33940
  try {
33581
- walBytes = import_node_fs14.default.statSync(dbPath2 + "-wal").size;
33941
+ walBytes = import_node_fs15.default.statSync(dbPath2 + "-wal").size;
33582
33942
  } catch {
33583
33943
  walBytes = 0;
33584
33944
  }
@@ -33669,9 +34029,9 @@ function isWhitelisted(route, whitelist) {
33669
34029
  (entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
33670
34030
  );
33671
34031
  }
33672
- function normalizePath2(path45) {
33673
- if (!path45) return "/";
33674
- return path45.endsWith("/") && path45.length > 1 ? path45.slice(0, -1) : path45;
34032
+ function normalizePath2(path47) {
34033
+ if (!path47) return "/";
34034
+ return path47.endsWith("/") && path47.length > 1 ? path47.slice(0, -1) : path47;
33675
34035
  }
33676
34036
  function auditRouteScopes(app, whitelist = []) {
33677
34037
  const declared = [];
@@ -33849,11 +34209,11 @@ function enforcePasswordChanged(req, res, next) {
33849
34209
  if (isPasswordChangeGateSkipped()) {
33850
34210
  return next();
33851
34211
  }
33852
- const path45 = req.path || "";
33853
- if (!path45.startsWith("/api/")) {
34212
+ const path47 = req.path || "";
34213
+ if (!path47.startsWith("/api/")) {
33854
34214
  return next();
33855
34215
  }
33856
- if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path45)) {
34216
+ if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path47)) {
33857
34217
  return next();
33858
34218
  }
33859
34219
  const token = getCookieToken(req);
@@ -33928,8 +34288,8 @@ function csrfGuard(req, res, next) {
33928
34288
  next();
33929
34289
  return;
33930
34290
  }
33931
- const path46 = req.path;
33932
- if (EXEMPT_PREFIXES.some((p) => path46 === p || path46.startsWith(p))) {
34291
+ const path48 = req.path;
34292
+ if (EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
33933
34293
  next();
33934
34294
  return;
33935
34295
  }
@@ -33939,10 +34299,10 @@ function csrfGuard(req, res, next) {
33939
34299
  const origin = req.headers.origin;
33940
34300
  const referer = req.headers.referer;
33941
34301
  const host = req.headers.host;
33942
- const path45 = req.path;
34302
+ const path47 = req.path;
33943
34303
  if (typeof origin === "string" && origin !== "") {
33944
34304
  if (origin === "null") {
33945
- if (!EXEMPT_PREFIXES.some((p) => path45 === p || path45.startsWith(p))) {
34305
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
33946
34306
  res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
33947
34307
  return;
33948
34308
  }
@@ -33950,7 +34310,7 @@ function csrfGuard(req, res, next) {
33950
34310
  try {
33951
34311
  const originHost = new URL(origin).host;
33952
34312
  if (host && originHost.toLowerCase() !== host.toLowerCase()) {
33953
- if (!EXEMPT_PREFIXES.some((p) => path45 === p || path45.startsWith(p))) {
34313
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
33954
34314
  res.status(403).json({
33955
34315
  success: false,
33956
34316
  code: "FORBIDDEN_CROSS_SITE",
@@ -33968,7 +34328,7 @@ function csrfGuard(req, res, next) {
33968
34328
  try {
33969
34329
  const refererHost = new URL(referer).host;
33970
34330
  if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
33971
- if (!EXEMPT_PREFIXES.some((p) => path45 === p || path45.startsWith(p))) {
34331
+ if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
33972
34332
  res.status(403).json({
33973
34333
  success: false,
33974
34334
  code: "FORBIDDEN_CROSS_SITE",
@@ -34021,8 +34381,8 @@ function parseSentryDsn(dsn) {
34021
34381
  const projectId = url.pathname.split("/").filter(Boolean).pop();
34022
34382
  if (!projectId || !/^\d+$/.test(projectId)) return null;
34023
34383
  const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
34024
- const path45 = prefix ? `/${prefix}` : "";
34025
- const envelopeUrl = `${url.protocol}//${url.host}${path45}/api/${projectId}/envelope/`;
34384
+ const path47 = prefix ? `/${prefix}` : "";
34385
+ const envelopeUrl = `${url.protocol}//${url.host}${path47}/api/${projectId}/envelope/`;
34026
34386
  return { envelopeUrl, publicKey };
34027
34387
  } catch {
34028
34388
  return null;
@@ -34462,23 +34822,23 @@ function createSocketIoCors(options) {
34462
34822
  }
34463
34823
 
34464
34824
  // server/routes/health.ts
34465
- var import_node_fs16 = __toESM(require("node:fs"), 1);
34466
- var import_node_path16 = __toESM(require("node:path"), 1);
34825
+ var import_node_fs17 = __toESM(require("node:fs"), 1);
34826
+ var import_node_path18 = __toESM(require("node:path"), 1);
34467
34827
 
34468
34828
  // packages/core/observability/health.ts
34469
- var import_node_fs15 = __toESM(require("node:fs"), 1);
34470
- var import_node_path15 = __toESM(require("node:path"), 1);
34829
+ var import_node_fs16 = __toESM(require("node:fs"), 1);
34830
+ var import_node_path17 = __toESM(require("node:path"), 1);
34471
34831
  var import_node_crypto13 = require("node:crypto");
34472
34832
  function probeStorageWritable(dir) {
34473
- const probe = import_node_path15.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")}`);
34474
34834
  try {
34475
- import_node_fs15.default.mkdirSync(dir, { recursive: true });
34476
- import_node_fs15.default.writeFileSync(probe, "ok", { flag: "w" });
34477
- import_node_fs15.default.unlinkSync(probe);
34835
+ import_node_fs16.default.mkdirSync(dir, { recursive: true });
34836
+ import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
34837
+ import_node_fs16.default.unlinkSync(probe);
34478
34838
  return true;
34479
34839
  } catch {
34480
34840
  try {
34481
- import_node_fs15.default.unlinkSync(probe);
34841
+ import_node_fs16.default.unlinkSync(probe);
34482
34842
  } catch {
34483
34843
  }
34484
34844
  return false;
@@ -34486,15 +34846,15 @@ function probeStorageWritable(dir) {
34486
34846
  }
34487
34847
  function listMigrationNames(migrationsDir) {
34488
34848
  try {
34489
- if (!import_node_fs15.default.existsSync(migrationsDir)) return null;
34490
- const names = import_node_fs15.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path15.default.basename(file, ".sql"));
34849
+ if (!import_node_fs16.default.existsSync(migrationsDir)) return null;
34850
+ const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path17.default.basename(file, ".sql"));
34491
34851
  return names.length > 0 ? names : null;
34492
34852
  } catch {
34493
34853
  return null;
34494
34854
  }
34495
34855
  }
34496
34856
  function migrationRecordName(name) {
34497
- return import_node_path15.default.basename(name.replace(/\r/g, "").trim(), ".sql");
34857
+ return import_node_path17.default.basename(name.replace(/\r/g, "").trim(), ".sql");
34498
34858
  }
34499
34859
  function migrationsComplete(expected, applied) {
34500
34860
  if (!expected) return { ok: false, reason: "migrations" };
@@ -34586,15 +34946,15 @@ function countPendingMigrations(migrationsDir, appliedMigrations) {
34586
34946
  function newestBackupAgeSeconds(backupDir, now) {
34587
34947
  let names;
34588
34948
  try {
34589
- if (!import_node_fs16.default.existsSync(backupDir)) return OPERATIONAL_GAUGE_UNKNOWN;
34590
- names = import_node_fs16.default.readdirSync(backupDir);
34949
+ if (!import_node_fs17.default.existsSync(backupDir)) return OPERATIONAL_GAUGE_UNKNOWN;
34950
+ names = import_node_fs17.default.readdirSync(backupDir);
34591
34951
  } catch {
34592
34952
  return OPERATIONAL_GAUGE_UNKNOWN;
34593
34953
  }
34594
34954
  let newestMs = Number.NEGATIVE_INFINITY;
34595
34955
  for (const name of names) {
34596
34956
  try {
34597
- const stat = import_node_fs16.default.statSync(import_node_path16.default.join(backupDir, name));
34957
+ const stat = import_node_fs17.default.statSync(import_node_path18.default.join(backupDir, name));
34598
34958
  if (!stat.isFile()) continue;
34599
34959
  if (stat.mtimeMs > newestMs) newestMs = stat.mtimeMs;
34600
34960
  } catch {
@@ -34626,7 +34986,7 @@ function defaultCountEvents() {
34626
34986
  function registerHealthRoutes(app, options) {
34627
34987
  const startTime = options?.startTime ?? Date.now();
34628
34988
  const platformVersion = options?.platformVersion ?? PLATFORM_VERSION;
34629
- const migrationsDir = options?.migrationsDir ?? import_node_path16.default.join(process.cwd(), "migrations");
34989
+ const migrationsDir = options?.migrationsDir ?? import_node_path18.default.join(process.cwd(), "migrations");
34630
34990
  const storageDir = options?.storageDir ?? getStorageDir();
34631
34991
  const backupDir = options?.backupDir ?? getBackupsDir();
34632
34992
  const countEvents = options?.countEvents ?? defaultCountEvents;
@@ -34746,8 +35106,8 @@ var import_express2 = __toESM(require("express"), 1);
34746
35106
  var import_zod2 = require("zod");
34747
35107
 
34748
35108
  // server/services/course-package-assets.ts
34749
- var import_node_fs17 = __toESM(require("node:fs"), 1);
34750
- var import_node_path17 = __toESM(require("node:path"), 1);
35109
+ var import_node_fs18 = __toESM(require("node:fs"), 1);
35110
+ var import_node_path19 = __toESM(require("node:path"), 1);
34751
35111
  var import_node_crypto14 = __toESM(require("node:crypto"), 1);
34752
35112
  var import_jszip3 = __toESM(require("jszip"), 1);
34753
35113
  var import_jspdf = require("jspdf");
@@ -34991,8 +35351,8 @@ async function materializePackageAssets(assets) {
34991
35351
  const result = /* @__PURE__ */ new Map();
34992
35352
  if (assets.length === 0) return result;
34993
35353
  const uploadsDir = getUploadsDir();
34994
- if (!import_node_fs17.default.existsSync(uploadsDir)) {
34995
- import_node_fs17.default.mkdirSync(uploadsDir, { recursive: true });
35354
+ if (!import_node_fs18.default.existsSync(uploadsDir)) {
35355
+ import_node_fs18.default.mkdirSync(uploadsDir, { recursive: true });
34996
35356
  }
34997
35357
  const writtenPaths = [];
34998
35358
  try {
@@ -35014,8 +35374,8 @@ async function materializePackageAssets(assets) {
35014
35374
  throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
35015
35375
  }
35016
35376
  const uniqueName = `${Date.now()}-${import_node_crypto14.default.randomBytes(4).toString("hex")}${ext}`;
35017
- const filePath = import_node_path17.default.join(uploadsDir, uniqueName);
35018
- import_node_fs17.default.writeFileSync(filePath, buffer);
35377
+ const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
35378
+ import_node_fs18.default.writeFileSync(filePath, buffer);
35019
35379
  writtenPaths.push(filePath);
35020
35380
  const slideCount = isEmbeddableAssetType(asset.contentType) ? asset.contentType === "pdf" ? countPdfPages(buffer) : await countPptxSlides(buffer) : 0;
35021
35381
  result.set(asset.id, {
@@ -35029,7 +35389,7 @@ async function materializePackageAssets(assets) {
35029
35389
  } catch (err) {
35030
35390
  for (const filePath of writtenPaths) {
35031
35391
  try {
35032
- import_node_fs17.default.unlinkSync(filePath);
35392
+ import_node_fs18.default.unlinkSync(filePath);
35033
35393
  } catch (cleanupErr) {
35034
35394
  console.warn("[course-package] \u9644\u4EF6\u8865\u507F\u6E05\u7406\u5931\u8D25:", cleanupErr.message);
35035
35395
  }
@@ -40031,7 +40391,7 @@ ${runtimeScripts}` : BRIDGE_SDK_CODE;
40031
40391
  }
40032
40392
 
40033
40393
  // server/routes/lessons.ts
40034
- var import_node_fs18 = __toESM(require("node:fs"), 1);
40394
+ var import_node_fs19 = __toESM(require("node:fs"), 1);
40035
40395
 
40036
40396
  // server/services/diagnostic-service.ts
40037
40397
  var import_crypto12 = require("crypto");
@@ -41943,8 +42303,8 @@ function registerLessonsRoutes(ctx) {
41943
42303
  passed += 1;
41944
42304
  try {
41945
42305
  const dir = getUploadsDir();
41946
- import_node_fs18.default.mkdirSync(dir, { recursive: true });
41947
- import_node_fs18.default.accessSync(dir, import_node_fs18.default.constants.W_OK);
42306
+ import_node_fs19.default.mkdirSync(dir, { recursive: true });
42307
+ import_node_fs19.default.accessSync(dir, import_node_fs19.default.constants.W_OK);
41948
42308
  checks.staticResources = { status: "passed", message: "\u8BFE\u4EF6\u5B58\u50A8\u76EE\u5F55\u53EF\u5199" };
41949
42309
  passed += 1;
41950
42310
  } catch (err) {
@@ -41977,8 +42337,8 @@ function registerLessonsRoutes(ctx) {
41977
42337
  }
41978
42338
 
41979
42339
  // server/services/course-package-import.ts
41980
- var import_node_fs19 = __toESM(require("node:fs"), 1);
41981
- var import_node_path18 = __toESM(require("node:path"), 1);
42340
+ var import_node_fs20 = __toESM(require("node:fs"), 1);
42341
+ var import_node_path20 = __toESM(require("node:path"), 1);
41982
42342
  var import_uuid18 = require("uuid");
41983
42343
 
41984
42344
  // server/services/course-package-identity.ts
@@ -42253,11 +42613,11 @@ function findReferencedAsset(rawData, assets) {
42253
42613
  function cleanupMaterializedAssets(materialized) {
42254
42614
  const uploadsDir = getUploadsDir();
42255
42615
  for (const asset of materialized.values()) {
42256
- const fileName = import_node_path18.default.basename(asset.fileUrl);
42257
- const target = import_node_path18.default.join(uploadsDir, fileName);
42616
+ const fileName = import_node_path20.default.basename(asset.fileUrl);
42617
+ const target = import_node_path20.default.join(uploadsDir, fileName);
42258
42618
  try {
42259
- if (import_node_fs19.default.existsSync(target)) {
42260
- import_node_fs19.default.unlinkSync(target);
42619
+ if (import_node_fs20.default.existsSync(target)) {
42620
+ import_node_fs20.default.unlinkSync(target);
42261
42621
  }
42262
42622
  } catch (err) {
42263
42623
  console.warn(`[course-package] \u6E05\u7406\u5B64\u513F\u9644\u4EF6\u5931\u8D25\uFF1A${target}`, err.message);
@@ -46390,6 +46750,85 @@ function registerAssignmentsRoutes(ctx) {
46390
46750
 
46391
46751
  // server/routes/assignment-hub.ts
46392
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
46393
46832
  function decodeFileName(raw) {
46394
46833
  const text = String(raw ?? "").trim();
46395
46834
  if (!text) return "";
@@ -46569,6 +47008,19 @@ function registerAssignmentHubRoutes(ctx) {
46569
47008
  );
46570
47009
  res.setHeader("X-Content-Type-Options", "nosniff");
46571
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
+ }
46572
47024
  res.download(absPath, originalName);
46573
47025
  } catch (e) {
46574
47026
  sendHubError(res, e);
@@ -48334,7 +48786,7 @@ async function checkVersion(source, installedVersion) {
48334
48786
  init_audit_log();
48335
48787
 
48336
48788
  // server/routes/plugin-api-gateway.ts
48337
- var import_node_path19 = __toESM(require("node:path"), 1);
48789
+ var import_node_path21 = __toESM(require("node:path"), 1);
48338
48790
  init_http_router();
48339
48791
  init_interfaces();
48340
48792
  var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
@@ -48508,7 +48960,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
48508
48960
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
48509
48961
  return;
48510
48962
  }
48511
- const normalizedSubPath = import_node_path19.default.posix.normalize("/" + rawSubPath);
48963
+ const normalizedSubPath = import_node_path21.default.posix.normalize("/" + rawSubPath);
48512
48964
  if (normalizedSubPath.includes("..")) {
48513
48965
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
48514
48966
  return;
@@ -48945,8 +49397,8 @@ init_fetch_with_retry();
48945
49397
 
48946
49398
  // server/services/community-registry.ts
48947
49399
  var import_semver7 = __toESM(require("semver"), 1);
48948
- var import_node_fs20 = __toESM(require("node:fs"), 1);
48949
- var import_node_path20 = __toESM(require("node:path"), 1);
49400
+ var import_node_fs21 = __toESM(require("node:fs"), 1);
49401
+ var import_node_path22 = __toESM(require("node:path"), 1);
48950
49402
  var import_node_os3 = __toESM(require("node:os"), 1);
48951
49403
  var import_node_crypto16 = require("node:crypto");
48952
49404
  var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
@@ -49168,8 +49620,8 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
49168
49620
  if (!response.body) {
49169
49621
  throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
49170
49622
  }
49171
- const zipPath = import_node_path20.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
49172
- const out = import_node_fs20.default.createWriteStream(zipPath);
49623
+ const zipPath = import_node_path22.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
49624
+ const out = import_node_fs21.default.createWriteStream(zipPath);
49173
49625
  let received = 0;
49174
49626
  try {
49175
49627
  for await (const chunk of response.body) {
@@ -49191,13 +49643,13 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
49191
49643
  } catch (err) {
49192
49644
  out.destroy();
49193
49645
  try {
49194
- import_node_fs20.default.rmSync(zipPath, { force: true });
49646
+ import_node_fs21.default.rmSync(zipPath, { force: true });
49195
49647
  } catch {
49196
49648
  }
49197
49649
  throw err;
49198
49650
  }
49199
49651
  if (received === 0) {
49200
- import_node_fs20.default.rmSync(zipPath, { force: true });
49652
+ import_node_fs21.default.rmSync(zipPath, { force: true });
49201
49653
  throw new Error("\u63D2\u4EF6\u5305\u4E3A\u7A7A");
49202
49654
  }
49203
49655
  return { zipPath, filename: resolveFilename(response, downloadUrl), bytes: received };
@@ -57121,8 +57573,10 @@ async function startServer() {
57121
57573
  if (migrations.length === 0) {
57122
57574
  throw new Error(`[Migration] FATAL: ${migrationsDir} \u4E2D\u6CA1\u6709\u8FC1\u79FB\u6587\u4EF6\uFF0C\u62D2\u7EDD\u542F\u52A8\u3002`);
57123
57575
  }
57124
- runMigrations(kernelContainer.db, migrations);
57576
+ runMigrations(kernelContainer.db, migrations, { strictLedger: true });
57125
57577
  console.log(`[Migration] \u542F\u52A8\u590D\u67E5\u5B8C\u6210\uFF1A${migrationsDir}\uFF08${migrations.length} \u4E2A\u6587\u4EF6\uFF09`);
57578
+ assertSchemaMatchesGolden(kernelContainer.db, migrationsDir);
57579
+ console.log("[SchemaGolden] \u542F\u52A8\u7EC8\u68C0\u901A\u8FC7\uFF1A\u6570\u636E\u5E93\u7ED3\u6784\u8986\u76D6\u4EE3\u7801\u671F\u671B\u7684\u5168\u90E8\u8868\u4E0E\u5217\u3002");
57126
57580
  } catch (err) {
57127
57581
  console.error("[Migration] FATAL: \u6570\u636E\u5E93\u8FC1\u79FB\u5931\u8D25\uFF0C\u62D2\u7EDD\u5728\u672A\u5B8C\u6210\u8FC1\u79FB\u7684\u5E93\u4E0A\u63D0\u4F9B\u670D\u52A1\u3002");
57128
57582
  console.error(err);