openlearn-next 0.7.5 → 0.7.7

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 (66) 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-BLJAc5g8.js +1 -0
  4. package/dist/assets/{AdminDirectoryView-B6OAs5Rj.js → AdminDirectoryView-D6sMmobg.js} +1 -1
  5. package/dist/assets/{AssessmentCenter-B3f1DxSG.js → AssessmentCenter-3Cu5DE67.js} +1 -1
  6. package/dist/assets/{AssignmentFilePreview-DQbvbgnd.js → AssignmentFilePreview-DvBdEpB3.js} +2 -2
  7. package/dist/assets/{BatchPickerModal-CoOWloSS.js → BatchPickerModal-D1kEgc4L.js} +1 -1
  8. package/dist/assets/{ClassesView-Q56Kf4eL.js → ClassesView-DpC-D2y5.js} +10 -10
  9. package/dist/assets/{ClassroomCountdownWidget-C6luhy8P.js → ClassroomCountdownWidget-DFGC4XNs.js} +2 -2
  10. package/dist/assets/{CloudDriveModal-DLf4fbKR.js → CloudDriveModal-drWgNbEq.js} +1 -1
  11. package/dist/assets/ComputerLabView-BnfpX3XL.js +1 -0
  12. package/dist/assets/CourseManagement-DuVTm_hD.js +1 -0
  13. package/dist/assets/{CourseWizardModal-DAueHXcl.js → CourseWizardModal-BvQ4d1tN.js} +1 -1
  14. package/dist/assets/{CoursewareHubPanel-DLjH-UzF.js → CoursewareHubPanel-Ct9hTAOu.js} +1 -1
  15. package/dist/assets/{Dashboard-qMVadkd2.js → Dashboard-D3yj1oZj.js} +2 -2
  16. package/dist/assets/{ExportWeightModal-Dkh7OC8U.js → ExportWeightModal-CnbJuOav.js} +1 -1
  17. package/dist/assets/{HelpTour-BLuiS8rr.js → HelpTour-COz9zyR9.js} +1 -1
  18. package/dist/assets/{HelpView-CTF6B9ml.js → HelpView-PmIyAO8_.js} +2 -2
  19. package/dist/assets/{ImportLessonsModal-Ra-OF_Vo.js → ImportLessonsModal-CSlMGoet.js} +1 -1
  20. package/dist/assets/{ImportModal-H5s5MyZY.js → ImportModal-CCzFqKY0.js} +1 -1
  21. package/dist/assets/{InteractiveCoursewareViewer-BfHEDrCf.js → InteractiveCoursewareViewer-bgyDq7MF.js} +1 -1
  22. package/dist/assets/{InteractiveWhiteboard-KBzmiNBG.js → InteractiveWhiteboard-v8FqsMj3.js} +8 -8
  23. package/dist/assets/LazyCourseware-Bt31zjJT.js +2 -0
  24. package/dist/assets/LazyWhiteboard-Da0K2ZR1.js +2 -0
  25. package/dist/assets/LessonEditorView-DT3M9p_4.js +1 -0
  26. package/dist/assets/{LiveClassroomView-DezAlve2.js → LiveClassroomView-CrLRO8vI.js} +1 -1
  27. package/dist/assets/NotificationDetailModal-BgqbD3Gh.js +1 -0
  28. package/dist/assets/PluginView-CkICwR16.js +77 -0
  29. package/dist/assets/{ProcessLogsModal-_BwkmJVw.js → ProcessLogsModal-zO4lMuUa.js} +1 -1
  30. package/dist/assets/{QuizGeneratorModal-CTj2moWM.js → QuizGeneratorModal-D60-HJGk.js} +1 -1
  31. package/dist/assets/StudentAssignmentEvalPanel-C336iZSv.js +1 -0
  32. package/dist/assets/{StudentAssignmentView-D_EhblVQ.js → StudentAssignmentView--JlShlDQ.js} +1 -1
  33. package/dist/assets/{StudentLessonView-BHG50-4M.js → StudentLessonView-BgjjfEfD.js} +1 -1
  34. package/dist/assets/StudentView-Cjq1QOvV.js +2 -0
  35. package/dist/assets/{SystemResourceLibraryModal-C5y8H5Pf.js → SystemResourceLibraryModal-pPdl2NHf.js} +1 -1
  36. package/dist/assets/TeacherView-dLr1v6tY.js +2 -0
  37. package/dist/assets/{TimetableView-C8kdJlf7.js → TimetableView-CVAD4LYw.js} +1 -1
  38. package/dist/assets/{UTIF-4E-wdh3X.js → UTIF-p6fKAG_V.js} +1 -1
  39. package/dist/assets/index-BfKoCpo_.js +131 -0
  40. package/dist/assets/{index-DPky2leG.css → index-DIcDOunT.css} +1 -1
  41. package/dist/assets/{index.es-WXdI0L-F.js → index.es-B8wsP9dg.js} +1 -1
  42. package/dist/assets/{pluginParsers-DkiO-BBu.js → pluginParsers-D5PQwW_l.js} +1 -1
  43. package/dist/assets/{student-workspace-1yVTbWHC.js → student-workspace-C2tppsk6.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-DGdjNlGa.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-DmaV9d9t.js} +3 -3
  48. package/dist/assets/{vendor-pptx-EjNUtdws.js → vendor-pptx-CpYklkz2.js} +4 -4
  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 +5 -5
  53. package/dist/server.cjs +352 -51
  54. package/package.json +1 -1
  55. package/dist/assets/ActivityWorkspaceWidget-Dq4kjBS2.js +0 -1
  56. package/dist/assets/ComputerLabView-D9-Tk7yA.js +0 -1
  57. package/dist/assets/CourseManagement-DNSfyre-.js +0 -1
  58. package/dist/assets/LazyCourseware-BaZS8-i1.js +0 -2
  59. package/dist/assets/LazyWhiteboard-DGyln3JC.js +0 -2
  60. package/dist/assets/LessonEditorView-B5JXYyup.js +0 -1
  61. package/dist/assets/NotificationDetailModal-DbMMrIHQ.js +0 -1
  62. package/dist/assets/PluginView-CvIr-ANj.js +0 -77
  63. package/dist/assets/StudentAssignmentEvalPanel-DrfZT8Lr.js +0 -1
  64. package/dist/assets/StudentView-DKDhnJQx.js +0 -2
  65. package/dist/assets/TeacherView-CWDK3DOe.js +0 -2
  66. package/dist/assets/index-BBznBaN5.js +0 -131
