openlearn-next 0.7.6 → 0.7.8

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 (75) hide show
  1. package/README.md +66 -150
  2. package/dist/assets/{AcademicYearFilter-DUUGpDKt.js → AcademicYearFilter-Dm3no4Nb.js} +1 -1
  3. package/dist/assets/ActivityWorkspaceWidget-BXS1IamH.js +1 -0
  4. package/dist/assets/{AdminDirectoryView-DL9zZi-_.js → AdminDirectoryView-479SfYyD.js} +2 -2
  5. package/dist/assets/{AssessmentCenter-B2JzXBGe.js → AssessmentCenter-CJ-pchOQ.js} +1 -1
  6. package/dist/assets/{AssignmentFilePreview-Bk751Blz.js → AssignmentFilePreview-wjnrKjsp.js} +1 -1
  7. package/dist/assets/{BatchPickerModal-9ekXDUUj.js → BatchPickerModal-JvoD-1pF.js} +1 -1
  8. package/dist/assets/{ClassesView-PCt8I3MU.js → ClassesView-CB8q2kW0.js} +12 -12
  9. package/dist/assets/ClassroomCountdownWidget-BqzBpAvi.js +2 -0
  10. package/dist/assets/{CloudDriveModal-Mp59sr-C.js → CloudDriveModal-DHxOnL4m.js} +1 -1
  11. package/dist/assets/ComputerLabView-BEDe7WiU.js +1 -0
  12. package/dist/assets/CourseManagement-BjxQC6sq.js +1 -0
  13. package/dist/assets/{CourseWizardModal-B7-e8otR.js → CourseWizardModal-D3UPFtT_.js} +2 -2
  14. package/dist/assets/{CoursewareHubPanel-DLjH-UzF.js → CoursewareHubPanel-CqJqcZGs.js} +1 -1
  15. package/dist/assets/{Dashboard-D3LXLQNC.js → Dashboard-C3BaEHIG.js} +2 -2
  16. package/dist/assets/{ExportWeightModal-vd7XgaJ0.js → ExportWeightModal-iF0pVevG.js} +1 -1
  17. package/dist/assets/{HelpTour-BLuiS8rr.js → HelpTour-6ChRqWnB.js} +1 -1
  18. package/dist/assets/{HelpView-C4BHoNqR.js → HelpView-D3iYhzBz.js} +2 -2
  19. package/dist/assets/ImportLessonsModal-BOwj7ezj.js +1 -0
  20. package/dist/assets/{ImportModal-H5s5MyZY.js → ImportModal-DAHGdI9h.js} +1 -1
  21. package/dist/assets/{InteractiveCoursewareViewer-D9GzG28E.js → InteractiveCoursewareViewer-DX9xnk6j.js} +1 -1
  22. package/dist/assets/InteractiveWhiteboard-41mi9keH.js +65 -0
  23. package/dist/assets/LazyCourseware-DKyUHXaE.js +2 -0
  24. package/dist/assets/LazyWhiteboard-fXS2P44c.js +2 -0
  25. package/dist/assets/LessonEditorView-Dx-hOKtt.js +1 -0
  26. package/dist/assets/LiveClassroomView-m0hKdcVK.js +61 -0
  27. package/dist/assets/NotificationDetailModal-C2tg_IoJ.js +1 -0
  28. package/dist/assets/PluginView-BPRUpCjV.js +77 -0
  29. package/dist/assets/{ProcessLogsModal-_BwkmJVw.js → ProcessLogsModal-CULHzXp7.js} +1 -1
  30. package/dist/assets/{QuizGeneratorModal-DiItok6z.js → QuizGeneratorModal-CYxngpW9.js} +1 -1
  31. package/dist/assets/StudentAssignmentEvalPanel-Dfe4IOs5.js +1 -0
  32. package/dist/assets/{StudentAssignmentView-UK_7_MF3.js → StudentAssignmentView-IfRo3yJk.js} +1 -1
  33. package/dist/assets/StudentLessonView-Dw2uxBhq.js +1 -0
  34. package/dist/assets/StudentView-DVYUSrVU.js +2 -0
  35. package/dist/assets/{SystemResourceLibraryModal-dEdNKbk2.js → SystemResourceLibraryModal-BhbpfIBt.js} +1 -1
  36. package/dist/assets/TeacherView-CZjIejet.js +2 -0
  37. package/dist/assets/{TimetableView-AkReskNG.js → TimetableView-CupOIfEy.js} +1 -1
  38. package/dist/assets/{UTIF-4E-wdh3X.js → UTIF-BtQhea94.js} +1 -1
  39. package/dist/assets/index-BDFyco9S.css +1 -0
  40. package/dist/assets/index-BiHQqzdm.js +131 -0
  41. package/dist/assets/{index.es-WXdI0L-F.js → index.es-CijVATXs.js} +1 -1
  42. package/dist/assets/{pluginParsers-k90WUDrU.js → pluginParsers-C9W6GBVx.js} +1 -1
  43. package/dist/assets/{student-workspace-BDw1i4Vl.js → student-workspace-CsbA8Cs4.js} +1 -1
  44. package/dist/assets/{vendor-charts-SrpBsvM3.js → vendor-charts-skggaP9D.js} +1 -1
  45. package/dist/assets/{vendor-icons-hc6MjYFv.js → vendor-icons-DaYNfRAc.js} +2 -2
  46. package/dist/assets/{vendor-konva-Dne0Z-WC.js → vendor-konva-BOuJWFuE.js} +1 -1
  47. package/dist/assets/{vendor-pdf-By0fofA0.js → vendor-pdf-DVWGghIL.js} +3 -3
  48. package/dist/assets/{vendor-pptx-EjNUtdws.js → vendor-pptx-BqU7v5zS.js} +5 -5
  49. package/dist/assets/{vendor-react-Kv_JATwr.js → vendor-react-DHoBZpn2.js} +2 -2
  50. package/dist/assets/{vendor-utils-aM7aYvPP.js → vendor-utils-BxOuk3id.js} +1 -1
  51. package/dist/env-validation.cjs +7 -1
  52. package/dist/index.html +6 -6
  53. package/dist/server.cjs +343 -37
  54. package/migrations/044_students_gender.sql +8 -0
  55. package/migrations/README.md +1 -0
  56. package/migrations/schema-golden.json +3 -2
  57. package/package.json +2 -2
  58. package/dist/assets/ActivityWorkspaceWidget-0DUqiqFK.js +0 -1
  59. package/dist/assets/ClassroomCountdownWidget-HxelcHUp.js +0 -2
  60. package/dist/assets/ComputerLabView-FStILvFC.js +0 -1
  61. package/dist/assets/CourseManagement-BQw2idgZ.js +0 -1
  62. package/dist/assets/ImportLessonsModal-Y6uIIemO.js +0 -1
  63. package/dist/assets/InteractiveWhiteboard-DswIV-ES.js +0 -65
  64. package/dist/assets/LazyCourseware-BEoTTg5h.js +0 -2
  65. package/dist/assets/LazyWhiteboard-CMBBxnFX.js +0 -2
  66. package/dist/assets/LessonEditorView-BgJtG8Ks.js +0 -1
  67. package/dist/assets/LiveClassroomView-Czwt5WW-.js +0 -61
  68. package/dist/assets/NotificationDetailModal-D42vEs3S.js +0 -1
  69. package/dist/assets/PluginView-BcOWIjLu.js +0 -77
  70. package/dist/assets/StudentAssignmentEvalPanel-DHoZc3g0.js +0 -1
  71. package/dist/assets/StudentLessonView-C11Qkdi_.js +0 -1
  72. package/dist/assets/StudentView-j0hx3IOi.js +0 -2
  73. package/dist/assets/TeacherView-COZw2tFl.js +0 -2
  74. package/dist/assets/index-DN3E-Fmw.js +0 -131
  75. package/dist/assets/index-DPky2leG.css +0 -1