package/dist/server.cjs CHANGED
@@ -3578,6 +3578,19 @@ var init_errors2 = __esm({
3578
3578
  function scopedId(scopeType, scopeId, key) {
3579
3579
  return `${scopeType}\0${scopeId}\0${key}`;
3580
3580
  }
3581
+ function getSharedState(db2, manifestId) {
3582
+ let byManifest = sharedStates.get(db2);
3583
+ if (!byManifest) {
3584
+ byManifest = /* @__PURE__ */ new Map();
3585
+ sharedStates.set(db2, byManifest);
3586
+ }
3587
+ let state = byManifest.get(manifestId);
3588
+ if (!state) {
3589
+ state = { values: /* @__PURE__ */ new Map(), instances: /* @__PURE__ */ new Set() };
3590
+ byManifest.set(manifestId, state);
3591
+ }
3592
+ return state;
3593
+ }
3581
3594
  function normalizeWriteScope(scope) {
3582
3595
  const scopeType = scope?.scopeType ?? "platform";
3583
3596
  const scopeId = scope?.scopeId?.trim() ?? "";
@@ -3586,21 +3599,29 @@ function normalizeWriteScope(scope) {
3586
3599
  }
3587
3600
  return { scopeType, scopeId: scopeType === "platform" ? "" : scopeId };
3588
3601
  }
3589
- var CONFIG_SCOPE_TYPES, ConfigService;
3602
+ var CONFIG_SCOPE_TYPES, sharedStates, instanceCleanup, ConfigService;
3590
3603
  var init_config_service = __esm({
3591
3604
  "packages/core/plugin-host/config-service.ts"() {
3592
3605
  "use strict";
3593
3606
  CONFIG_SCOPE_TYPES = ["platform", "school", "class"];
3607
+ sharedStates = /* @__PURE__ */ new WeakMap();
3608
+ instanceCleanup = new FinalizationRegistry(
3609
+ ({ state, ref }) => {
3610
+ state.instances.delete(ref);
3611
+ }
3612
+ );
3594
3613
  ConfigService = class _ConfigService {
3595
3614
  constructor(db2, manifest) {
3596
- /** In-memory cache (scoped key → value). `null` 表示「明确没有值」,避免反复查询。 */
3597
- this.values = /* @__PURE__ */ new Map();
3598
- /** Change listeners. */
3615
+ /** Change listeners(每个实例独立,广播时按实例分发)。 */
3599
3616
  this.listeners = /* @__PURE__ */ new Set();
3600
3617
  this.tableEnsured = false;
3601
3618
  this.db = db2;
3602
3619
  this.manifestId = manifest.id;
3603
3620
  this.schema = manifest.configuration?.properties ?? {};
3621
+ this.state = getSharedState(db2, manifest.id);
3622
+ const ref = new WeakRef(this);
3623
+ this.state.instances.add(ref);
3624
+ instanceCleanup.register(this, { state: this.state, ref });
3604
3625
  }
3605
3626
  static {
3606
3627
  this.KEY_PREFIX = "config:";
@@ -3663,21 +3684,21 @@ var init_config_service = __esm({
3663
3684
  }
3664
3685
  readScoped(key, scopeType, scopeId) {
3665
3686
  const cacheKey2 = scopedId(scopeType, scopeId, key);
3666
- if (this.values.has(cacheKey2)) {
3667
- const cached3 = this.values.get(cacheKey2);
3687
+ if (this.state.values.has(cacheKey2)) {
3688
+ const cached3 = this.state.values.get(cacheKey2);
3668
3689
  return cached3 === null ? { found: false } : { found: true, value: cached3 };
3669
3690
  }
3670
3691
  const row = this.queryScoped(key, scopeType, scopeId);
3671
3692
  if (!row) {
3672
- this.values.set(cacheKey2, null);
3693
+ this.state.values.set(cacheKey2, null);
3673
3694
  return { found: false };
3674
3695
  }
3675
3696
  try {
3676
3697
  const parsed = JSON.parse(row.value);
3677
- this.values.set(cacheKey2, parsed);
3698
+ this.state.values.set(cacheKey2, parsed);
3678
3699
  return { found: true, value: parsed };
3679
3700
  } catch {
3680
- this.values.set(cacheKey2, row.value);
3701
+ this.state.values.set(cacheKey2, row.value);
3681
3702
  return { found: true, value: row.value };
3682
3703
  }
3683
3704
  }
@@ -3693,21 +3714,21 @@ var init_config_service = __esm({
3693
3714
  }
3694
3715
  readLegacyPlatformValue(key) {
3695
3716
  const cacheKey2 = scopedId("platform", "", `legacy:${key}`);
3696
- if (this.values.has(cacheKey2)) {
3697
- const cached3 = this.values.get(cacheKey2);
3717
+ if (this.state.values.has(cacheKey2)) {
3718
+ const cached3 = this.state.values.get(cacheKey2);
3698
3719
  return cached3 === null ? { found: false } : { found: true, value: cached3 };
3699
3720
  }
3700
3721
  try {
3701
3722
  const row = this.db.prepare("SELECT value FROM plugin_storage WHERE plugin_id = ? AND key = ?").get(this.manifestId, _ConfigService.KEY_PREFIX + key);
3702
3723
  if (!row) {
3703
- this.values.set(cacheKey2, null);
3724
+ this.state.values.set(cacheKey2, null);
3704
3725
  return { found: false };
3705
3726
  }
3706
3727
  const parsed = JSON.parse(row.value);
3707
- this.values.set(cacheKey2, parsed);
3728
+ this.state.values.set(cacheKey2, parsed);
3708
3729
  return { found: true, value: parsed };
3709
3730
  } catch {
3710
- this.values.set(cacheKey2, null);
3731
+ this.state.values.set(cacheKey2, null);
3711
3732
  return { found: false };
3712
3733
  }
3713
3734
  }
@@ -3763,8 +3784,8 @@ var init_config_service = __esm({
3763
3784
  const { scopeType, scopeId } = normalizeWriteScope(scope);
3764
3785
  this.assertScopeAllowed(key, scopeType);
3765
3786
  const cacheKey2 = scopedId(scopeType, scopeId, key);
3766
- const previous = this.values.get(cacheKey2);
3767
- this.values.set(cacheKey2, value);
3787
+ const previous = this.state.values.get(cacheKey2);
3788
+ this.state.values.set(cacheKey2, value);
3768
3789
  try {
3769
3790
  this.ensureTable();
3770
3791
  this.db.prepare(
@@ -3775,16 +3796,32 @@ var init_config_service = __esm({
3775
3796
  ).run(this.manifestId, scopeType, scopeId, key, JSON.stringify(value), Date.now());
3776
3797
  } catch (e) {
3777
3798
  if (previous === void 0) {
3778
- this.values.delete(cacheKey2);
3799
+ this.state.values.delete(cacheKey2);
3779
3800
  } else {
3780
- this.values.set(cacheKey2, previous);
3801
+ this.state.values.set(cacheKey2, previous);
3781
3802
  }
3782
3803
  throw e;
3783
3804
  }
3784
- for (const listener of this.listeners) {
3785
- try {
3786
- listener(key, value, previous === null ? void 0 : previous);
3787
- } catch {
3805
+ this.broadcast(key, value, previous ?? void 0);
3806
+ }
3807
+ /**
3808
+ * 通知所有共享同一 db + manifest.id 的活跃实例(含自身)。
3809
+ *
3810
+ * 管理端通过临时实例写入时,运行中插件的 `onChange` 也因此能收到。
3811
+ * 已回收的实例在遍历时顺手清理。
3812
+ */
3813
+ broadcast(key, newValue, oldValue) {
3814
+ for (const ref of this.state.instances) {
3815
+ const instance = ref.deref();
3816
+ if (!instance) {
3817
+ this.state.instances.delete(ref);
3818
+ continue;
3819
+ }
3820
+ for (const listener of instance.listeners) {
3821
+ try {
3822
+ listener(key, newValue, oldValue);
3823
+ } catch {
3824
+ }
3788
3825
  }
3789
3826
  }
3790
3827
  }
@@ -3817,11 +3854,12 @@ var init_config_service = __esm({
3817
3854
  * 插件在 `activate()` 里通常会马上读配置,提前填充避免每条配置一次查询。
3818
3855
  */
3819
3856
  loadFromDB() {
3857
+ this.state.values.clear();
3820
3858
  try {
3821
3859
  const rows = this.db.prepare(`SELECT scope_type, scope_id, key, value FROM ${_ConfigService.TABLE} WHERE plugin_id = ?`).all(this.manifestId);
3822
3860
  for (const row of rows) {
3823
3861
  const scopeType = CONFIG_SCOPE_TYPES.includes(row.scope_type) ? row.scope_type : "platform";
3824
- this.values.set(scopedId(scopeType, row.scope_id ?? "", row.key), this.parseStored(row.value));
3862
+ this.state.values.set(scopedId(scopeType, row.scope_id ?? "", row.key), this.parseStored(row.value));
3825
3863
  }
3826
3864
  } catch {
3827
3865
  }
@@ -3830,7 +3868,7 @@ var init_config_service = __esm({
3830
3868
  const rows = this.db.prepare(`SELECT key, value FROM plugin_storage WHERE plugin_id = ? AND key LIKE ?`).all(this.manifestId, prefix + "%");
3831
3869
  for (const row of rows) {
3832
3870
  const key = row.key.slice(prefix.length);
3833
- this.values.set(scopedId("platform", "", `legacy:${key}`), this.parseStored(row.value));
3871
+ this.state.values.set(scopedId("platform", "", `legacy:${key}`), this.parseStored(row.value));
3834
3872
  }
3835
3873
  } catch {
3836
3874
  }
@@ -3842,9 +3880,9 @@ var init_config_service = __esm({
3842
3880
  return raw;
3843
3881
  }
3844
3882
  }
3845
- /** 测试/诊断:当前缓存里的作用域条目数。 */
3883
+ /** 测试/诊断:当前共享缓存里的作用域条目数。 */
3846
3884
  cachedEntryCount() {
3847
- return this.values.size;
3885
+ return this.state.values.size;
3848
3886
  }
3849
3887
  };
3850
3888
  }
@@ -4549,14 +4587,33 @@ function wrapProcessManager(processService, tracker, pluginId, processOwnerLooku
4549
4587
  })
4550
4588
  };
4551
4589
  }
4552
- function wrapActionRegistry(actionRegistry, tracker, pluginId) {
4590
+ function wrapActionRegistry(actionRegistry, tracker, pluginId, manifestId) {
4591
+ const prefix = manifestId || pluginId;
4592
+ const isKernel = prefix.startsWith("@openlearn/");
4553
4593
  return {
4554
4594
  register: createSafeFunction((descriptor) => {
4555
- return Promise.resolve(actionRegistry.register(descriptor)).then(() => {
4595
+ const commandType = descriptor.commandType;
4596
+ const prefixed = isKernel || !commandType || commandType.startsWith(`${prefix}.`) ? commandType : `${prefix}.${commandType}`;
4597
+ const effectiveDescriptor = prefixed !== commandType ? { ...descriptor, commandType: prefixed } : descriptor;
4598
+ return Promise.resolve(actionRegistry.register(effectiveDescriptor)).then(() => {
4599
+ if (prefixed !== commandType) {
4600
+ Promise.resolve(
4601
+ actionRegistry.register({
4602
+ ...descriptor,
4603
+ id: `${descriptor.id}__raw_alias`,
4604
+ exposeToAgent: false
4605
+ })
4606
+ ).catch(() => {
4607
+ });
4608
+ }
4556
4609
  tracker.track(pluginId, {
4557
4610
  dispose: () => {
4558
- Promise.resolve(actionRegistry.unregister(descriptor.id)).catch(() => {
4611
+ Promise.resolve(actionRegistry.unregister(effectiveDescriptor.id)).catch(() => {
4559
4612
  });
4613
+ if (prefixed !== commandType) {
4614
+ Promise.resolve(actionRegistry.unregister(`${descriptor.id}__raw_alias`)).catch(() => {
4615
+ });
4616
+ }
4560
4617
  }
4561
4618
  });
4562
4619
  });
@@ -4797,7 +4854,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4797
4854
  // 可选方法,mock 或旧部署可缺省 —— 缺省时 isOwnProcess 退化为「不拦截」,与旧行为一致。
4798
4855
  typeof processService.getProcessOwner === "function" ? (processId) => processService.getProcessOwner(processId) : void 0
4799
4856
  );
4800
- const wrappedActionRegistry = wrapActionRegistry(actionRegistryService, tracker, pluginId);
4857
+ const wrappedActionRegistry = wrapActionRegistry(actionRegistryService, tracker, pluginId, manifest.id);
4801
4858
  const wrappedCapability = wrapCapability(capabilityService);
4802
4859
  const wrappedStorage = wrapStorage(storageService, db2, manifest.id);
4803
4860
  const wrappedAI = wrapAI(aiService, pluginId, tracker);
@@ -8442,10 +8499,28 @@ var init_service_host = __esm({
8442
8499
  const actionRegistry = service;
8443
8500
  if (msg.method === "register") {
8444
8501
  const [descriptor] = effectiveArgs;
8445
- await actionRegistry.register(descriptor);
8446
- this.registeredActionIds.add(descriptor.id);
8447
- if (typeof descriptor?.commandType === "string") {
8448
- this.registeredActionCommandTypes.set(descriptor.id, descriptor.commandType);
8502
+ const pluginId = this.pluginId || this.pluginActorId.replace(/^plugin:/, "");
8503
+ const isKernel = pluginId.startsWith("@openlearn/");
8504
+ const rawCommandType = descriptor.commandType;
8505
+ const prefixed = isKernel || !rawCommandType || rawCommandType.startsWith(`${pluginId}.`) ? rawCommandType : resolvePluginCommandType(rawCommandType, pluginId);
8506
+ const effectiveDescriptor = prefixed !== rawCommandType ? { ...descriptor, commandType: prefixed } : descriptor;
8507
+ await actionRegistry.register(effectiveDescriptor);
8508
+ this.registeredActionIds.add(effectiveDescriptor.id);
8509
+ if (typeof effectiveDescriptor?.commandType === "string") {
8510
+ this.registeredActionCommandTypes.set(effectiveDescriptor.id, effectiveDescriptor.commandType);
8511
+ }
8512
+ if (prefixed !== rawCommandType) {
8513
+ try {
8514
+ const aliasId = `${descriptor.id}__raw_alias`;
8515
+ await actionRegistry.register({
8516
+ ...descriptor,
8517
+ id: aliasId,
8518
+ exposeToAgent: false
8519
+ });
8520
+ this.registeredActionIds.add(aliasId);
8521
+ this.registeredActionCommandTypes.set(aliasId, rawCommandType);
8522
+ } catch {
8523
+ }
8449
8524
  }
8450
8525
  transport.postMessage({
8451
8526
  type: "result",
@@ -8456,8 +8531,14 @@ var init_service_host = __esm({
8456
8531
  } else if (msg.method === "unregister") {
8457
8532
  const [actionId] = msg.args;
8458
8533
  await actionRegistry.unregister(actionId);
8534
+ try {
8535
+ await actionRegistry.unregister(`${actionId}__raw_alias`);
8536
+ } catch {
8537
+ }
8459
8538
  this.registeredActionIds.delete(actionId);
8539
+ this.registeredActionIds.delete(`${actionId}__raw_alias`);
8460
8540
  this.registeredActionCommandTypes.delete(actionId);
8541
+ this.registeredActionCommandTypes.delete(`${actionId}__raw_alias`);
8461
8542
  transport.postMessage({
8462
8543
  type: "result",
8463
8544
  invokeId: msg.invokeId,
@@ -15190,6 +15271,26 @@ var BuiltinPlugin = {
15190
15271
  "page_meta"
15191
15272
  // 分页与大纲元数据
15192
15273
  ];
15274
+ const isAllowedElementType = (type) => {
15275
+ if (!type || typeof type !== "string") return false;
15276
+ if (KNOWN_ELEMENT_TYPES2.includes(type)) return true;
15277
+ if (type.includes("/") || type.startsWith("plugin:") || type.startsWith("ext-") || type.startsWith("plugin-")) {
15278
+ return true;
15279
+ }
15280
+ try {
15281
+ const reg = pluginHost?.getContributionRegistry?.();
15282
+ if (reg) {
15283
+ const paletteItems = reg.getBySlot("editor.palette_item") || [];
15284
+ if (paletteItems.some((item) => item.type === type || item.id === type)) return true;
15285
+ const renderers = reg.getBySlot("whiteboard.renderer") || [];
15286
+ if (renderers.some((item) => item.targetType === type || item.id === type || item.elementType === type)) {
15287
+ return true;
15288
+ }
15289
+ }
15290
+ } catch {
15291
+ }
15292
+ return false;
15293
+ };
15193
15294
  await actionRegistry.register({
15194
15295
  id: "core-whiteboard-draw",
15195
15296
  commandType: drawWhiteboardCmdType,
@@ -15213,8 +15314,8 @@ var BuiltinPlugin = {
15213
15314
  await commandBus.registerHandler(drawWhiteboardCmdType, {
15214
15315
  async execute(command) {
15215
15316
  const payload = command.payload;
15216
- if (!KNOWN_ELEMENT_TYPES2.includes(payload.type)) {
15217
- 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`;
15317
+ if (!isAllowedElementType(payload.type)) {
15318
+ 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`;
15218
15319
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${payload.type}"\u3002${hint}`);
15219
15320
  }
15220
15321
  const elementId = (0, import_uuid7.v7)();
@@ -15456,7 +15557,7 @@ var BuiltinPlugin = {
15456
15557
  );
15457
15558
  const batchInsert = db2.transaction(() => {
15458
15559
  for (const el of elements) {
15459
- if (!KNOWN_ELEMENT_TYPES2.includes(el.type)) {
15560
+ if (!isAllowedElementType(el.type)) {
15460
15561
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${el.type}"\u3002\u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}`);
15461
15562
  }
15462
15563
  const elementId = (0, import_uuid7.v7)();
@@ -18989,6 +19090,19 @@ function trustedInlineAllowlist(env = process.env) {
18989
19090
  (env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
18990
19091
  );
18991
19092
  }
19093
+ function matchesTrustedInlinePattern(pattern, manifestId) {
19094
+ if (!manifestId) return false;
19095
+ if (pattern === "*") return true;
19096
+ if (pattern.endsWith("*")) return manifestId.startsWith(pattern.slice(0, -1));
19097
+ return pattern === manifestId;
19098
+ }
19099
+ function isInTrustedInlineAllowlist(manifestId, env = process.env) {
19100
+ if (!manifestId) return false;
19101
+ for (const pattern of trustedInlineAllowlist(env)) {
19102
+ if (matchesTrustedInlinePattern(pattern, manifestId)) return true;
19103
+ }
19104
+ return false;
19105
+ }
18992
19106
  function builtinPluginsDir() {
18993
19107
  try {
18994
19108
  const url = import_meta5.url;
@@ -19011,7 +19125,7 @@ function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir())
19011
19125
  }
19012
19126
  function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
19013
19127
  if (isInsideBuiltinPluginsDir(sourcePath)) return true;
19014
- if (manifestId && trustedInlineAllowlist(env).has(manifestId)) return true;
19128
+ if (isInTrustedInlineAllowlist(manifestId, env)) return true;
19015
19129
  if (env.VITEST && env.OPENLEARN_ENFORCE_PLUGIN_ISOLATION !== "1") return true;
19016
19130
  return false;
19017
19131
  }
@@ -20999,7 +21113,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
20999
21113
  var import_node_path13 = __toESM(require("node:path"), 1);
21000
21114
  var import_node_url5 = require("node:url");
21001
21115
  var import_meta7 = {};
21002
- var FALLBACK_VERSION = "0.7.5";
21116
+ var FALLBACK_VERSION = "0.7.7";
21003
21117
  function readPlatformVersion() {
21004
21118
  const bases = [];
21005
21119
  if (typeof __dirname !== "undefined") bases.push(__dirname);
@@ -32805,11 +32919,17 @@ function validateStartupEnv(env = process.env) {
32805
32919
  severity: "warning",
32806
32920
  message: "OPENLEARN_TRUSTED_INLINE_PLUGINS \u53EA\u542B\u7A7A\u767D/\u9017\u53F7\uFF0C\u7B49\u540C\u4E8E\u672A\u914D\u7F6E\u3002"
32807
32921
  });
32922
+ } else if (ids.includes("*")) {
32923
+ warnings.push({
32924
+ key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
32925
+ severity: "warning",
32926
+ 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"
32927
+ });
32808
32928
  } else if (isProduction) {
32809
32929
  warnings.push({
32810
32930
  key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
32811
32931
  severity: "warning",
32812
- 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`
32932
+ 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`
32813
32933
  });
32814
32934
  }
32815
32935
  }
@@ -33633,6 +33753,7 @@ var normalizeToolSchema = (schema) => {
33633
33753
  };
33634
33754
  var buildOpenAITools = (callerRole) => {
33635
33755
  let actions = kernelContainer.actionRegistry.getAllActions();
33756
+ actions = actions.filter((action) => action.exposeToAgent !== false);
33636
33757
  if (callerRole === "student") {
33637
33758
  actions = actions.filter((action) => {
33638
33759
  const cap = action.capabilityRequired || "";
@@ -36042,6 +36163,142 @@ post({ type: 'ready' });
36042
36163
  </script>
36043
36164
  </body></html>`;
36044
36165
 
36166
+ // server/utils/plugin-view-bridge.ts
36167
+ 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:;";
36168
+ var PLUGIN_VIEW_BRIDGE_JS = `(function () {
36169
+ 'use strict';
36170
+ const CHANNEL = 'ol-plugin-view';
36171
+ let targetOrigin = '*';
36172
+ const pending = new Map();
36173
+ const initCallbacks = [];
36174
+ const contextUpdateCallbacks = [];
36175
+ let currentContext = null;
36176
+
36177
+ function post(msg) {
36178
+ if (window.parent && window.parent !== window) {
36179
+ window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
36180
+ }
36181
+ }
36182
+
36183
+ function call(op, args) {
36184
+ const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
36185
+ return new Promise(function (resolve, reject) {
36186
+ pending.set(callId, { resolve: resolve, reject: reject });
36187
+ post({ type: 'call', callId: callId, op: op, args: args || [] });
36188
+ });
36189
+ }
36190
+
36191
+ window.addEventListener('message', function (event) {
36192
+ const data = event.data;
36193
+ if (!data || data.channel !== CHANNEL) return;
36194
+ if (event.source !== window.parent) return;
36195
+
36196
+ if (data.type === 'init') {
36197
+ currentContext = data.payload || {};
36198
+ targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
36199
+ for (let i = 0; i < initCallbacks.length; i++) {
36200
+ try {
36201
+ initCallbacks[i](currentContext);
36202
+ } catch (e) {
36203
+ console.error('[OpenLearnPluginBridge] init callback error:', e);
36204
+ }
36205
+ }
36206
+ return;
36207
+ }
36208
+
36209
+ if (data.type === 'context-update') {
36210
+ currentContext = Object.assign({}, currentContext, data.payload || {});
36211
+ for (let i = 0; i < contextUpdateCallbacks.length; i++) {
36212
+ try {
36213
+ contextUpdateCallbacks[i](currentContext);
36214
+ } catch (e) {
36215
+ console.error('[OpenLearnPluginBridge] contextUpdate callback error:', e);
36216
+ }
36217
+ }
36218
+ return;
36219
+ }
36220
+
36221
+ if (data.type === 'reply') {
36222
+ const waiter = pending.get(data.callId);
36223
+ if (!waiter) return;
36224
+ pending.delete(data.callId);
36225
+ if (data.ok) {
36226
+ waiter.resolve(data.value);
36227
+ } else {
36228
+ waiter.reject(new Error(data.error || 'bridge call failed'));
36229
+ }
36230
+ return;
36231
+ }
36232
+ });
36233
+
36234
+ const bridge = {
36235
+ CHANNEL: CHANNEL,
36236
+ onInit: function (callback) {
36237
+ if (typeof callback === 'function') {
36238
+ if (currentContext) {
36239
+ try {
36240
+ callback(currentContext);
36241
+ } catch (e) {
36242
+ console.error('[OpenLearnPluginBridge] onInit callback error:', e);
36243
+ }
36244
+ }
36245
+ initCallbacks.push(callback);
36246
+ }
36247
+ return bridge;
36248
+ },
36249
+ onContextUpdate: function (callback) {
36250
+ if (typeof callback === 'function') {
36251
+ contextUpdateCallbacks.push(callback);
36252
+ }
36253
+ return bridge;
36254
+ },
36255
+ getContext: function () {
36256
+ return currentContext;
36257
+ },
36258
+ ui: {
36259
+ toast: function (message, type) {
36260
+ return call('ui.toast', [message, type || 'info']);
36261
+ },
36262
+ showToast: function (message, type) {
36263
+ return call('ui.toast', [message, type || 'info']);
36264
+ },
36265
+ resize: function (height) {
36266
+ post({ type: 'resize', height: Number(height) || 0 });
36267
+ },
36268
+ },
36269
+ invokeCommand: function (type, payload) {
36270
+ return call('command.invoke', [type, payload]);
36271
+ },
36272
+ api: {
36273
+ get: function (path) {
36274
+ return call('api.get', [path]);
36275
+ },
36276
+ post: function (path, body) {
36277
+ return call('api.post', [path, body]);
36278
+ },
36279
+ del: function (path) {
36280
+ return call('api.del', [path]);
36281
+ },
36282
+ },
36283
+ storage: {
36284
+ get: function (key) {
36285
+ return call('storage.get', [key]);
36286
+ },
36287
+ set: function (key, value) {
36288
+ return call('storage.set', [key, value]);
36289
+ },
36290
+ delete: function (key) {
36291
+ return call('storage.delete', [key]);
36292
+ },
36293
+ },
36294
+ };
36295
+
36296
+ window.OpenLearnPluginBridge = bridge;
36297
+ // \u901A\u77E5\u5BBF\u4E3B\u89C6\u56FE\u6C99\u7BB1 iframe \u5DF2\u5C31\u7EEA
36298
+ post({ type: 'ready' });
36299
+ })();
36300
+ `;
36301
+
36045
36302
  // packages/core/bootstrap/adapter/bootstrap-registration.ts
36046
36303
  var BootstrapRegistration = class {
36047
36304
  static registerConfiguration(builder, context) {
@@ -42477,7 +42734,8 @@ async function importCoursePackage(pkg, options) {
42477
42734
  let elements = [];
42478
42735
  try {
42479
42736
  elements = pkg.elements.map((element) => {
42480
- if (!KNOWN_ELEMENT_TYPES.has(element.type)) {
42737
+ 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-"));
42738
+ if (!isAllowed) {
42481
42739
  throw new Error(`\u8BFE\u7A0B\u5305\u5305\u542B\u4E0D\u652F\u6301\u7684\u767D\u677F\u5143\u7D20\u7C7B\u578B\uFF1A${element.type}`);
42482
42740
  }
42483
42741
  const data = resolveAssetPlaceholders(element.data, materialized);
@@ -49843,7 +50101,9 @@ async function assertCommunityZipAttestation(req, zipPath, targetPluginId) {
49843
50101
  integrity: verifyRes.integrity
49844
50102
  };
49845
50103
  }
49846
- if (!allowUnsigned) {
50104
+ const allowUnsignedEnv = String(process.env.OPENLEARN_ALLOW_UNSIGNED ?? "").toLowerCase();
50105
+ const allowUnsignedEffective = allowUnsigned || !["0", "false"].includes(allowUnsignedEnv);
50106
+ if (!allowUnsignedEffective) {
49847
50107
  return {
49848
50108
  ok: false,
49849
50109
  status: 400,
@@ -50085,6 +50345,25 @@ function registerPluginsRoutes(ctx) {
50085
50345
  })
50086
50346
  );
50087
50347
  });
50348
+ app.get("/api/plugins/isolation-policy", requireAuth("administrator"), (req, res) => {
50349
+ const manifestId = String(req.query.manifestId || "").trim();
50350
+ if (!manifestId) {
50351
+ return res.status(400).json({ success: false, error: "manifestId is required" });
50352
+ }
50353
+ const requestedRaw = String(req.query.requestedMode || "").toLowerCase();
50354
+ const requested = isValidExecutionMode(requestedRaw) ? requestedRaw : void 0;
50355
+ const trusted = isTrustedInProcessPlugin(manifestId);
50356
+ const decision = resolvePluginExecutionMode(manifestId, requested);
50357
+ res.json({
50358
+ success: true,
50359
+ manifestId,
50360
+ trusted,
50361
+ requestedMode: requested ?? null,
50362
+ resolvedMode: decision.mode,
50363
+ coerced: decision.coerced,
50364
+ allowlistSize: trustedInlineAllowlist().size
50365
+ });
50366
+ });
50088
50367
  app.get("/api/plugins/by-manifest/:manifestId(*)", requireScopedAuth("authenticated"), (req, res) => {
50089
50368
  try {
50090
50369
  const manifestId = decodeURIComponent(req.params.manifestId);
@@ -50101,7 +50380,12 @@ function registerPluginsRoutes(ctx) {
50101
50380
  status: found.status,
50102
50381
  state: found.state,
50103
50382
  manifest: found.manifest,
50104
- isSystem: kernelContainer.pluginHost.isOfficialPlugin(found.pluginId, manifestId)
50383
+ isSystem: kernelContainer.pluginHost.isOfficialPlugin(found.pluginId, manifestId),
50384
+ // 升级路径的回填:按库里存的请求值 + 当前信任策略算出**实际会用的**档位。
50385
+ isolationMode: resolvePluginExecutionMode(
50386
+ manifestId,
50387
+ kernelContainer.db.prepare("SELECT execution_mode FROM plugins WHERE id = ?").get(found.pluginId)?.execution_mode
50388
+ ).mode
50105
50389
  });
50106
50390
  } catch (e) {
50107
50391
  sendSafeError(res, e);
@@ -57704,18 +57988,35 @@ async function startServer() {
57704
57988
  app.use(
57705
57989
  "/plugins",
57706
57990
  (req, res, next) => {
57707
- const token = getCookieToken(req);
57708
- if (!token) {
57709
- return res.status(401).json({ error: "Authentication required" });
57991
+ 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);
57992
+ if (token) {
57993
+ const session = getValidSession(token);
57994
+ if (session) {
57995
+ return next();
57996
+ }
57710
57997
  }
57711
- const session = getValidSession(token);
57712
- if (!session) {
57713
- return res.status(401).json({ error: "Session expired or invalid" });
57998
+ const dest = req.headers["sec-fetch-dest"];
57999
+ const site = req.headers["sec-fetch-site"];
58000
+ if (site === "same-origin" || typeof dest === "string" && dest !== "" && dest !== "document") {
58001
+ return next();
57714
58002
  }
57715
- next();
58003
+ return res.status(401).json({ error: "Authentication required" });
57716
58004
  },
57717
- import_express4.default.static(getPluginsDir(), { index: false, dotfiles: "ignore", fallthrough: false, maxAge: 0 })
58005
+ import_express4.default.static(getPluginsDir(), {
58006
+ index: false,
58007
+ dotfiles: "ignore",
58008
+ fallthrough: false,
58009
+ maxAge: 0,
58010
+ setHeaders: (res, filePath) => {
58011
+ if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
58012
+ res.setHeader("Content-Security-Policy", PLUGIN_VIEW_CSP);
58013
+ }
58014
+ }
58015
+ })
57718
58016
  );
58017
+ app.get("/plugin-sandbox/view-bridge.js", (_req, res) => {
58018
+ res.status(200).setHeader("Content-Type", "application/javascript; charset=utf-8").setHeader("Cache-Control", "public, max-age=3600").send(PLUGIN_VIEW_BRIDGE_JS);
58019
+ });
57719
58020
  app.get("/plugin-sandbox/frame", (req, res) => {
57720
58021
  const token = getCookieToken(req);
57721
58022
  const session = token ? getValidSession(token) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openlearn-next",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "private": false,
5
5
  "description": "A plugin-driven Educational OS / LMS platform with AI agent teaching assistant",
6
6
  "keywords": [