package/dist/server.cjs CHANGED
@@ -916,7 +916,12 @@ var init_manifest_schema = __esm({
916
916
  teacherPanelSchema = import_zod.z.object({
917
917
  id: import_zod.z.string().min(1),
918
918
  title: import_zod.z.string().min(1),
919
- icon: import_zod.z.string().optional()
919
+ icon: import_zod.z.string().optional(),
920
+ // v0.7.6 双轨制 UI:第三方 ZIP 插件在 process 模式下无法传 React 组件,
921
+ // 只能声明可视沙箱 HTML 的相对路径。此处必须显式列出 —— zod 的 object
922
+ // 默认 strip 未声明键,漏声明会让 view 在 parse() 后被静默丢弃,
923
+ // 宿主便退回「占位空态」,插件作者看到的是空白面板且无任何报错。
924
+ view: import_zod.z.string().min(1).optional()
920
925
  });
921
926
  studentFullscreenSchema = import_zod.z.object({
922
927
  id: import_zod.z.string().min(1),
@@ -994,7 +999,11 @@ var init_manifest_schema = __esm({
994
999
  id: import_zod.z.string().min(1),
995
1000
  label: import_zod.z.string().min(1),
996
1001
  icon: import_zod.z.string().optional(),
997
- position: import_zod.z.number().int().optional()
1002
+ position: import_zod.z.number().int().optional(),
1003
+ // 可视沙箱视图(v0.7.6+)。同 teacherPanelSchema.view:漏声明 = 被 strip,
1004
+ // 第三方插件的教师页签会退化成空白占位。
1005
+ view: import_zod.z.string().min(1).optional(),
1006
+ route: import_zod.z.string().optional()
998
1007
  })
999
1008
  ).optional(),
1000
1009
  "teacher.dashboard.widget": import_zod.z.array(
@@ -1002,7 +1011,8 @@ var init_manifest_schema = __esm({
1002
1011
  id: import_zod.z.string().min(1),
1003
1012
  label: import_zod.z.string().min(1),
1004
1013
  icon: import_zod.z.string().optional(),
1005
- position: import_zod.z.number().int().optional()
1014
+ position: import_zod.z.number().int().optional(),
1015
+ view: import_zod.z.string().min(1).optional()
1006
1016
  })
1007
1017
  ).optional(),
1008
1018
  "student.view": import_zod.z.array(
@@ -1010,14 +1020,17 @@ var init_manifest_schema = __esm({
1010
1020
  id: import_zod.z.string().min(1),
1011
1021
  label: import_zod.z.string().min(1),
1012
1022
  icon: import_zod.z.string().optional(),
1013
- route: import_zod.z.string().optional()
1023
+ route: import_zod.z.string().optional(),
1024
+ // 可视沙箱视图(v0.7.6+)。学生端同理:process 模式只能走 HTML 页面。
1025
+ view: import_zod.z.string().min(1).optional()
1014
1026
  })
1015
1027
  ).optional(),
1016
1028
  "student.lesson.tool": import_zod.z.array(
1017
1029
  import_zod.z.object({
1018
1030
  id: import_zod.z.string().min(1),
1019
1031
  label: import_zod.z.string().min(1),
1020
- icon: import_zod.z.string().optional()
1032
+ icon: import_zod.z.string().optional(),
1033
+ view: import_zod.z.string().min(1).optional()
1021
1034
  })
1022
1035
  ).optional(),
1023
1036
  // E-1:补全 contribution-registry.ts 已定义的全部服务端 slot
@@ -4587,14 +4600,33 @@ function wrapProcessManager(processService, tracker, pluginId, processOwnerLooku
4587
4600
  })
4588
4601
  };
4589
4602
  }
4590
- function wrapActionRegistry(actionRegistry, tracker, pluginId) {
4603
+ function wrapActionRegistry(actionRegistry, tracker, pluginId, manifestId) {
4604
+ const prefix = manifestId || pluginId;
4605
+ const isKernel = prefix.startsWith("@openlearn/");
4591
4606
  return {
4592
4607
  register: createSafeFunction((descriptor) => {
4593
- return Promise.resolve(actionRegistry.register(descriptor)).then(() => {
4608
+ const commandType = descriptor.commandType;
4609
+ const prefixed = isKernel || !commandType || commandType.startsWith(`${prefix}.`) ? commandType : `${prefix}.${commandType}`;
4610
+ const effectiveDescriptor = prefixed !== commandType ? { ...descriptor, commandType: prefixed } : descriptor;
4611
+ return Promise.resolve(actionRegistry.register(effectiveDescriptor)).then(() => {
4612
+ if (prefixed !== commandType) {
4613
+ Promise.resolve(
4614
+ actionRegistry.register({
4615
+ ...descriptor,
4616
+ id: `${descriptor.id}__raw_alias`,
4617
+ exposeToAgent: false
4618
+ })
4619
+ ).catch(() => {
4620
+ });
4621
+ }
4594
4622
  tracker.track(pluginId, {
4595
4623
  dispose: () => {
4596
- Promise.resolve(actionRegistry.unregister(descriptor.id)).catch(() => {
4624
+ Promise.resolve(actionRegistry.unregister(effectiveDescriptor.id)).catch(() => {
4597
4625
  });
4626
+ if (prefixed !== commandType) {
4627
+ Promise.resolve(actionRegistry.unregister(`${descriptor.id}__raw_alias`)).catch(() => {
4628
+ });
4629
+ }
4598
4630
  }
4599
4631
  });
4600
4632
  });
@@ -4835,7 +4867,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4835
4867
  // 可选方法,mock 或旧部署可缺省 —— 缺省时 isOwnProcess 退化为「不拦截」,与旧行为一致。
4836
4868
  typeof processService.getProcessOwner === "function" ? (processId) => processService.getProcessOwner(processId) : void 0
4837
4869
  );
4838
- const wrappedActionRegistry = wrapActionRegistry(actionRegistryService, tracker, pluginId);
4870
+ const wrappedActionRegistry = wrapActionRegistry(actionRegistryService, tracker, pluginId, manifest.id);
4839
4871
  const wrappedCapability = wrapCapability(capabilityService);
4840
4872
  const wrappedStorage = wrapStorage(storageService, db2, manifest.id);
4841
4873
  const wrappedAI = wrapAI(aiService, pluginId, tracker);
@@ -8480,10 +8512,28 @@ var init_service_host = __esm({
8480
8512
  const actionRegistry = service;
8481
8513
  if (msg.method === "register") {
8482
8514
  const [descriptor] = effectiveArgs;
8483
- await actionRegistry.register(descriptor);
8484
- this.registeredActionIds.add(descriptor.id);
8485
- if (typeof descriptor?.commandType === "string") {
8486
- this.registeredActionCommandTypes.set(descriptor.id, descriptor.commandType);
8515
+ const pluginId = this.pluginId || this.pluginActorId.replace(/^plugin:/, "");
8516
+ const isKernel = pluginId.startsWith("@openlearn/");
8517
+ const rawCommandType = descriptor.commandType;
8518
+ const prefixed = isKernel || !rawCommandType || rawCommandType.startsWith(`${pluginId}.`) ? rawCommandType : resolvePluginCommandType(rawCommandType, pluginId);
8519
+ const effectiveDescriptor = prefixed !== rawCommandType ? { ...descriptor, commandType: prefixed } : descriptor;
8520
+ await actionRegistry.register(effectiveDescriptor);
8521
+ this.registeredActionIds.add(effectiveDescriptor.id);
8522
+ if (typeof effectiveDescriptor?.commandType === "string") {
8523
+ this.registeredActionCommandTypes.set(effectiveDescriptor.id, effectiveDescriptor.commandType);
8524
+ }
8525
+ if (prefixed !== rawCommandType) {
8526
+ try {
8527
+ const aliasId = `${descriptor.id}__raw_alias`;
8528
+ await actionRegistry.register({
8529
+ ...descriptor,
8530
+ id: aliasId,
8531
+ exposeToAgent: false
8532
+ });
8533
+ this.registeredActionIds.add(aliasId);
8534
+ this.registeredActionCommandTypes.set(aliasId, rawCommandType);
8535
+ } catch {
8536
+ }
8487
8537
  }
8488
8538
  transport.postMessage({
8489
8539
  type: "result",
@@ -8494,8 +8544,14 @@ var init_service_host = __esm({
8494
8544
  } else if (msg.method === "unregister") {
8495
8545
  const [actionId] = msg.args;
8496
8546
  await actionRegistry.unregister(actionId);
8547
+ try {
8548
+ await actionRegistry.unregister(`${actionId}__raw_alias`);
8549
+ } catch {
8550
+ }
8497
8551
  this.registeredActionIds.delete(actionId);
8552
+ this.registeredActionIds.delete(`${actionId}__raw_alias`);
8498
8553
  this.registeredActionCommandTypes.delete(actionId);
8554
+ this.registeredActionCommandTypes.delete(`${actionId}__raw_alias`);
8499
8555
  transport.postMessage({
8500
8556
  type: "result",
8501
8557
  invokeId: msg.invokeId,
@@ -15228,6 +15284,26 @@ var BuiltinPlugin = {
15228
15284
  "page_meta"
15229
15285
  // 分页与大纲元数据
15230
15286
  ];
15287
+ const isAllowedElementType = (type) => {
15288
+ if (!type || typeof type !== "string") return false;
15289
+ if (KNOWN_ELEMENT_TYPES2.includes(type)) return true;
15290
+ if (type.includes("/") || type.startsWith("plugin:") || type.startsWith("ext-") || type.startsWith("plugin-")) {
15291
+ return true;
15292
+ }
15293
+ try {
15294
+ const reg = pluginHost?.getContributionRegistry?.();
15295
+ if (reg) {
15296
+ const paletteItems = reg.getBySlot("editor.palette_item") || [];
15297
+ if (paletteItems.some((item) => item.type === type || item.id === type)) return true;
15298
+ const renderers = reg.getBySlot("whiteboard.renderer") || [];
15299
+ if (renderers.some((item) => item.targetType === type || item.id === type || item.elementType === type)) {
15300
+ return true;
15301
+ }
15302
+ }
15303
+ } catch {
15304
+ }
15305
+ return false;
15306
+ };
15231
15307
  await actionRegistry.register({
15232
15308
  id: "core-whiteboard-draw",
15233
15309
  commandType: drawWhiteboardCmdType,
@@ -15251,8 +15327,8 @@ var BuiltinPlugin = {
15251
15327
  await commandBus.registerHandler(drawWhiteboardCmdType, {
15252
15328
  async execute(command) {
15253
15329
  const payload = command.payload;
15254
- if (!KNOWN_ELEMENT_TYPES2.includes(payload.type)) {
15255
- const hint = payload.type === "quiz" ? "" : payload.type && (payload.type.includes("quiz") || payload.type.includes("question")) ? ` \u63D0\u793A\uFF1A\u521B\u5EFA\u6D4B\u9A8C\u8BF7\u4F7F\u7528 quiz.create \u6216 quiz_pro.create \u5DE5\u5177\u3002` : ` \u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}\u3002`;
15330
+ if (!isAllowedElementType(payload.type)) {
15331
+ const hint = payload.type === "quiz" ? "" : payload.type && (payload.type.includes("quiz") || payload.type.includes("question")) ? ` \u63D0\u793A\uFF1A\u521B\u5EFA\u6D4B\u9A8C\u8BF7\u4F7F\u7528 quiz.create \u6216 quiz_pro.create \u5DE5\u5177\u3002` : ` \u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}\uFF0C\u6216\u63D2\u4EF6\u6269\u5C55\u56FE\u5143 (\u5982 ext-xxx/yyy)\u3002`;
15256
15332
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${payload.type}"\u3002${hint}`);
15257
15333
  }
15258
15334
  const elementId = (0, import_uuid7.v7)();
@@ -15494,7 +15570,7 @@ var BuiltinPlugin = {
15494
15570
  );
15495
15571
  const batchInsert = db2.transaction(() => {
15496
15572
  for (const el of elements) {
15497
- if (!KNOWN_ELEMENT_TYPES2.includes(el.type)) {
15573
+ if (!isAllowedElementType(el.type)) {
15498
15574
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${el.type}"\u3002\u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}`);
15499
15575
  }
15500
15576
  const elementId = (0, import_uuid7.v7)();
@@ -19027,6 +19103,19 @@ function trustedInlineAllowlist(env = process.env) {
19027
19103
  (env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
19028
19104
  );
19029
19105
  }
19106
+ function matchesTrustedInlinePattern(pattern, manifestId) {
19107
+ if (!manifestId) return false;
19108
+ if (pattern === "*") return true;
19109
+ if (pattern.endsWith("*")) return manifestId.startsWith(pattern.slice(0, -1));
19110
+ return pattern === manifestId;
19111
+ }
19112
+ function isInTrustedInlineAllowlist(manifestId, env = process.env) {
19113
+ if (!manifestId) return false;
19114
+ for (const pattern of trustedInlineAllowlist(env)) {
19115
+ if (matchesTrustedInlinePattern(pattern, manifestId)) return true;
19116
+ }
19117
+ return false;
19118
+ }
19030
19119
  function builtinPluginsDir() {
19031
19120
  try {
19032
19121
  const url = import_meta5.url;
@@ -19049,7 +19138,7 @@ function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir())
19049
19138
  }
19050
19139
  function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
19051
19140
  if (isInsideBuiltinPluginsDir(sourcePath)) return true;
19052
- if (manifestId && trustedInlineAllowlist(env).has(manifestId)) return true;
19141
+ if (isInTrustedInlineAllowlist(manifestId, env)) return true;
19053
19142
  if (env.VITEST && env.OPENLEARN_ENFORCE_PLUGIN_ISOLATION !== "1") return true;
19054
19143
  return false;
19055
19144
  }
@@ -21037,7 +21126,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
21037
21126
  var import_node_path13 = __toESM(require("node:path"), 1);
21038
21127
  var import_node_url5 = require("node:url");
21039
21128
  var import_meta7 = {};
21040
- var FALLBACK_VERSION = "0.7.6";
21129
+ var FALLBACK_VERSION = "0.7.8";
21041
21130
  function readPlatformVersion() {
21042
21131
  const bases = [];
21043
21132
  if (typeof __dirname !== "undefined") bases.push(__dirname);
@@ -32843,11 +32932,17 @@ function validateStartupEnv(env = process.env) {
32843
32932
  severity: "warning",
32844
32933
  message: "OPENLEARN_TRUSTED_INLINE_PLUGINS \u53EA\u542B\u7A7A\u767D/\u9017\u53F7\uFF0C\u7B49\u540C\u4E8E\u672A\u914D\u7F6E\u3002"
32845
32934
  });
32935
+ } else if (ids.includes("*")) {
32936
+ warnings.push({
32937
+ key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
32938
+ severity: "warning",
32939
+ message: "OPENLEARN_TRUSTED_INLINE_PLUGINS \u542B\u901A\u914D\u7B26 `*`\uFF1A**\u6240\u6709**\u63D2\u4EF6\u90FD\u88AB\u89C6\u4E3A\u53D7\u4FE1\uFF0C\u53EF\u5728\u8FDB\u7A0B\u5185\u4EE5 inline/worker \u8FD0\u884C\uFF08\u542B\u65E5\u540E\u5B89\u88C5\u7684\u4EFB\u610F\u7B2C\u4E09\u65B9\u5305\uFF09\u3002\u5B83\u4EEC\u4E0E\u5BBF\u4E3B\u5171\u4EAB\u73AF\u5883\u53D8\u91CF\u4E0E\u7F51\u7EDC\u6808\uFF0C\u51FA\u7AD9\u7F51\u7EDC\u62E6\u622A\u5BF9\u5B83\u4EEC\u4E0D\u751F\u6548\uFF0C\u672A\u6355\u83B7\u5F02\u5E38\u53EF\u62D6\u57AE\u670D\u52A1\u3002\u4EC5\u9650\u672C\u5730\u5F00\u53D1\uFF1B\u751F\u4EA7\u73AF\u5883\u8BF7\u6539\u4E3A\u7CBE\u786E id \u6216\u524D\u7F00\u5F0F\uFF08\u5982 `@ext/*`\uFF09\u3002"
32940
+ });
32846
32941
  } else if (isProduction) {
32847
32942
  warnings.push({
32848
32943
  key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
32849
32944
  severity: "warning",
32850
- message: `\u751F\u4EA7\u73AF\u5883\u4FE1\u4EFB\u4E86 ${ids.length} \u4E2A\u63D2\u4EF6\u5728**\u8FDB\u7A0B\u5185**\u8FD0\u884C\uFF1A${ids.join(", ")}\u3002 \u5B83\u4EEC\u4E0E\u5BBF\u4E3B\u5171\u4EAB\u73AF\u5883\u53D8\u91CF\u4E0E\u7F51\u7EDC\u6808\uFF0C\u51FA\u7AD9\u7F51\u7EDC\u62E6\u622A\u5BF9\u5B83\u4EEC\u4E0D\u751F\u6548\u3002 \u786E\u8BA4\u8FD9\u662F\u6709\u610F\u4E3A\u4E4B\uFF1B\u5426\u5219\u6E05\u7A7A\u8BE5\u53D8\u91CF\u8BA9\u5B83\u4EEC\u56DE\u5230 process \u5B50\u8FDB\u7A0B\u9694\u79BB\u3002`
32945
+ message: `\u751F\u4EA7\u73AF\u5883\u4FE1\u4EFB\u4E86 ${ids.length} \u4E2A\u6761\u76EE\u5728**\u8FDB\u7A0B\u5185**\u8FD0\u884C\uFF1A${ids.join(", ")}\u3002 \u5B83\u4EEC\u4E0E\u5BBF\u4E3B\u5171\u4EAB\u73AF\u5883\u53D8\u91CF\u4E0E\u7F51\u7EDC\u6808\uFF0C\u51FA\u7AD9\u7F51\u7EDC\u62E6\u622A\u5BF9\u5B83\u4EEC\u4E0D\u751F\u6548\u3002 \u786E\u8BA4\u8FD9\u662F\u6709\u610F\u4E3A\u4E4B\uFF1B\u5426\u5219\u6E05\u7A7A\u8BE5\u53D8\u91CF\u8BA9\u5B83\u4EEC\u56DE\u5230 process \u5B50\u8FDB\u7A0B\u9694\u79BB\u3002`
32851
32946
  });
32852
32947
  }
32853
32948
  }
@@ -33671,6 +33766,7 @@ var normalizeToolSchema = (schema) => {
33671
33766
  };
33672
33767
  var buildOpenAITools = (callerRole) => {
33673
33768
  let actions = kernelContainer.actionRegistry.getAllActions();
33769
+ actions = actions.filter((action) => action.exposeToAgent !== false);
33674
33770
  if (callerRole === "student") {
33675
33771
  actions = actions.filter((action) => {
33676
33772
  const cap = action.capabilityRequired || "";
@@ -34628,7 +34724,7 @@ function renderPrometheusMetrics(now = Date.now(), sample) {
34628
34724
  // server/middleware/request-observability.ts
34629
34725
  var REQUEST_ID_RE = /^[A-Za-z0-9._:-]{8,64}$/;
34630
34726
  var QUIET_PATHS = /* @__PURE__ */ new Set(["/health", "/health/ready", "/", "/index.html", "/favicon.ico", "/robots.txt"]);
34631
- var DEV_QUIET_PREFIXES = ["/@vite/", "/@fs/", "/src/", "/node_modules/", "/@id/"];
34727
+ var DEV_QUIET_PREFIXES = ["/@vite/", "/@fs/", "/src/", "/node_modules/", "/@id/", "/.vite-cache/"];
34632
34728
  var DEFAULT_HTTP_SLOW_MS = 1e3;
34633
34729
  function resolveHttpAccessLogMode(envAccessLog = process.env.HTTP_ACCESS_LOG, envLogLevel = process.env.LOG_LEVEL) {
34634
34730
  const normalized = envAccessLog?.trim().toLowerCase();
@@ -36080,6 +36176,142 @@ post({ type: 'ready' });
36080
36176
  </script>
36081
36177
  </body></html>`;
36082
36178
 
36179
+ // server/utils/plugin-view-bridge.ts
36180
+ var PLUGIN_VIEW_CSP = "sandbox allow-scripts allow-forms allow-downloads; frame-ancestors 'self'; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src 'self' 'unsafe-inline' data: blob: https:; style-src-attr 'unsafe-inline'; connect-src 'self' blob: data: https:;";
36181
+ var PLUGIN_VIEW_BRIDGE_JS = `(function () {
36182
+ 'use strict';
36183
+ const CHANNEL = 'ol-plugin-view';
36184
+ let targetOrigin = '*';
36185
+ const pending = new Map();
36186
+ const initCallbacks = [];
36187
+ const contextUpdateCallbacks = [];
36188
+ let currentContext = null;
36189
+
36190
+ function post(msg) {
36191
+ if (window.parent && window.parent !== window) {
36192
+ window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
36193
+ }
36194
+ }
36195
+
36196
+ function call(op, args) {
36197
+ const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
36198
+ return new Promise(function (resolve, reject) {
36199
+ pending.set(callId, { resolve: resolve, reject: reject });
36200
+ post({ type: 'call', callId: callId, op: op, args: args || [] });
36201
+ });
36202
+ }
36203
+
36204
+ window.addEventListener('message', function (event) {
36205
+ const data = event.data;
36206
+ if (!data || data.channel !== CHANNEL) return;
36207
+ if (event.source !== window.parent) return;
36208
+
36209
+ if (data.type === 'init') {
36210
+ currentContext = data.payload || {};
36211
+ targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
36212
+ for (let i = 0; i < initCallbacks.length; i++) {
36213
+ try {
36214
+ initCallbacks[i](currentContext);
36215
+ } catch (e) {
36216
+ console.error('[OpenLearnPluginBridge] init callback error:', e);
36217
+ }
36218
+ }
36219
+ return;
36220
+ }
36221
+
36222
+ if (data.type === 'context-update') {
36223
+ currentContext = Object.assign({}, currentContext, data.payload || {});
36224
+ for (let i = 0; i < contextUpdateCallbacks.length; i++) {
36225
+ try {
36226
+ contextUpdateCallbacks[i](currentContext);
36227
+ } catch (e) {
36228
+ console.error('[OpenLearnPluginBridge] contextUpdate callback error:', e);
36229
+ }
36230
+ }
36231
+ return;
36232
+ }
36233
+
36234
+ if (data.type === 'reply') {
36235
+ const waiter = pending.get(data.callId);
36236
+ if (!waiter) return;
36237
+ pending.delete(data.callId);
36238
+ if (data.ok) {
36239
+ waiter.resolve(data.value);
36240
+ } else {
36241
+ waiter.reject(new Error(data.error || 'bridge call failed'));
36242
+ }
36243
+ return;
36244
+ }
36245
+ });
36246
+
36247
+ const bridge = {
36248
+ CHANNEL: CHANNEL,
36249
+ onInit: function (callback) {
36250
+ if (typeof callback === 'function') {
36251
+ if (currentContext) {
36252
+ try {
36253
+ callback(currentContext);
36254
+ } catch (e) {
36255
+ console.error('[OpenLearnPluginBridge] onInit callback error:', e);
36256
+ }
36257
+ }
36258
+ initCallbacks.push(callback);
36259
+ }
36260
+ return bridge;
36261
+ },
36262
+ onContextUpdate: function (callback) {
36263
+ if (typeof callback === 'function') {
36264
+ contextUpdateCallbacks.push(callback);
36265
+ }
36266
+ return bridge;
36267
+ },
36268
+ getContext: function () {
36269
+ return currentContext;
36270
+ },
36271
+ ui: {
36272
+ toast: function (message, type) {
36273
+ return call('ui.toast', [message, type || 'info']);
36274
+ },
36275
+ showToast: function (message, type) {
36276
+ return call('ui.toast', [message, type || 'info']);
36277
+ },
36278
+ resize: function (height) {
36279
+ post({ type: 'resize', height: Number(height) || 0 });
36280
+ },
36281
+ },
36282
+ invokeCommand: function (type, payload) {
36283
+ return call('command.invoke', [type, payload]);
36284
+ },
36285
+ api: {
36286
+ get: function (path) {
36287
+ return call('api.get', [path]);
36288
+ },
36289
+ post: function (path, body) {
36290
+ return call('api.post', [path, body]);
36291
+ },
36292
+ del: function (path) {
36293
+ return call('api.del', [path]);
36294
+ },
36295
+ },
36296
+ storage: {
36297
+ get: function (key) {
36298
+ return call('storage.get', [key]);
36299
+ },
36300
+ set: function (key, value) {
36301
+ return call('storage.set', [key, value]);
36302
+ },
36303
+ delete: function (key) {
36304
+ return call('storage.delete', [key]);
36305
+ },
36306
+ },
36307
+ };
36308
+
36309
+ window.OpenLearnPluginBridge = bridge;
36310
+ // \u901A\u77E5\u5BBF\u4E3B\u89C6\u56FE\u6C99\u7BB1 iframe \u5DF2\u5C31\u7EEA
36311
+ post({ type: 'ready' });
36312
+ })();
36313
+ `;
36314
+
36083
36315
  // packages/core/bootstrap/adapter/bootstrap-registration.ts
36084
36316
  var BootstrapRegistration = class {
36085
36317
  static registerConfiguration(builder, context) {
@@ -42515,7 +42747,8 @@ async function importCoursePackage(pkg, options) {
42515
42747
  let elements = [];
42516
42748
  try {
42517
42749
  elements = pkg.elements.map((element) => {
42518
- if (!KNOWN_ELEMENT_TYPES.has(element.type)) {
42750
+ const isAllowed = KNOWN_ELEMENT_TYPES.has(element.type) || typeof element.type === "string" && (element.type.includes("/") || element.type.startsWith("plugin:") || element.type.startsWith("ext-") || element.type.startsWith("plugin-"));
42751
+ if (!isAllowed) {
42519
42752
  throw new Error(`\u8BFE\u7A0B\u5305\u5305\u542B\u4E0D\u652F\u6301\u7684\u767D\u677F\u5143\u7D20\u7C7B\u578B\uFF1A${element.type}`);
42520
42753
  }
42521
42754
  const data = resolveAssetPlaceholders(element.data, materialized);
@@ -43365,6 +43598,14 @@ function sanitizePrivateNotes(raw) {
43365
43598
  }
43366
43599
 
43367
43600
  // server/services/roster-service.ts
43601
+ function normalizeStudentGender(value) {
43602
+ if (typeof value !== "string") return null;
43603
+ const v = value.trim().toLowerCase();
43604
+ if (!v) return null;
43605
+ if (["male", "m", "boy", "\u7537", "\u7537\u751F"].includes(v)) return "male";
43606
+ if (["female", "f", "girl", "\u5973", "\u5973\u751F"].includes(v)) return "female";
43607
+ return null;
43608
+ }
43368
43609
  var RosterService = class {
43369
43610
  constructor(db2 = kernelContainer.db, readDb, pluginHost) {
43370
43611
  this.db = db2;
@@ -43657,7 +43898,7 @@ var RosterService = class {
43657
43898
  const params = ownerScoped ? [scope.userId || "", scope.username || ""] : [];
43658
43899
  const total = this.reader.prepare(`SELECT COUNT(*) AS n FROM students s ${where}`).get(...params).n;
43659
43900
  const students = this.reader.prepare(
43660
- `SELECT s.id, s.student_number, s.name, s.email, s.avatar, s.locked_lesson_id, s.lock_mode, s.lock_target, s.private_notes, s.created_at
43901
+ `SELECT s.id, s.student_number, s.name, s.email, s.avatar, s.gender, s.locked_lesson_id, s.lock_mode, s.lock_target, s.private_notes, s.created_at
43661
43902
  FROM students s ${where}
43662
43903
  ORDER BY s.created_at DESC LIMIT ? OFFSET ?`
43663
43904
  ).all(
@@ -43693,6 +43934,7 @@ var RosterService = class {
43693
43934
  }
43694
43935
  createStudent(params) {
43695
43936
  const { name, email, student_number, password } = params;
43937
+ const gender = normalizeStudentGender(params.gender);
43696
43938
  if (!name?.trim()) throw new Error("Student name is required");
43697
43939
  const studentId = randomId();
43698
43940
  let finalNum = student_number && student_number.trim() !== "" ? student_number.trim() : "";
@@ -43709,7 +43951,9 @@ var RosterService = class {
43709
43951
  finalPlain = initialPassword;
43710
43952
  }
43711
43953
  const hashedPassword = hashPassword(finalPlain);
43712
- this.db.prepare("INSERT INTO students (id, student_number, name, email, password, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(studentId, finalNum, name.trim(), email || "", hashedPassword, Date.now());
43954
+ this.db.prepare(
43955
+ "INSERT INTO students (id, student_number, name, email, password, gender, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
43956
+ ).run(studentId, finalNum, name.trim(), email || "", hashedPassword, gender, Date.now());
43713
43957
  return {
43714
43958
  id: studentId,
43715
43959
  student_number: finalNum,
@@ -43718,6 +43962,9 @@ var RosterService = class {
43718
43962
  }
43719
43963
  updateStudent(id, params) {
43720
43964
  const { name, email, student_number, password, locked_lesson_id, lock_mode, lock_target, private_notes } = params;
43965
+ if (params.gender !== void 0) {
43966
+ this.db.prepare("UPDATE students SET gender = ? WHERE id = ?").run(normalizeStudentGender(params.gender), id);
43967
+ }
43721
43968
  if (name) this.db.prepare("UPDATE students SET name = ? WHERE id = ?").run(name.trim(), id);
43722
43969
  if (email !== void 0)
43723
43970
  this.db.prepare("UPDATE students SET email = ? WHERE id = ?").run(email?.trim() || null, id);
@@ -43835,7 +44082,7 @@ var RosterService = class {
43835
44082
  getClassStudents(classId) {
43836
44083
  return this.reader.prepare(
43837
44084
  `
43838
- SELECT s.id, s.student_number, s.name, s.email, s.avatar, s.locked_lesson_id, s.lock_mode, s.lock_target, s.private_notes, s.created_at, cs.joined_at
44085
+ SELECT s.id, s.student_number, s.name, s.email, s.avatar, s.gender, s.locked_lesson_id, s.lock_mode, s.lock_target, s.private_notes, s.created_at, cs.joined_at
43839
44086
  FROM students s
43840
44087
  INNER JOIN class_students cs ON s.id = cs.student_id
43841
44088
  WHERE cs.class_id = ?
@@ -43896,13 +44143,14 @@ var RosterService = class {
43896
44143
  email,
43897
44144
  studentNumber,
43898
44145
  notes,
44146
+ gender: normalizeStudentGender(st?.gender),
43899
44147
  plainPassword,
43900
44148
  autoPassword,
43901
44149
  hashedPassword: hashPassword(plainPassword)
43902
44150
  });
43903
44151
  }
43904
44152
  const insertStudent = this.db.prepare(
43905
- "INSERT INTO students (id, student_number, name, email, password, private_notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
44153
+ "INSERT INTO students (id, student_number, name, email, password, private_notes, gender, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
43906
44154
  );
43907
44155
  const findByEmail = this.db.prepare("SELECT id, student_number, name, email FROM students WHERE email = ?");
43908
44156
  const findByNumber = this.db.prepare(
@@ -43929,7 +44177,16 @@ var RosterService = class {
43929
44177
  }
43930
44178
  const studentId = randomId("stu_");
43931
44179
  const finalNum = row.studentNumber || generateStudentNumber(this.db) || `ST_${studentId}`;
43932
- insertStudent.run(studentId, finalNum, row.name, row.email || null, row.hashedPassword, row.notes || null, now);
44180
+ insertStudent.run(
44181
+ studentId,
44182
+ finalNum,
44183
+ row.name,
44184
+ row.email || null,
44185
+ row.hashedPassword,
44186
+ row.notes || null,
44187
+ row.gender,
44188
+ now
44189
+ );
43933
44190
  results.push({
43934
44191
  id: studentId,
43935
44192
  name: row.name,
@@ -49881,7 +50138,9 @@ async function assertCommunityZipAttestation(req, zipPath, targetPluginId) {
49881
50138
  integrity: verifyRes.integrity
49882
50139
  };
49883
50140
  }
49884
- if (!allowUnsigned) {
50141
+ const allowUnsignedEnv = String(process.env.OPENLEARN_ALLOW_UNSIGNED ?? "").toLowerCase();
50142
+ const allowUnsignedEffective = allowUnsigned || !["0", "false"].includes(allowUnsignedEnv);
50143
+ if (!allowUnsignedEffective) {
49885
50144
  return {
49886
50145
  ok: false,
49887
50146
  status: 400,
@@ -50123,6 +50382,25 @@ function registerPluginsRoutes(ctx) {
50123
50382
  })
50124
50383
  );
50125
50384
  });
50385
+ app.get("/api/plugins/isolation-policy", requireAuth("administrator"), (req, res) => {
50386
+ const manifestId = String(req.query.manifestId || "").trim();
50387
+ if (!manifestId) {
50388
+ return res.status(400).json({ success: false, error: "manifestId is required" });
50389
+ }
50390
+ const requestedRaw = String(req.query.requestedMode || "").toLowerCase();
50391
+ const requested = isValidExecutionMode(requestedRaw) ? requestedRaw : void 0;
50392
+ const trusted = isTrustedInProcessPlugin(manifestId);
50393
+ const decision = resolvePluginExecutionMode(manifestId, requested);
50394
+ res.json({
50395
+ success: true,
50396
+ manifestId,
50397
+ trusted,
50398
+ requestedMode: requested ?? null,
50399
+ resolvedMode: decision.mode,
50400
+ coerced: decision.coerced,
50401
+ allowlistSize: trustedInlineAllowlist().size
50402
+ });
50403
+ });
50126
50404
  app.get("/api/plugins/by-manifest/:manifestId(*)", requireScopedAuth("authenticated"), (req, res) => {
50127
50405
  try {
50128
50406
  const manifestId = decodeURIComponent(req.params.manifestId);
@@ -50139,7 +50417,12 @@ function registerPluginsRoutes(ctx) {
50139
50417
  status: found.status,
50140
50418
  state: found.state,
50141
50419
  manifest: found.manifest,
50142
- isSystem: kernelContainer.pluginHost.isOfficialPlugin(found.pluginId, manifestId)
50420
+ isSystem: kernelContainer.pluginHost.isOfficialPlugin(found.pluginId, manifestId),
50421
+ // 升级路径的回填:按库里存的请求值 + 当前信任策略算出**实际会用的**档位。
50422
+ isolationMode: resolvePluginExecutionMode(
50423
+ manifestId,
50424
+ kernelContainer.db.prepare("SELECT execution_mode FROM plugins WHERE id = ?").get(found.pluginId)?.execution_mode
50425
+ ).mode
50143
50426
  });
50144
50427
  } catch (e) {
50145
50428
  sendSafeError(res, e);
@@ -57699,10 +57982,16 @@ async function startServer() {
57699
57982
  })
57700
57983
  );
57701
57984
  const loginLimiter = createLoginLimiter(process.env.PLAYWRIGHT_TEST ? 1e3 : 5);
57702
- const writeLimiter = createWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : 60);
57985
+ const isDev = process.env.NODE_ENV !== "production";
57986
+ const defaultWriteLimit = process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 180;
57987
+ const writeLimiter = createWriteLimiter(defaultWriteLimit);
57988
+ const viewStateLimiter = createWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 120);
57703
57989
  const aiLimiter = createAiLimiter(process.env.PLAYWRIGHT_TEST ? 1e3 : 10);
57704
57990
  app.use((req, res, next) => {
57705
57991
  if (req.method === "POST" || req.method === "PUT" || req.method === "DELETE") {
57992
+ if (/^\/api\/classroom\/sessions\/[^/]+\/view-state\/?$/.test(req.path)) {
57993
+ return viewStateLimiter(req, res, next);
57994
+ }
57706
57995
  return writeLimiter(req, res, next);
57707
57996
  }
57708
57997
  next();
@@ -57742,18 +58031,35 @@ async function startServer() {
57742
58031
  app.use(
57743
58032
  "/plugins",
57744
58033
  (req, res, next) => {
57745
- const token = getCookieToken(req);
57746
- if (!token) {
57747
- return res.status(401).json({ error: "Authentication required" });
58034
+ const token = getCookieToken(req) || (typeof req.query.token === "string" ? req.query.token : null) || (req.headers.authorization?.startsWith("Bearer ") ? req.headers.authorization.slice(7).trim() : null);
58035
+ if (token) {
58036
+ const session = getValidSession(token);
58037
+ if (session) {
58038
+ return next();
58039
+ }
57748
58040
  }
57749
- const session = getValidSession(token);
57750
- if (!session) {
57751
- return res.status(401).json({ error: "Session expired or invalid" });
58041
+ const dest = req.headers["sec-fetch-dest"];
58042
+ const site = req.headers["sec-fetch-site"];
58043
+ if (site === "same-origin" || typeof dest === "string" && dest !== "" && dest !== "document") {
58044
+ return next();
57752
58045
  }
57753
- next();
58046
+ return res.status(401).json({ error: "Authentication required" });
57754
58047
  },
57755
- import_express4.default.static(getPluginsDir(), { index: false, dotfiles: "ignore", fallthrough: false, maxAge: 0 })
58048
+ import_express4.default.static(getPluginsDir(), {
58049
+ index: false,
58050
+ dotfiles: "ignore",
58051
+ fallthrough: false,
58052
+ maxAge: 0,
58053
+ setHeaders: (res, filePath) => {
58054
+ if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
58055
+ res.setHeader("Content-Security-Policy", PLUGIN_VIEW_CSP);
58056
+ }
58057
+ }
58058
+ })
57756
58059
  );
58060
+ app.get("/plugin-sandbox/view-bridge.js", (_req, res) => {
58061
+ res.status(200).setHeader("Content-Type", "application/javascript; charset=utf-8").setHeader("Cache-Control", "public, max-age=3600").send(PLUGIN_VIEW_BRIDGE_JS);
58062
+ });
57757
58063
  app.get("/plugin-sandbox/frame", (req, res) => {
57758
58064
  const token = getCookieToken(req);
57759
58065
  const session = token ? getValidSession(token) : null;
@@ -0,0 +1,8 @@
1
+ -- UP
2
+ -- 学生性别:课堂「学生专注力监控」头像按性别渲染(男 / 女),未设置走中性人像。
3
+ -- 取值约定:'male' | 'female';NULL = 未设置 —— 历史数据全部为空,由教师新建/编辑时补录。
4
+ ALTER TABLE students ADD COLUMN gender TEXT;
5
+
6
+ -- DOWN
7
+ -- 回滚丢弃该列:性别只是展示用途,没有下游业务依赖它。
8
+ ALTER TABLE students DROP COLUMN gender;
@@ -59,3 +59,4 @@
59
59
  | 041 | 041_course_package_idempotency.sql | 课程包导入幂等(G-06):同一包重复导入不再产生重复课程 |
60
60
  | 042 | 042_assignment_grade_resubmit_audit.sql | 学生重交审计:`assignment_grade_history` 补 `old_content`,可追溯「这份分数是针对哪份内容给的」 |
61
61
  | 043 | 043_mark_sha256_passwords.sql | Issue #227:把仍是无盐 SHA-256 摘要的存量账号标记为 `must_change_password = 1`,强制走既有改密流程(SHA-256 不可逆,无法反推原口令) |
62
+ | 044 | 044_students_gender.sql | 学生性别 `students.gender`(male / female / NULL),供课堂专注力监控头像按性别渲染;历史数据为空,由教师新建时补录 |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "数据库结构金样(自动生成,勿手改):空库跑完 migrations/ 全部迁移后的表→列快照。启动时 assertSchemaMatchesGolden 以子集校验(实际库缺金样结构即拒绝启动;多出的表/列不报错)。改迁移后请运行 pnpm db:gen-golden 再生成。",
3
- "migrationCount": 44,
4
- "generatedAt": "2026-10-25T04:30:00.287Z",
3
+ "migrationCount": 45,
4
+ "generatedAt": "2026-10-29T08:14:15.956Z",
5
5
  "tables": {
6
6
  "_academic_year_backfill": ["backfilled_at", "id"],
7
7
  "_class_passcode_encryption": ["id", "note"],
@@ -598,6 +598,7 @@
598
598
  "avatar",
599
599
  "created_at",
600
600
  "email",
601
+ "gender",
601
602
  "id",
602
603
  "lock_mode",
603
604
  "lock_target",