openlearn-next 0.3.21 → 0.3.22
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.
- package/README.md +161 -38
- package/dist/assets/{AdminDirectoryView-CGo33RNh.js → AdminDirectoryView-D-AOtp1o.js} +1 -1
- package/dist/assets/{BatchPickerModal-CPmu_uF8.js → BatchPickerModal-C9FH3J57.js} +1 -1
- package/dist/assets/{ClassesView-TVobZ_Vs.js → ClassesView-DTuyCvYd.js} +1 -1
- package/dist/assets/{CloudDriveModal-ul9v6wHK.js → CloudDriveModal-Bi-aMuax.js} +1 -1
- package/dist/assets/{ComputerLabView-DGha_J8B.js → ComputerLabView-DWMtMSHV.js} +1 -1
- package/dist/assets/{CourseManagement-DFQsUxE1.js → CourseManagement-CeelAyst.js} +1 -1
- package/dist/assets/{CourseWizardModal-D92teHMD.js → CourseWizardModal-m7TDgcB2.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-CEK_QEc5.js → CoursewareHubPanel-C4LQjt4v.js} +1 -1
- package/dist/assets/{Dashboard-CY138ODd.js → Dashboard-CiBtG7Ge.js} +2 -2
- package/dist/assets/{ExportWeightModal-BvoODxwp.js → ExportWeightModal-C4d-0uTM.js} +1 -1
- package/dist/assets/{HelpTour-Dlc1vvy3.js → HelpTour-00eT17Vg.js} +1 -1
- package/dist/assets/{HelpView-CHkXbtfB.js → HelpView-B9Pny6u9.js} +4 -4
- package/dist/assets/{ImportLessonsModal-YuTV3mnv.js → ImportLessonsModal-hKRuw9p4.js} +1 -1
- package/dist/assets/{ImportModal-B2acbUT8.js → ImportModal-CVXHZ1Ei.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-BBeuViik.js → InteractiveCoursewareViewer-DMMAs2k_.js} +1 -1
- package/dist/assets/InteractiveWhiteboard-jpweQ094.js +77 -0
- package/dist/assets/{LazyCourseware-Du9MgZyH.js → LazyCourseware-DVgyrg3r.js} +2 -2
- package/dist/assets/{LazyWhiteboard-CpSzdON0.js → LazyWhiteboard-BSzsAPXC.js} +2 -2
- package/dist/assets/{LessonEditorView-D9YYZQyP.js → LessonEditorView-D4g48fr5.js} +1 -1
- package/dist/assets/LiveClassroomView-HQGhVwd_.js +1 -0
- package/dist/assets/{NotificationDetailModal-BthQNwyd.js → NotificationDetailModal-1MOkBDr_.js} +1 -1
- package/dist/assets/{PluginView-CQLEVURc.js → PluginView-D8cQ8mLM.js} +3 -3
- package/dist/assets/{ProcessLogsModal-MtK-Pe4R.js → ProcessLogsModal-gZ9LILYM.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-Bp4pUx-H.js → QuizGeneratorModal-BFCJe1cm.js} +1 -1
- package/dist/assets/{StudentAssignmentView-B-jN_m_B.js → StudentAssignmentView-C34nleZT.js} +1 -1
- package/dist/assets/{StudentLessonView-8-zC7BLx.js → StudentLessonView-DNbu2o04.js} +1 -1
- package/dist/assets/{StudentPreviewModal-DZdcKy1x.js → StudentPreviewModal-BXbKzQgo.js} +1 -1
- package/dist/assets/{StudentView-Cg5G4Guz.js → StudentView-BF2WjF4_.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-CJ16MGxn.js → SystemResourceLibraryModal-Ce-Dis6M.js} +1 -1
- package/dist/assets/TeacherView-BySyCiZG.js +2 -0
- package/dist/assets/{TimetableView-Do_LWpV2.js → TimetableView-0VaTfpz3.js} +4 -4
- package/dist/assets/index-C7jMgry5.js +216 -0
- package/dist/assets/index-huP36ZNf.css +1 -0
- package/dist/assets/{vendor-icons-B2DRJfZU.js → vendor-icons-30iiuCQb.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/server.cjs +2689 -341
- package/package.json +21 -22
- package/dist/assets/InteractiveWhiteboard-BASy3Ak8.js +0 -78
- package/dist/assets/LiveClassroomView-B2lKKiZK.js +0 -1
- package/dist/assets/TeacherView-_n9QWf7y.js +0 -2
- package/dist/assets/index-BApV4fuy.js +0 -202
- package/dist/assets/index-Ch_mD6B0.css +0 -1
package/dist/server.cjs
CHANGED
|
@@ -343,7 +343,7 @@ var init_token = __esm({
|
|
|
343
343
|
});
|
|
344
344
|
|
|
345
345
|
// packages/core/di/interfaces.ts
|
|
346
|
-
var ICommandBusServiceToken4, IEventBusServiceToken3, IActionRegistryServiceToken4, ICapabilityServiceToken, IProcessServiceToken2, IStorageServiceToken, IAIServiceToken, IDatabaseToken4, IPluginHostToken, IPluginLifecycleManagerToken, IPluginDistributionManagerToken, IPluginRuntimeCompositionToken, IUnifiedExtensionRegistryToken, IPluginCapabilityGatewayToken, ICapabilityRegistryToken, IAuthSessionBridgeToken, ISemesterGradeServiceToken, IPointsDimensionRegistryToken, IPointsLedgerServiceToken, ILessonEngineServiceToken, IClassroomRuntimeServiceToken, IPresenceEngineServiceToken, ITeachingCollaborationServiceToken, ILearningAnalyticsServiceToken, IAICapabilityServiceToken, ICapabilityRuntimeServiceToken, ICapabilityGovernanceServiceToken, IPlatformServiceRegistryToken;
|
|
346
|
+
var ICommandBusServiceToken4, IEventBusServiceToken3, IActionRegistryServiceToken4, ICapabilityServiceToken, IProcessServiceToken2, IStorageServiceToken, IAIServiceToken, IDatabaseToken4, IPluginHostToken, IPluginLifecycleManagerToken, IPluginDistributionManagerToken, IPluginRuntimeCompositionToken, IUnifiedExtensionRegistryToken, IPluginCapabilityGatewayToken, ICapabilityRegistryToken, IAuthSessionBridgeToken, ISemesterGradeServiceToken, IPointsDimensionRegistryToken, IPointsLedgerServiceToken, ILessonEngineServiceToken, IClassroomRuntimeServiceToken, IPresenceEngineServiceToken, ITeachingCollaborationServiceToken, ILearningAnalyticsServiceToken, IAICapabilityServiceToken, ICapabilityRuntimeServiceToken, ICapabilityGovernanceServiceToken, IPlatformServiceRegistryToken, ICoursewareRuntimeScriptRegistryToken;
|
|
347
347
|
var init_interfaces = __esm({
|
|
348
348
|
"packages/core/di/interfaces.ts"() {
|
|
349
349
|
init_token();
|
|
@@ -402,6 +402,9 @@ var init_interfaces = __esm({
|
|
|
402
402
|
IPlatformServiceRegistryToken = new Token(
|
|
403
403
|
"@openlearn/core:IPlatformServiceRegistryService"
|
|
404
404
|
);
|
|
405
|
+
ICoursewareRuntimeScriptRegistryToken = new Token(
|
|
406
|
+
"@openlearn/core:ICoursewareRuntimeScriptRegistry"
|
|
407
|
+
);
|
|
405
408
|
}
|
|
406
409
|
});
|
|
407
410
|
|
|
@@ -682,6 +685,93 @@ var init_ai_service = __esm({
|
|
|
682
685
|
}
|
|
683
686
|
});
|
|
684
687
|
|
|
688
|
+
// packages/core/di/courseware-runtime-script-registry.ts
|
|
689
|
+
var DEFAULT_PRIORITY, DEFAULT_POSITION, CoursewareRuntimeScriptRegistry;
|
|
690
|
+
var init_courseware_runtime_script_registry = __esm({
|
|
691
|
+
"packages/core/di/courseware-runtime-script-registry.ts"() {
|
|
692
|
+
DEFAULT_PRIORITY = 100;
|
|
693
|
+
DEFAULT_POSITION = "body-end";
|
|
694
|
+
CoursewareRuntimeScriptRegistry = class _CoursewareRuntimeScriptRegistry {
|
|
695
|
+
constructor() {
|
|
696
|
+
/** key = `${owner}::${id}`,保证不同插件可以使用相同的脚本 id */
|
|
697
|
+
this.scripts = /* @__PURE__ */ new Map();
|
|
698
|
+
}
|
|
699
|
+
static key(owner, id) {
|
|
700
|
+
return `${owner}::${id}`;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* 注册(或覆盖)一个课件运行时脚本。
|
|
704
|
+
*
|
|
705
|
+
* @param owner - 注册方,通常是 `ctx.pluginId`
|
|
706
|
+
* @param script - 脚本描述;同一 owner 下相同 id 视为覆盖
|
|
707
|
+
*/
|
|
708
|
+
register(owner, script) {
|
|
709
|
+
if (!owner) throw new Error("CoursewareRuntimeScriptRegistry.register: owner is required");
|
|
710
|
+
if (!script || !script.id) throw new Error("CoursewareRuntimeScriptRegistry.register: script.id is required");
|
|
711
|
+
if (typeof script.source !== "string" || !script.source.trim()) {
|
|
712
|
+
throw new Error("CoursewareRuntimeScriptRegistry.register: script.source must be a non-empty string");
|
|
713
|
+
}
|
|
714
|
+
this.scripts.set(_CoursewareRuntimeScriptRegistry.key(owner, script.id), {
|
|
715
|
+
...script,
|
|
716
|
+
owner,
|
|
717
|
+
priority: typeof script.priority === "number" ? script.priority : DEFAULT_PRIORITY,
|
|
718
|
+
position: script.position === "head" ? "head" : DEFAULT_POSITION
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
/** 注销某个 owner 下的单个脚本 */
|
|
722
|
+
unregister(owner, id) {
|
|
723
|
+
this.scripts.delete(_CoursewareRuntimeScriptRegistry.key(owner, id));
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 清理脚本。
|
|
727
|
+
*
|
|
728
|
+
* @param owner - 省略时清空所有 owner(谨慎使用,仅用于测试或平台重置)
|
|
729
|
+
*/
|
|
730
|
+
clear(owner) {
|
|
731
|
+
if (!owner) {
|
|
732
|
+
this.scripts.clear();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const prefix = `${owner}::`;
|
|
736
|
+
for (const key of [...this.scripts.keys()]) {
|
|
737
|
+
if (key.startsWith(prefix)) this.scripts.delete(key);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* 列出对目标课件生效的脚本(已按 priority 升序排序)。
|
|
742
|
+
*
|
|
743
|
+
* @param courseware - 目标课件的 `id` / `uuid`;省略时只返回「全局脚本」(两者均未限定者)
|
|
744
|
+
*/
|
|
745
|
+
list(courseware) {
|
|
746
|
+
const matched = [];
|
|
747
|
+
for (const entry of this.scripts.values()) {
|
|
748
|
+
if (this.matches(entry, courseware)) matched.push(entry);
|
|
749
|
+
}
|
|
750
|
+
return matched.map((entry, index) => ({ entry, index })).sort((a, b) => a.entry.priority - b.entry.priority || a.index - b.index).map((item) => item.entry);
|
|
751
|
+
}
|
|
752
|
+
/** 列出当前所有注册方(便于诊断与测试) */
|
|
753
|
+
listOwners() {
|
|
754
|
+
return [...new Set([...this.scripts.values()].map((entry) => entry.owner))];
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* 判定脚本是否对目标课件生效。
|
|
758
|
+
*
|
|
759
|
+
* - 未限定 `coursewareId` 也未限定 `coursewareUuid` → 全局脚本,对所有课件生效;
|
|
760
|
+
* - 限定了任一者 → 需要与传入的 `courseware` 对应字段精确相等(两者都命中任一即算命中)。
|
|
761
|
+
*/
|
|
762
|
+
matches(entry, courseware) {
|
|
763
|
+
const scopedById = typeof entry.coursewareId === "string" && entry.coursewareId.length > 0;
|
|
764
|
+
const scopedByUuid = typeof entry.coursewareUuid === "string" && entry.coursewareUuid.length > 0;
|
|
765
|
+
if (!scopedById && !scopedByUuid) return true;
|
|
766
|
+
if (!courseware) return false;
|
|
767
|
+
if (scopedById && courseware.id && entry.coursewareId === courseware.id) return true;
|
|
768
|
+
if (scopedByUuid && courseware.uuid && entry.coursewareUuid === courseware.uuid) return true;
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
685
775
|
// packages/core/plugin-host/types.ts
|
|
686
776
|
var PLUGIN_SHARED_MODULES;
|
|
687
777
|
var init_types = __esm({
|
|
@@ -873,9 +963,9 @@ var init_http_router = __esm({
|
|
|
873
963
|
/**
|
|
874
964
|
* 注册指定动词与路径的 Handler
|
|
875
965
|
*/
|
|
876
|
-
route(method,
|
|
966
|
+
route(method, path24, handler) {
|
|
877
967
|
const upperMethod = method.toUpperCase();
|
|
878
|
-
const normalized =
|
|
968
|
+
const normalized = path24.startsWith("/") ? path24 : "/" + path24;
|
|
879
969
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
880
970
|
this.routes.push({
|
|
881
971
|
method: upperMethod,
|
|
@@ -886,36 +976,36 @@ var init_http_router = __esm({
|
|
|
886
976
|
isStream: false
|
|
887
977
|
});
|
|
888
978
|
}
|
|
889
|
-
get(
|
|
890
|
-
this.route("GET",
|
|
979
|
+
get(path24, handler) {
|
|
980
|
+
this.route("GET", path24, handler);
|
|
891
981
|
}
|
|
892
|
-
post(
|
|
893
|
-
this.route("POST",
|
|
982
|
+
post(path24, handler) {
|
|
983
|
+
this.route("POST", path24, handler);
|
|
894
984
|
}
|
|
895
|
-
put(
|
|
896
|
-
this.route("PUT",
|
|
985
|
+
put(path24, handler) {
|
|
986
|
+
this.route("PUT", path24, handler);
|
|
897
987
|
}
|
|
898
|
-
patch(
|
|
899
|
-
this.route("PATCH",
|
|
988
|
+
patch(path24, handler) {
|
|
989
|
+
this.route("PATCH", path24, handler);
|
|
900
990
|
}
|
|
901
|
-
delete(
|
|
902
|
-
this.route("DELETE",
|
|
991
|
+
delete(path24, handler) {
|
|
992
|
+
this.route("DELETE", path24, handler);
|
|
903
993
|
}
|
|
904
994
|
stream(methodOrPath, pathOrHandler, maybeHandler) {
|
|
905
995
|
if (typeof pathOrHandler === "function") {
|
|
906
|
-
const
|
|
996
|
+
const path24 = methodOrPath;
|
|
907
997
|
const handler = pathOrHandler;
|
|
908
|
-
this.registerStream("GET",
|
|
909
|
-
this.registerStream("POST",
|
|
998
|
+
this.registerStream("GET", path24, handler);
|
|
999
|
+
this.registerStream("POST", path24, handler);
|
|
910
1000
|
} else {
|
|
911
1001
|
const method = methodOrPath.toUpperCase();
|
|
912
|
-
const
|
|
1002
|
+
const path24 = pathOrHandler;
|
|
913
1003
|
const handler = maybeHandler;
|
|
914
|
-
this.registerStream(method,
|
|
1004
|
+
this.registerStream(method, path24, handler);
|
|
915
1005
|
}
|
|
916
1006
|
}
|
|
917
|
-
registerStream(method,
|
|
918
|
-
const normalized =
|
|
1007
|
+
registerStream(method, path24, handler) {
|
|
1008
|
+
const normalized = path24.startsWith("/") ? path24 : "/" + path24;
|
|
919
1009
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
920
1010
|
this.routes.push({
|
|
921
1011
|
method: method.toUpperCase(),
|
|
@@ -929,9 +1019,9 @@ var init_http_router = __esm({
|
|
|
929
1019
|
/**
|
|
930
1020
|
* 检查指定动词与路径是否为流式路由
|
|
931
1021
|
*/
|
|
932
|
-
isStream(method,
|
|
1022
|
+
isStream(method, path24) {
|
|
933
1023
|
const upperMethod = method.toUpperCase();
|
|
934
|
-
const normalized =
|
|
1024
|
+
const normalized = path24.startsWith("/") ? path24 : "/" + path24;
|
|
935
1025
|
for (const entry of this.routes) {
|
|
936
1026
|
if (entry.method !== upperMethod) continue;
|
|
937
1027
|
if (entry.regex.test(normalized)) {
|
|
@@ -943,9 +1033,9 @@ var init_http_router = __esm({
|
|
|
943
1033
|
/**
|
|
944
1034
|
* 匹配路由并提取路由参数
|
|
945
1035
|
*/
|
|
946
|
-
match(method,
|
|
1036
|
+
match(method, path24) {
|
|
947
1037
|
const upperMethod = method.toUpperCase();
|
|
948
|
-
const normalized =
|
|
1038
|
+
const normalized = path24.startsWith("/") ? path24 : "/" + path24;
|
|
949
1039
|
for (const entry of this.routes) {
|
|
950
1040
|
if (entry.method !== upperMethod) continue;
|
|
951
1041
|
const match = normalized.match(entry.regex);
|
|
@@ -1122,6 +1212,21 @@ __export(context_builder_exports, {
|
|
|
1122
1212
|
createSafeFunction: () => createSafeFunction,
|
|
1123
1213
|
registerSharedModule: () => registerSharedModule
|
|
1124
1214
|
});
|
|
1215
|
+
function assertSafeSqlIdentifier(value, label) {
|
|
1216
|
+
if (typeof value !== "string" || !SQL_IDENTIFIER_PATTERN.test(value)) {
|
|
1217
|
+
throw new Error(`[SEC] Invalid SQL identifier for ${label}: ${JSON.stringify(value)}`);
|
|
1218
|
+
}
|
|
1219
|
+
return value;
|
|
1220
|
+
}
|
|
1221
|
+
function assertSafeCreateTableSchema(schema) {
|
|
1222
|
+
if (typeof schema !== "string" || schema.length === 0 || schema.length > 4e3) {
|
|
1223
|
+
throw new Error("[SEC] createTable schema must be a non-empty string (max 4000 chars)");
|
|
1224
|
+
}
|
|
1225
|
+
if (schema.includes(";")) {
|
|
1226
|
+
throw new Error('[SEC] createTable schema must not contain ";" (multiple statements are not allowed)');
|
|
1227
|
+
}
|
|
1228
|
+
return schema;
|
|
1229
|
+
}
|
|
1125
1230
|
function registerSharedModule(name, mod) {
|
|
1126
1231
|
sharedModules[name] = mod;
|
|
1127
1232
|
}
|
|
@@ -1452,15 +1557,19 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
1452
1557
|
const tablePrefix = `plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, "_")}_`;
|
|
1453
1558
|
const dbApi = {
|
|
1454
1559
|
async ensureTable(tableName, schema) {
|
|
1560
|
+
assertSafeSqlIdentifier(tableName, "ensureTable(tableName)");
|
|
1561
|
+
assertSafeCreateTableSchema(schema);
|
|
1455
1562
|
const fullName = tablePrefix + tableName;
|
|
1456
1563
|
db2.exec(`CREATE TABLE IF NOT EXISTS ${fullName} (${schema})`);
|
|
1457
1564
|
},
|
|
1458
1565
|
table(tableName) {
|
|
1566
|
+
assertSafeSqlIdentifier(tableName, "table(tableName)");
|
|
1459
1567
|
return tablePrefix + tableName;
|
|
1460
1568
|
},
|
|
1461
1569
|
async dropAllTables() {
|
|
1462
1570
|
const tables = db2.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ?`).all(tablePrefix + "%");
|
|
1463
1571
|
for (const t of tables) {
|
|
1572
|
+
if (!/^plugin_[A-Za-z0-9_]+$/.test(t.name)) continue;
|
|
1464
1573
|
db2.exec(`DROP TABLE IF EXISTS ${t.name}`);
|
|
1465
1574
|
}
|
|
1466
1575
|
},
|
|
@@ -1558,7 +1667,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
1558
1667
|
}
|
|
1559
1668
|
};
|
|
1560
1669
|
}
|
|
1561
|
-
var sharedModules;
|
|
1670
|
+
var SQL_IDENTIFIER_PATTERN, sharedModules;
|
|
1562
1671
|
var init_context_builder = __esm({
|
|
1563
1672
|
"packages/core/plugin-host/context-builder.ts"() {
|
|
1564
1673
|
init_types();
|
|
@@ -1567,6 +1676,7 @@ var init_context_builder = __esm({
|
|
|
1567
1676
|
init_logger();
|
|
1568
1677
|
init_interfaces();
|
|
1569
1678
|
init_plugin_namespace();
|
|
1679
|
+
SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
1570
1680
|
sharedModules = {};
|
|
1571
1681
|
}
|
|
1572
1682
|
});
|
|
@@ -2842,6 +2952,7 @@ var init_di = __esm({
|
|
|
2842
2952
|
init_interfaces();
|
|
2843
2953
|
init_storage_service();
|
|
2844
2954
|
init_ai_service();
|
|
2955
|
+
init_courseware_runtime_script_registry();
|
|
2845
2956
|
init_container();
|
|
2846
2957
|
}
|
|
2847
2958
|
});
|
|
@@ -3387,15 +3498,29 @@ parentPort.on('message', async function(msg) {
|
|
|
3387
3498
|
var dbService = rawServices['@openlearn/core:IDatabase'];
|
|
3388
3499
|
var dbApi = dbService ? {
|
|
3389
3500
|
ensureTable: function(tableName, schema) {
|
|
3501
|
+
// SEC: tableName / schema \u7531\u63D2\u4EF6\u63D0\u4F9B\uFF0C\u5C5E\u4E0D\u53EF\u4FE1\u8F93\u5165\u3002\u76F4\u63A5\u62FC\u63A5\u4F1A\u5BFC\u81F4 SQL \u6CE8\u5165
|
|
3502
|
+
// \uFF08\u4F8B\u5982 tableName = "t (x); DROP TABLE events; --"\uFF09\u3002\u6B64\u5904\u5F3A\u5236\u6807\u8BC6\u7B26\u767D\u540D\u5355
|
|
3503
|
+
// \u5E76\u7981\u6B62 schema \u4E2D\u7684\u5206\u53F7\uFF0C\u907F\u514D\u591A\u8BED\u53E5\u6CE8\u5165\u3002\u4E0E inline \u6A21\u5F0F\u4FDD\u6301\u4E00\u81F4\u3002
|
|
3504
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(String(tableName))) {
|
|
3505
|
+
throw new Error("[SEC] Invalid SQL identifier for ensureTable: " + String(tableName));
|
|
3506
|
+
}
|
|
3507
|
+
if (typeof schema !== "string" || schema.length === 0 || schema.length > 4000 || schema.indexOf(";") !== -1) {
|
|
3508
|
+
throw new Error("[SEC] Invalid CREATE TABLE schema fragment: must be non-empty and contain no semicolon");
|
|
3509
|
+
}
|
|
3390
3510
|
var fullName = tablePrefix + tableName;
|
|
3391
3511
|
return dbService.prepareAndRun('CREATE TABLE IF NOT EXISTS ' + fullName + ' (' + schema + ')', []);
|
|
3392
3512
|
},
|
|
3393
3513
|
table: function(tableName) {
|
|
3514
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(String(tableName))) {
|
|
3515
|
+
throw new Error("[SEC] Invalid SQL identifier for table(): " + String(tableName));
|
|
3516
|
+
}
|
|
3394
3517
|
return tablePrefix + tableName;
|
|
3395
3518
|
},
|
|
3396
3519
|
dropAllTables: async function() {
|
|
3397
3520
|
var tables = await dbService.prepareAndAll("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ?", [tablePrefix + '%']);
|
|
3398
3521
|
for (var i = 0; i < tables.length; i++) {
|
|
3522
|
+
// SEC: \u8868\u540D\u6765\u81EA sqlite_master\uFF0C\u4ECD\u4E8C\u6B21\u6821\u9A8C\u540E\u518D\u62FC\u8FDB DDL
|
|
3523
|
+
if (!/^plugin_[A-Za-z0-9_]+$/.test(String(tables[i].name))) continue;
|
|
3399
3524
|
await dbService.prepareAndRun('DROP TABLE IF EXISTS ' + tables[i].name, []);
|
|
3400
3525
|
}
|
|
3401
3526
|
},
|
|
@@ -4129,8 +4254,8 @@ __export(server_exports, {
|
|
|
4129
4254
|
startServer: () => startServer
|
|
4130
4255
|
});
|
|
4131
4256
|
module.exports = __toCommonJS(server_exports);
|
|
4132
|
-
var
|
|
4133
|
-
var
|
|
4257
|
+
var import_express4 = __toESM(require("express"), 1);
|
|
4258
|
+
var import_path17 = __toESM(require("path"), 1);
|
|
4134
4259
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
4135
4260
|
var import_os2 = __toESM(require("os"), 1);
|
|
4136
4261
|
var import_child_process3 = require("child_process");
|
|
@@ -4172,6 +4297,14 @@ var EventBus = class {
|
|
|
4172
4297
|
|
|
4173
4298
|
// packages/core/command-bus/index.ts
|
|
4174
4299
|
var import_uuid = require("uuid");
|
|
4300
|
+
var DEFAULT_QUIET_COMMANDS = /* @__PURE__ */ new Set([
|
|
4301
|
+
"courseware.list",
|
|
4302
|
+
"courseware.get_attempt_raw_data",
|
|
4303
|
+
"whiteboard.query",
|
|
4304
|
+
"whiteboard.get_element",
|
|
4305
|
+
"vfs.read_path",
|
|
4306
|
+
"vfs.list_dir"
|
|
4307
|
+
]);
|
|
4175
4308
|
var CommandBus = class {
|
|
4176
4309
|
constructor(eventBus) {
|
|
4177
4310
|
this.eventBus = eventBus;
|
|
@@ -4215,9 +4348,12 @@ var CommandBus = class {
|
|
|
4215
4348
|
if (!handler) {
|
|
4216
4349
|
throw new Error(`No handler registered for command: ${normalizedCommand.type}`);
|
|
4217
4350
|
}
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4351
|
+
const isQuiet = normalizedCommand.metadata?.silent === true || DEFAULT_QUIET_COMMANDS.has(normalizedCommand.type);
|
|
4352
|
+
if (!isQuiet || process.env.DEBUG?.includes("commandbus") || process.env.DEBUG_COMMAND_BUS === "true") {
|
|
4353
|
+
console.log(
|
|
4354
|
+
`[CommandBus] Executing: ${normalizedCommand.type} (ID: ${normalizedCommand.id}) by ${normalizedCommand.actorId}`
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4221
4357
|
try {
|
|
4222
4358
|
const result = await handler.execute(normalizedCommand);
|
|
4223
4359
|
return result;
|
|
@@ -4281,7 +4417,7 @@ var ActionRegistry = class {
|
|
|
4281
4417
|
};
|
|
4282
4418
|
|
|
4283
4419
|
// packages/core/capability-system/index.ts
|
|
4284
|
-
var CapabilityGuard = class {
|
|
4420
|
+
var CapabilityGuard = class _CapabilityGuard {
|
|
4285
4421
|
constructor() {
|
|
4286
4422
|
this.actorCapabilities = /* @__PURE__ */ new Map();
|
|
4287
4423
|
this.actorCapabilities.set("user-demo", ["*:*:*"]);
|
|
@@ -4321,11 +4457,33 @@ var CapabilityGuard = class {
|
|
|
4321
4457
|
revokeAll(actorId) {
|
|
4322
4458
|
this.actorCapabilities.delete(actorId);
|
|
4323
4459
|
}
|
|
4460
|
+
static extractRole(actorId) {
|
|
4461
|
+
if (!actorId || typeof actorId !== "string") return null;
|
|
4462
|
+
const lastColon = actorId.lastIndexOf(":");
|
|
4463
|
+
if (lastColon === -1) return null;
|
|
4464
|
+
const role = actorId.slice(lastColon + 1);
|
|
4465
|
+
if (["administrator", "admin", "teacher", "student", "anonymous"].includes(role)) return role;
|
|
4466
|
+
return null;
|
|
4467
|
+
}
|
|
4324
4468
|
check(actorId, requiredCap) {
|
|
4325
|
-
const
|
|
4469
|
+
const role = _CapabilityGuard.extractRole(actorId);
|
|
4470
|
+
const isAdmin = actorId === "role:administrator" || actorId === "admin" || actorId === "usr_admin" || actorId === "admin-demo" || role === "administrator" || role === "admin";
|
|
4326
4471
|
if (isAdmin) return true;
|
|
4327
|
-
if (
|
|
4328
|
-
const teacherCaps = [
|
|
4472
|
+
if (role === "teacher") {
|
|
4473
|
+
const teacherCaps = [
|
|
4474
|
+
"lesson:*",
|
|
4475
|
+
"whiteboard:*",
|
|
4476
|
+
"management:*",
|
|
4477
|
+
"quiz:*",
|
|
4478
|
+
"vfs:*",
|
|
4479
|
+
"process:*",
|
|
4480
|
+
"plugin:*",
|
|
4481
|
+
"student:write",
|
|
4482
|
+
"assignment:read",
|
|
4483
|
+
"assignment:submit",
|
|
4484
|
+
"assignment:review",
|
|
4485
|
+
"assignment:manage"
|
|
4486
|
+
];
|
|
4329
4487
|
const [reqRes2, reqAct2] = requiredCap.split(":");
|
|
4330
4488
|
if (teacherCaps.some((c) => {
|
|
4331
4489
|
const [res, act] = c.split(":");
|
|
@@ -4334,8 +4492,15 @@ var CapabilityGuard = class {
|
|
|
4334
4492
|
return true;
|
|
4335
4493
|
}
|
|
4336
4494
|
}
|
|
4337
|
-
if (
|
|
4338
|
-
const studentCaps = [
|
|
4495
|
+
if (role === "student") {
|
|
4496
|
+
const studentCaps = [
|
|
4497
|
+
"student:write",
|
|
4498
|
+
"lesson:read",
|
|
4499
|
+
"whiteboard:read",
|
|
4500
|
+
"assignment:read",
|
|
4501
|
+
"assignment:submit",
|
|
4502
|
+
"assignment:review"
|
|
4503
|
+
];
|
|
4339
4504
|
const [reqRes2, reqAct2] = requiredCap.split(":");
|
|
4340
4505
|
if (studentCaps.some((c) => {
|
|
4341
4506
|
const [res, act] = c.split(":");
|
|
@@ -5061,15 +5226,89 @@ db.exec(`
|
|
|
5061
5226
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
5062
5227
|
);
|
|
5063
5228
|
|
|
5229
|
+
-- \u2500\u2500 \u4F5C\u4E1A\u4E2D\u5FC3\uFF08Assignment Hub\uFF09\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5230
|
+
-- plugin_assignments \u662F\u4F5C\u4E1A\u5B9E\u4F53\uFF1A\u767D\u677F\u4E0A\u7684\u300C\u8BFE\u5802\u4F5C\u4E1A\u4EFB\u52A1\u300D\u6559\u5B66\u5BF9\u8C61\u53EA\u662F\u5B83\u7684
|
|
5231
|
+
-- \u4E00\u4E2A\u6295\u5F71\u7247\u6BB5\uFF08element_id\uFF09\uFF0Cclass_id \u8BA9\u5B83\u540C\u65F6\u51FA\u73B0\u5728\u73ED\u7EA7\u4F5C\u4E1A\u6210\u7EE9\u9875\u3002
|
|
5232
|
+
CREATE TABLE IF NOT EXISTS plugin_assignments (
|
|
5233
|
+
id TEXT PRIMARY KEY,
|
|
5234
|
+
class_id TEXT,
|
|
5235
|
+
lesson_id TEXT,
|
|
5236
|
+
element_id TEXT,
|
|
5237
|
+
title TEXT NOT NULL,
|
|
5238
|
+
description TEXT,
|
|
5239
|
+
instructions TEXT,
|
|
5240
|
+
due_at INTEGER,
|
|
5241
|
+
allow_late INTEGER NOT NULL DEFAULT 1,
|
|
5242
|
+
allow_text INTEGER NOT NULL DEFAULT 1,
|
|
5243
|
+
allow_link INTEGER NOT NULL DEFAULT 0,
|
|
5244
|
+
max_files INTEGER NOT NULL DEFAULT 10,
|
|
5245
|
+
max_file_size INTEGER NOT NULL DEFAULT 20971520,
|
|
5246
|
+
allowed_ext TEXT NOT NULL DEFAULT '',
|
|
5247
|
+
peer_review_mode TEXT NOT NULL DEFAULT 'assigned',
|
|
5248
|
+
peer_review_count INTEGER NOT NULL DEFAULT 2,
|
|
5249
|
+
peer_review_due_at INTEGER,
|
|
5250
|
+
teacher_weight REAL NOT NULL DEFAULT 0.6,
|
|
5251
|
+
peer_weight REAL NOT NULL DEFAULT 0.4,
|
|
5252
|
+
status TEXT NOT NULL DEFAULT 'published',
|
|
5253
|
+
created_by TEXT,
|
|
5254
|
+
created_at INTEGER NOT NULL,
|
|
5255
|
+
updated_at INTEGER NOT NULL
|
|
5256
|
+
);
|
|
5257
|
+
|
|
5258
|
+
-- \u4E00\u4E2A\u4F5C\u4E1A\u4E00\u4E2A\u5B66\u751F\u4E00\u884C\uFF08assignment_id \u975E\u7A7A\u65F6\uFF09\uFF1B\u4FDD\u7559 lesson_id \u5217\u4EE5\u517C\u5BB9\u65E7\u6570\u636E
|
|
5064
5259
|
CREATE TABLE IF NOT EXISTS plugin_submissions (
|
|
5065
5260
|
id TEXT PRIMARY KEY,
|
|
5066
|
-
|
|
5261
|
+
assignment_id TEXT,
|
|
5262
|
+
lesson_id TEXT,
|
|
5067
5263
|
student_id TEXT NOT NULL,
|
|
5068
|
-
file_path TEXT
|
|
5264
|
+
file_path TEXT,
|
|
5069
5265
|
version INTEGER NOT NULL DEFAULT 1,
|
|
5070
5266
|
created_at INTEGER NOT NULL,
|
|
5071
|
-
updated_at INTEGER NOT NULL
|
|
5072
|
-
|
|
5267
|
+
updated_at INTEGER NOT NULL
|
|
5268
|
+
);
|
|
5269
|
+
|
|
5270
|
+
-- \u6BCF\u6B21\u63D0\u4EA4\u90FD\u7559\u6863\uFF08\u91CD\u4EA4\u4E0D\u8986\u76D6\u5386\u53F2\uFF09
|
|
5271
|
+
CREATE TABLE IF NOT EXISTS plugin_submission_versions (
|
|
5272
|
+
id TEXT PRIMARY KEY,
|
|
5273
|
+
submission_id TEXT NOT NULL,
|
|
5274
|
+
assignment_id TEXT,
|
|
5275
|
+
student_id TEXT NOT NULL,
|
|
5276
|
+
version INTEGER NOT NULL,
|
|
5277
|
+
files_json TEXT NOT NULL DEFAULT '[]',
|
|
5278
|
+
text_content TEXT,
|
|
5279
|
+
link_url TEXT,
|
|
5280
|
+
is_late INTEGER NOT NULL DEFAULT 0,
|
|
5281
|
+
submitted_at INTEGER NOT NULL,
|
|
5282
|
+
UNIQUE(submission_id, version)
|
|
5283
|
+
);
|
|
5284
|
+
|
|
5285
|
+
-- \u4E0A\u4F20\u7684\u5B9E\u4F53\u6587\u4EF6\uFF08\u78C1\u76D8\u5B58\u50A8 + \u5143\u6570\u636E\uFF09\uFF0C\u4E0B\u8F7D\u7AEF\u70B9\u6309\u6B64\u8868\u9274\u6743
|
|
5286
|
+
CREATE TABLE IF NOT EXISTS plugin_assignment_files (
|
|
5287
|
+
id TEXT PRIMARY KEY,
|
|
5288
|
+
assignment_id TEXT NOT NULL,
|
|
5289
|
+
submission_id TEXT,
|
|
5290
|
+
version_id TEXT,
|
|
5291
|
+
student_id TEXT NOT NULL,
|
|
5292
|
+
original_name TEXT NOT NULL,
|
|
5293
|
+
stored_path TEXT NOT NULL,
|
|
5294
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
5295
|
+
mime TEXT,
|
|
5296
|
+
sha256 TEXT,
|
|
5297
|
+
uploaded_at INTEGER NOT NULL,
|
|
5298
|
+
deleted_at INTEGER
|
|
5299
|
+
);
|
|
5300
|
+
|
|
5301
|
+
-- \u4E92\u8BC4\u4EFB\u52A1\uFF08\u8C01\u8BC4\u8C01 / \u533F\u540D / \u622A\u6B62\uFF09\uFF0C\u7531\u5206\u914D\u7B56\u7565\u751F\u6210
|
|
5302
|
+
CREATE TABLE IF NOT EXISTS plugin_peer_review_tasks (
|
|
5303
|
+
id TEXT PRIMARY KEY,
|
|
5304
|
+
assignment_id TEXT NOT NULL,
|
|
5305
|
+
submission_id TEXT NOT NULL,
|
|
5306
|
+
reviewer_id TEXT NOT NULL,
|
|
5307
|
+
anonymous INTEGER NOT NULL DEFAULT 1,
|
|
5308
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
5309
|
+
due_at INTEGER,
|
|
5310
|
+
created_at INTEGER NOT NULL,
|
|
5311
|
+
UNIQUE(submission_id, reviewer_id)
|
|
5073
5312
|
);
|
|
5074
5313
|
|
|
5075
5314
|
CREATE TABLE IF NOT EXISTS plugin_peer_reviews (
|
|
@@ -5079,6 +5318,11 @@ db.exec(`
|
|
|
5079
5318
|
score INTEGER NOT NULL,
|
|
5080
5319
|
comment TEXT,
|
|
5081
5320
|
created_at INTEGER NOT NULL,
|
|
5321
|
+
assignment_id TEXT,
|
|
5322
|
+
task_id TEXT,
|
|
5323
|
+
anonymous INTEGER NOT NULL DEFAULT 1,
|
|
5324
|
+
status TEXT NOT NULL DEFAULT 'submitted',
|
|
5325
|
+
updated_at INTEGER,
|
|
5082
5326
|
UNIQUE(submission_id, reviewer_id)
|
|
5083
5327
|
);
|
|
5084
5328
|
|
|
@@ -5091,7 +5335,12 @@ db.exec(`
|
|
|
5091
5335
|
peer_weight REAL NOT NULL DEFAULT 0.4,
|
|
5092
5336
|
calculated_final_score INTEGER,
|
|
5093
5337
|
status TEXT NOT NULL DEFAULT 'draft',
|
|
5094
|
-
graded_at INTEGER
|
|
5338
|
+
graded_at INTEGER,
|
|
5339
|
+
assignment_id TEXT,
|
|
5340
|
+
peer_average_score REAL,
|
|
5341
|
+
source TEXT NOT NULL DEFAULT 'teacher',
|
|
5342
|
+
published_at INTEGER,
|
|
5343
|
+
graded_by TEXT
|
|
5095
5344
|
);
|
|
5096
5345
|
|
|
5097
5346
|
-- Performance Indexes
|
|
@@ -5104,7 +5353,22 @@ db.exec(`
|
|
|
5104
5353
|
CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(type, timestamp);
|
|
5105
5354
|
CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
|
|
5106
5355
|
CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
|
|
5356
|
+
|
|
5357
|
+
-- \u4F5C\u4E1A\u4E2D\u5FC3\u7D22\u5F15
|
|
5358
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_plugin_assignments_element ON plugin_assignments(element_id);
|
|
5359
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_assignments_lesson ON plugin_assignments(lesson_id);
|
|
5360
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_assignments_class ON plugin_assignments(class_id);
|
|
5361
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_submission_versions_submission ON plugin_submission_versions(submission_id);
|
|
5362
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_assignment_files_owner ON plugin_assignment_files(assignment_id, student_id);
|
|
5363
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_peer_review_tasks_reviewer ON plugin_peer_review_tasks(reviewer_id, status);
|
|
5107
5364
|
`);
|
|
5365
|
+
try {
|
|
5366
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_plugin_submissions_assignment_student ON plugin_submissions(assignment_id, student_id)");
|
|
5367
|
+
db.exec(
|
|
5368
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_plugin_submissions_legacy ON plugin_submissions(lesson_id, student_id) WHERE assignment_id IS NULL"
|
|
5369
|
+
);
|
|
5370
|
+
} catch {
|
|
5371
|
+
}
|
|
5108
5372
|
try {
|
|
5109
5373
|
db.exec("ALTER TABLE lessons ADD COLUMN creator_id TEXT");
|
|
5110
5374
|
} catch {
|
|
@@ -6606,7 +6870,632 @@ ${htmlContent}
|
|
|
6606
6870
|
}
|
|
6607
6871
|
};
|
|
6608
6872
|
|
|
6873
|
+
// packages/plugins/score-monitor-script.ts
|
|
6874
|
+
var SCORE_MONITOR_SCRIPT_ID = "score-variable-monitor";
|
|
6875
|
+
var SCORE_MONITOR_SCRIPT_OWNER = "@openlearn/plugin-builtin";
|
|
6876
|
+
var SCORE_MONITOR_SCRIPT = `(function () {
|
|
6877
|
+
'use strict';
|
|
6878
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
6879
|
+
if (window.__LMS_WATCH_DISABLED__ === true) return;
|
|
6880
|
+
if (window.__LMS_WATCH__ === false) return;
|
|
6881
|
+
if (window.__LMS_SCORE_WATCHER_READY__ === true) return;
|
|
6882
|
+
window.__LMS_SCORE_WATCHER_READY__ = true;
|
|
6883
|
+
|
|
6884
|
+
var INTERVAL_MS = 800;
|
|
6885
|
+
var SILENCE_MS = 1200;
|
|
6886
|
+
var MAX_SUBMITS = 60;
|
|
6887
|
+
var KEY_RE = /score|point|grade|mark|correct|right/i;
|
|
6888
|
+
var SKIP_RE = /^_|^on[A-Z]|^webkit|^moz|^ms/;
|
|
6889
|
+
var NUM_RE = /[0-9]+(?:[.][0-9]+)?/;
|
|
6890
|
+
var RATIO_RE = /([0-9]+(?:[.][0-9]+)?)[^0-9]*[/\uFF0F|\u4E4B][^0-9]*([0-9]+)/;
|
|
6891
|
+
var DOM_SELECTORS = [
|
|
6892
|
+
'#score', '#points', '#grade', '#finalScore', '#totalScore', '#scoreDisplay', '#score-num',
|
|
6893
|
+
'.score', '.points', '.grade', '.final-score',
|
|
6894
|
+
'[id*="score" i]', '[id*="point" i]', '[id*="grade" i]'
|
|
6895
|
+
];
|
|
6896
|
+
|
|
6897
|
+
var state = { snapshot: null, pendingChanged: null, timer: null, submitted: 0, stopped: false };
|
|
6898
|
+
var seenElements = [];
|
|
6899
|
+
|
|
6900
|
+
function num(value) {
|
|
6901
|
+
if (typeof value === 'number') return isFinite(value) ? value : null;
|
|
6902
|
+
if (typeof value !== 'string') return null;
|
|
6903
|
+
var match = value.match(RATIO_RE);
|
|
6904
|
+
if (match) {
|
|
6905
|
+
var part = parseFloat(match[1]);
|
|
6906
|
+
var whole = parseFloat(match[2]);
|
|
6907
|
+
if (whole > 0) return Math.round((part / whole) * 10000) / 100;
|
|
6908
|
+
}
|
|
6909
|
+
var found = value.match(NUM_RE);
|
|
6910
|
+
if (!found) return null;
|
|
6911
|
+
var parsed = parseFloat(found[0]);
|
|
6912
|
+
return isFinite(parsed) ? parsed : null;
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
function selectorKey(selector) {
|
|
6916
|
+
return 'dom_' + selector.replace(/[^A-Za-z0-9_-]+/g, '_');
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
function isShown(el) {
|
|
6920
|
+
if (!el || !el.getBoundingClientRect) return false;
|
|
6921
|
+
var rect = el.getBoundingClientRect();
|
|
6922
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
6923
|
+
var node = el;
|
|
6924
|
+
while (node && node.nodeType === 1) {
|
|
6925
|
+
var style = null;
|
|
6926
|
+
try { style = window.getComputedStyle(node); } catch (err) { style = null; }
|
|
6927
|
+
if (style && (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')) return false;
|
|
6928
|
+
node = node.parentNode;
|
|
6929
|
+
}
|
|
6930
|
+
return true;
|
|
6931
|
+
}
|
|
6932
|
+
|
|
6933
|
+
function resolvePath(path) {
|
|
6934
|
+
try {
|
|
6935
|
+
var node = window;
|
|
6936
|
+
var parts = String(path).split('.');
|
|
6937
|
+
for (var i = 0; i < parts.length; i++) {
|
|
6938
|
+
if (node === null || typeof node === 'undefined') return null;
|
|
6939
|
+
node = node[parts[i]];
|
|
6940
|
+
}
|
|
6941
|
+
return node;
|
|
6942
|
+
} catch (err) {
|
|
6943
|
+
return null;
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
|
|
6947
|
+
function snapshot() {
|
|
6948
|
+
var result = {};
|
|
6949
|
+
var explicit = window.__LMS_WATCH__;
|
|
6950
|
+
var names = [];
|
|
6951
|
+
if (typeof explicit === 'string' && explicit) names.push(explicit);
|
|
6952
|
+
else if (Object.prototype.toString.call(explicit) === '[object Array]') {
|
|
6953
|
+
for (var i = 0; i < explicit.length; i++) if (typeof explicit[i] === 'string') names.push(explicit[i]);
|
|
6954
|
+
}
|
|
6955
|
+
if (names.length) {
|
|
6956
|
+
for (var n = 0; n < names.length; n++) {
|
|
6957
|
+
var value = num(resolvePath(names[n]));
|
|
6958
|
+
if (value !== null) result[names[n]] = value;
|
|
6959
|
+
}
|
|
6960
|
+
} else {
|
|
6961
|
+
var keys = Object.keys(window);
|
|
6962
|
+
for (var k = 0; k < keys.length; k++) {
|
|
6963
|
+
var key = keys[k];
|
|
6964
|
+
if (SKIP_RE.test(key) || !KEY_RE.test(key)) continue;
|
|
6965
|
+
var raw = null;
|
|
6966
|
+
try { raw = window[key]; } catch (err) { continue; }
|
|
6967
|
+
if (typeof raw !== 'number' && typeof raw !== 'string') continue;
|
|
6968
|
+
var numValue = num(raw);
|
|
6969
|
+
if (numValue !== null) result[key] = numValue;
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
seenElements = [];
|
|
6973
|
+
for (var s = 0; s < DOM_SELECTORS.length; s++) {
|
|
6974
|
+
var selector = DOM_SELECTORS[s];
|
|
6975
|
+
var nodes = [];
|
|
6976
|
+
try { nodes = document.querySelectorAll(selector); } catch (err) { nodes = []; }
|
|
6977
|
+
for (var t = 0; t < nodes.length; t++) {
|
|
6978
|
+
var el = nodes[t];
|
|
6979
|
+
if (seenElements.indexOf(el) !== -1) continue;
|
|
6980
|
+
seenElements.push(el);
|
|
6981
|
+
if (!isShown(el)) continue;
|
|
6982
|
+
var text = (el.textContent || '').trim();
|
|
6983
|
+
if (!text || text.length > 40) continue;
|
|
6984
|
+
var domValue = num(text);
|
|
6985
|
+
if (domValue === null) continue;
|
|
6986
|
+
if (typeof result[selectorKey(selector)] === 'undefined') result[selectorKey(selector)] = domValue;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
return result;
|
|
6990
|
+
}
|
|
6991
|
+
|
|
6992
|
+
function diff(prev, next) {
|
|
6993
|
+
var changed = {};
|
|
6994
|
+
var keys = Object.keys(next);
|
|
6995
|
+
for (var i = 0; i < keys.length; i++) {
|
|
6996
|
+
var key = keys[i];
|
|
6997
|
+
if (!prev || prev[key] !== next[key]) changed[key] = next[key];
|
|
6998
|
+
}
|
|
6999
|
+
return changed;
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
function report(payload) {
|
|
7003
|
+
try {
|
|
7004
|
+
if (window.LMS && typeof window.LMS.saveProgress === 'function') {
|
|
7005
|
+
window.LMS.saveProgress(payload);
|
|
7006
|
+
return true;
|
|
7007
|
+
}
|
|
7008
|
+
} catch (err) { /* \u5FFD\u7565\u5E76\u5C1D\u8BD5\u515C\u5E95\u901A\u9053 */ }
|
|
7009
|
+
try {
|
|
7010
|
+
var student = window.__LMS_STUDENT__ || {};
|
|
7011
|
+
var courseware = window.__LMS_COURSEWARE__ || {};
|
|
7012
|
+
window.parent.postMessage(
|
|
7013
|
+
{ type: 'LMS_SAVE_PROGRESS', uuid: courseware.uuid, attempt_id: student.attempt_id, payload: payload },
|
|
7014
|
+
'*'
|
|
7015
|
+
);
|
|
7016
|
+
return true;
|
|
7017
|
+
} catch (err) {
|
|
7018
|
+
return false;
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
7021
|
+
|
|
7022
|
+
function flush() {
|
|
7023
|
+
if (state.stopped) return;
|
|
7024
|
+
if (window.__LMS_WATCH_DISABLED__ === true) { state.stopped = true; return; }
|
|
7025
|
+
var changed = state.pendingChanged || {};
|
|
7026
|
+
state.pendingChanged = null;
|
|
7027
|
+
var keys = Object.keys(changed);
|
|
7028
|
+
if (!keys.length) return;
|
|
7029
|
+
if (state.submitted >= MAX_SUBMITS) return;
|
|
7030
|
+
var best = null;
|
|
7031
|
+
for (var i = 0; i < keys.length; i++) {
|
|
7032
|
+
var value = changed[keys[i]];
|
|
7033
|
+
if (typeof value !== 'number') continue;
|
|
7034
|
+
if (best === null || value > best) best = value;
|
|
7035
|
+
}
|
|
7036
|
+
if (best === null) return;
|
|
7037
|
+
var snap = {};
|
|
7038
|
+
var all = state.snapshot || {};
|
|
7039
|
+
var allKeys = Object.keys(all);
|
|
7040
|
+
for (var j = 0; j < allKeys.length; j++) snap[allKeys[j]] = all[allKeys[j]];
|
|
7041
|
+
snap._changed = keys;
|
|
7042
|
+
snap._at = Date.now();
|
|
7043
|
+
try { console.warn('Score variable changed (' + keys.join(', ') + ') -> ' + best); } catch (err) { /* noop */ }
|
|
7044
|
+
var ok = report({ score: best, watch: snap });
|
|
7045
|
+
if (ok) state.submitted = state.submitted + 1;
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
function tick() {
|
|
7049
|
+
if (state.stopped) return;
|
|
7050
|
+
if (window.__LMS_WATCH_DISABLED__ === true) { state.stopped = true; return; }
|
|
7051
|
+
if (state.submitted >= MAX_SUBMITS) { state.stopped = true; return; }
|
|
7052
|
+
var student = window.__LMS_STUDENT__;
|
|
7053
|
+
if (!student || !student.attempt_id) return;
|
|
7054
|
+
var next = snapshot();
|
|
7055
|
+
var changed = diff(state.snapshot, next);
|
|
7056
|
+
state.snapshot = next;
|
|
7057
|
+
if (!Object.keys(changed).length) return;
|
|
7058
|
+
state.pendingChanged = Object.assign(state.pendingChanged || {}, changed);
|
|
7059
|
+
if (state.timer) clearTimeout(state.timer);
|
|
7060
|
+
state.timer = setTimeout(function () { state.timer = null; flush(); }, SILENCE_MS);
|
|
7061
|
+
}
|
|
7062
|
+
|
|
7063
|
+
function init() {
|
|
7064
|
+
state.snapshot = snapshot();
|
|
7065
|
+
setInterval(tick, INTERVAL_MS);
|
|
7066
|
+
try {
|
|
7067
|
+
if (!window.__LMS_SCORE_WATCHERS__ || Object.prototype.toString.call(window.__LMS_SCORE_WATCHERS__) !== '[object Array]') {
|
|
7068
|
+
window.__LMS_SCORE_WATCHERS__ = [];
|
|
7069
|
+
}
|
|
7070
|
+
window.__LMS_SCORE_WATCHERS__.push({
|
|
7071
|
+
stop: function () {
|
|
7072
|
+
state.stopped = true;
|
|
7073
|
+
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
|
7074
|
+
}
|
|
7075
|
+
});
|
|
7076
|
+
} catch (err) { /* noop */ }
|
|
7077
|
+
if (typeof MutationObserver === 'function' && document.documentElement) {
|
|
7078
|
+
var scheduled = false;
|
|
7079
|
+
var observer = new MutationObserver(function () {
|
|
7080
|
+
if (scheduled) return;
|
|
7081
|
+
scheduled = true;
|
|
7082
|
+
setTimeout(function () { scheduled = false; tick(); }, 300);
|
|
7083
|
+
});
|
|
7084
|
+
try {
|
|
7085
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
7086
|
+
} catch (err) { /* noop */ }
|
|
7087
|
+
}
|
|
7088
|
+
try { console.warn('[native] score monitor armed, watching score-like variables'); } catch (err) { /* noop */ }
|
|
7089
|
+
}
|
|
7090
|
+
|
|
7091
|
+
if (document.body) init();
|
|
7092
|
+
else document.addEventListener('DOMContentLoaded', init);
|
|
7093
|
+
})();`;
|
|
7094
|
+
|
|
7095
|
+
// packages/plugins/courseware-score.ts
|
|
7096
|
+
var DEFAULT_SCORE_POLICY = "LATEST";
|
|
7097
|
+
var GLOBAL_SCORE_CONFIG_KEY = "*";
|
|
7098
|
+
var SCORE_CONFIG_TABLE = "courseware_score_config";
|
|
7099
|
+
var DEFAULT_SCORE_CONFIG = {
|
|
7100
|
+
courseware_id: GLOBAL_SCORE_CONFIG_KEY,
|
|
7101
|
+
courseware_name: null,
|
|
7102
|
+
score_policy: DEFAULT_SCORE_POLICY,
|
|
7103
|
+
score_fields: "",
|
|
7104
|
+
raw_full_score: 100,
|
|
7105
|
+
target_full_score: 100,
|
|
7106
|
+
weight_percentage: 100,
|
|
7107
|
+
lesson_id: null,
|
|
7108
|
+
updated_at: 0
|
|
7109
|
+
};
|
|
7110
|
+
var BUILTIN_SCORE_CONFIG = { ...DEFAULT_SCORE_CONFIG };
|
|
7111
|
+
function toNumber(value) {
|
|
7112
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
7113
|
+
if (typeof value === "boolean") return null;
|
|
7114
|
+
if (typeof value === "string") {
|
|
7115
|
+
const trimmed = value.trim();
|
|
7116
|
+
if (!trimmed) return null;
|
|
7117
|
+
const cleaned = trimmed.endsWith("%") ? trimmed.slice(0, -1).trim() : trimmed;
|
|
7118
|
+
if (!cleaned || !/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(cleaned)) return null;
|
|
7119
|
+
const num = Number(cleaned);
|
|
7120
|
+
return Number.isFinite(num) ? num : null;
|
|
7121
|
+
}
|
|
7122
|
+
return null;
|
|
7123
|
+
}
|
|
7124
|
+
function clamp(value, min, max) {
|
|
7125
|
+
if (!Number.isFinite(value)) return min;
|
|
7126
|
+
if (value < min) return min;
|
|
7127
|
+
if (value > max) return max;
|
|
7128
|
+
return value;
|
|
7129
|
+
}
|
|
7130
|
+
function round2(value) {
|
|
7131
|
+
if (!Number.isFinite(value)) return value;
|
|
7132
|
+
return Math.round(value * 100) / 100;
|
|
7133
|
+
}
|
|
7134
|
+
function isKnownScorePolicy(value) {
|
|
7135
|
+
if (typeof value !== "string") return false;
|
|
7136
|
+
const raw = value.trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
7137
|
+
return KNOWN_POLICY_ALIASES.has(raw);
|
|
7138
|
+
}
|
|
7139
|
+
var KNOWN_POLICY_ALIASES = /* @__PURE__ */ new Set([
|
|
7140
|
+
"LATEST",
|
|
7141
|
+
"LAST",
|
|
7142
|
+
"LATEST_SCORE",
|
|
7143
|
+
"\u6700\u65B0",
|
|
7144
|
+
"\u6700\u8FD1",
|
|
7145
|
+
"MAX",
|
|
7146
|
+
"MAXIMUM",
|
|
7147
|
+
"HIGHEST",
|
|
7148
|
+
"MAX_SCORE",
|
|
7149
|
+
"\u6700\u9AD8",
|
|
7150
|
+
"\u6700\u9AD8\u5206",
|
|
7151
|
+
"AVERAGE",
|
|
7152
|
+
"AVG",
|
|
7153
|
+
"MEAN",
|
|
7154
|
+
"\u5E73\u5747",
|
|
7155
|
+
"\u5E73\u5747\u5206",
|
|
7156
|
+
"FIRST",
|
|
7157
|
+
"EARLIEST",
|
|
7158
|
+
"\u9996\u4E2A",
|
|
7159
|
+
"\u6700\u65E9"
|
|
7160
|
+
]);
|
|
7161
|
+
function normalizeScorePolicy(value) {
|
|
7162
|
+
const raw = typeof value === "string" ? value.trim().toUpperCase().replace(/[\s-]+/g, "_") : "";
|
|
7163
|
+
if (["MAX", "MAXIMUM", "HIGHEST", "MAX_SCORE", "\u6700\u9AD8", "\u6700\u9AD8\u5206"].includes(raw)) return "MAX";
|
|
7164
|
+
if (["AVERAGE", "AVG", "MEAN", "\u5E73\u5747", "\u5E73\u5747\u5206"].includes(raw)) return "AVERAGE";
|
|
7165
|
+
if (["FIRST", "EARLIEST", "\u9996\u4E2A", "\u6700\u65E9"].includes(raw)) return "FIRST";
|
|
7166
|
+
return "LATEST";
|
|
7167
|
+
}
|
|
7168
|
+
function parseScoreFields(fields) {
|
|
7169
|
+
if (Array.isArray(fields)) {
|
|
7170
|
+
return fields.map((f) => String(f ?? "").trim()).filter(Boolean);
|
|
7171
|
+
}
|
|
7172
|
+
if (typeof fields !== "string" || !fields.trim()) return [];
|
|
7173
|
+
return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
|
|
7174
|
+
}
|
|
7175
|
+
function getNested(raw, path24) {
|
|
7176
|
+
if (raw === null || raw === void 0) return void 0;
|
|
7177
|
+
if (!path24) return void 0;
|
|
7178
|
+
const parts = String(path24).split(".").map((p) => p.trim()).filter(Boolean);
|
|
7179
|
+
let cursor = raw;
|
|
7180
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
7181
|
+
if (cursor === null || cursor === void 0) return void 0;
|
|
7182
|
+
const part = parts[i];
|
|
7183
|
+
if (/^\d+$/.test(part)) {
|
|
7184
|
+
const idx = Number(part);
|
|
7185
|
+
cursor = Array.isArray(cursor) ? cursor[idx] : cursor[part];
|
|
7186
|
+
} else {
|
|
7187
|
+
cursor = cursor[part];
|
|
7188
|
+
}
|
|
7189
|
+
if (i < parts.length - 1 && typeof cursor === "string" && /^[[{]/.test(cursor.trim())) {
|
|
7190
|
+
try {
|
|
7191
|
+
cursor = JSON.parse(cursor);
|
|
7192
|
+
} catch (e) {
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
return cursor;
|
|
7197
|
+
}
|
|
7198
|
+
var COMMON_SCORE_KEYS = [
|
|
7199
|
+
"score",
|
|
7200
|
+
"points",
|
|
7201
|
+
"point",
|
|
7202
|
+
"grade",
|
|
7203
|
+
"mark",
|
|
7204
|
+
"correct",
|
|
7205
|
+
"right",
|
|
7206
|
+
"userScore",
|
|
7207
|
+
"totalScore",
|
|
7208
|
+
"finalScore",
|
|
7209
|
+
"rawScore"
|
|
7210
|
+
];
|
|
7211
|
+
var COMMON_SCORE_CONTAINERS = ["data", "body", "payload", "detail", "summary", "progress"];
|
|
7212
|
+
var SCAN_MAX_DEPTH = 4;
|
|
7213
|
+
var SCAN_MAX_NODES = 300;
|
|
7214
|
+
function scanForScore(node, depth, budget) {
|
|
7215
|
+
if (node === null || node === void 0) return null;
|
|
7216
|
+
budget.count += 1;
|
|
7217
|
+
if (budget.count > SCAN_MAX_NODES || depth > SCAN_MAX_DEPTH) return null;
|
|
7218
|
+
if (typeof node === "string") {
|
|
7219
|
+
const trimmed = node.trim();
|
|
7220
|
+
if (!trimmed) return null;
|
|
7221
|
+
if (/^[[{]/.test(trimmed)) {
|
|
7222
|
+
try {
|
|
7223
|
+
return scanForScore(JSON.parse(trimmed), depth + 1, budget);
|
|
7224
|
+
} catch (e) {
|
|
7225
|
+
return null;
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
const keyValue = /(?:^|[?&\s,;])(?:score|point|points|grade|mark|correct|right)=([+-]?\d+(?:\.\d+)?)/i.exec(trimmed);
|
|
7229
|
+
if (keyValue) return Number(keyValue[1]);
|
|
7230
|
+
const ratio = /^([+-]?\d+(?:\.\d+)?)\s*\/\s*([+-]?\d+(?:\.\d+)?)$/.exec(trimmed);
|
|
7231
|
+
if (ratio) return Number(ratio[1]);
|
|
7232
|
+
return null;
|
|
7233
|
+
}
|
|
7234
|
+
if (typeof node !== "object") return null;
|
|
7235
|
+
for (const key of COMMON_SCORE_KEYS) {
|
|
7236
|
+
const value = toNumber(node[key]);
|
|
7237
|
+
if (value !== null) return value;
|
|
7238
|
+
}
|
|
7239
|
+
for (const key of Object.keys(node)) {
|
|
7240
|
+
const child = node[key];
|
|
7241
|
+
if (child === null || child === void 0) continue;
|
|
7242
|
+
if (typeof child !== "object" && typeof child !== "string") continue;
|
|
7243
|
+
const value = scanForScore(child, depth + 1, budget);
|
|
7244
|
+
if (value !== null) return value;
|
|
7245
|
+
}
|
|
7246
|
+
return null;
|
|
7247
|
+
}
|
|
7248
|
+
function extractScoreFromFields(raw, fields) {
|
|
7249
|
+
for (const field of parseScoreFields(fields)) {
|
|
7250
|
+
const value = toNumber(getNested(raw, field));
|
|
7251
|
+
if (value !== null) return value;
|
|
7252
|
+
}
|
|
7253
|
+
return null;
|
|
7254
|
+
}
|
|
7255
|
+
function pickScoreFromPayload(payload, fields) {
|
|
7256
|
+
const byFields = extractScoreFromFields(payload, fields);
|
|
7257
|
+
if (byFields !== null) return byFields;
|
|
7258
|
+
if (typeof payload === "string") return scanForScore(payload, 0, { count: 0 });
|
|
7259
|
+
if (payload === null || typeof payload !== "object") return null;
|
|
7260
|
+
for (const key of COMMON_SCORE_KEYS) {
|
|
7261
|
+
const value = toNumber(payload[key]);
|
|
7262
|
+
if (value !== null) return value;
|
|
7263
|
+
}
|
|
7264
|
+
for (const container of COMMON_SCORE_CONTAINERS) {
|
|
7265
|
+
const nested = payload[container];
|
|
7266
|
+
if (nested === null || typeof nested !== "object") continue;
|
|
7267
|
+
for (const key of COMMON_SCORE_KEYS) {
|
|
7268
|
+
const value = toNumber(nested[key]);
|
|
7269
|
+
if (value !== null) return value;
|
|
7270
|
+
}
|
|
7271
|
+
}
|
|
7272
|
+
return scanForScore(payload, 0, { count: 0 });
|
|
7273
|
+
}
|
|
7274
|
+
function aggregateScores(scores, policy) {
|
|
7275
|
+
const list = (Array.isArray(scores) ? scores : []).map((s) => toNumber(s)).filter((n) => n !== null);
|
|
7276
|
+
if (list.length === 0) return null;
|
|
7277
|
+
const resolved = normalizeScorePolicy(policy);
|
|
7278
|
+
if (resolved === "MAX") return list.reduce((a, b) => b > a ? b : a, list[0]);
|
|
7279
|
+
if (resolved === "AVERAGE") return list.reduce((a, b) => a + b, 0) / list.length;
|
|
7280
|
+
if (resolved === "FIRST") return list[0];
|
|
7281
|
+
return list[list.length - 1];
|
|
7282
|
+
}
|
|
7283
|
+
function normalizeScoreConfig(row) {
|
|
7284
|
+
if (!row || typeof row !== "object") return { ...BUILTIN_SCORE_CONFIG };
|
|
7285
|
+
const positive = (value, fallback) => {
|
|
7286
|
+
const num = toNumber(value);
|
|
7287
|
+
return num !== null && num > 0 ? num : fallback;
|
|
7288
|
+
};
|
|
7289
|
+
const weight = toNumber(row.weight_percentage);
|
|
7290
|
+
const updatedAt = toNumber(row.updated_at);
|
|
7291
|
+
return {
|
|
7292
|
+
courseware_id: String(row.courseware_id ?? GLOBAL_SCORE_CONFIG_KEY),
|
|
7293
|
+
courseware_name: row.courseware_name === void 0 || row.courseware_name === null ? null : String(row.courseware_name),
|
|
7294
|
+
score_policy: normalizeScorePolicy(row.score_policy),
|
|
7295
|
+
score_fields: typeof row.score_fields === "string" ? row.score_fields : "",
|
|
7296
|
+
raw_full_score: positive(row.raw_full_score, 100),
|
|
7297
|
+
target_full_score: positive(row.target_full_score, 100),
|
|
7298
|
+
weight_percentage: weight === null ? 100 : clamp(weight, 0, 1e3),
|
|
7299
|
+
lesson_id: row.lesson_id === void 0 || row.lesson_id === null ? null : String(row.lesson_id),
|
|
7300
|
+
updated_at: updatedAt ?? 0
|
|
7301
|
+
};
|
|
7302
|
+
}
|
|
7303
|
+
function resolveScoreConfig(db2, coursewareId) {
|
|
7304
|
+
const id = coursewareId === null || coursewareId === void 0 ? "" : String(coursewareId).trim();
|
|
7305
|
+
if (id && id !== GLOBAL_SCORE_CONFIG_KEY) {
|
|
7306
|
+
try {
|
|
7307
|
+
const row = db2.prepare(`SELECT * FROM ${SCORE_CONFIG_TABLE} WHERE courseware_id = ?`).get(id);
|
|
7308
|
+
if (row) return { config: normalizeScoreConfig(row), source: "courseware" };
|
|
7309
|
+
} catch (e) {
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
try {
|
|
7313
|
+
const global = db2.prepare(`SELECT * FROM ${SCORE_CONFIG_TABLE} WHERE courseware_id = ?`).get(GLOBAL_SCORE_CONFIG_KEY);
|
|
7314
|
+
if (global) return { config: normalizeScoreConfig(global), source: "global" };
|
|
7315
|
+
} catch (e) {
|
|
7316
|
+
}
|
|
7317
|
+
return { config: { ...BUILTIN_SCORE_CONFIG }, source: "builtin" };
|
|
7318
|
+
}
|
|
7319
|
+
function listScoreConfigs(db2) {
|
|
7320
|
+
try {
|
|
7321
|
+
const rows = db2.prepare(
|
|
7322
|
+
`SELECT * FROM ${SCORE_CONFIG_TABLE} ORDER BY CASE WHEN courseware_id = '${GLOBAL_SCORE_CONFIG_KEY}' THEN 0 ELSE 1 END, updated_at DESC`
|
|
7323
|
+
).all();
|
|
7324
|
+
return (rows || []).map(normalizeScoreConfig);
|
|
7325
|
+
} catch (e) {
|
|
7326
|
+
return [];
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
function pickInput(input, camel, snake) {
|
|
7330
|
+
if (input[camel] !== void 0) return input[camel];
|
|
7331
|
+
if (input[snake] !== void 0) return input[snake];
|
|
7332
|
+
return void 0;
|
|
7333
|
+
}
|
|
7334
|
+
function saveScoreConfig(db2, input) {
|
|
7335
|
+
if (!input || typeof input !== "object") throw new Error("save_score_config: invalid payload");
|
|
7336
|
+
const coursewareId = String(
|
|
7337
|
+
pickInput(input, "coursewareId", "courseware_id") ?? ""
|
|
7338
|
+
).trim();
|
|
7339
|
+
if (!coursewareId) throw new Error("save_score_config: coursewareId is required");
|
|
7340
|
+
if (coursewareId.length > 128) throw new Error("save_score_config: coursewareId is too long");
|
|
7341
|
+
const policyRaw = pickInput(input, "scorePolicy", "score_policy");
|
|
7342
|
+
if (policyRaw !== void 0 && policyRaw !== null && String(policyRaw).trim() !== "" && !isKnownScorePolicy(String(policyRaw))) {
|
|
7343
|
+
throw new Error(
|
|
7344
|
+
`save_score_config: unknown scorePolicy "${policyRaw}" (expected one of LATEST / MAX / AVERAGE / FIRST)`
|
|
7345
|
+
);
|
|
7346
|
+
}
|
|
7347
|
+
const fieldsRaw = pickInput(input, "scoreFields", "score_fields");
|
|
7348
|
+
const fields = Array.isArray(fieldsRaw) ? parseScoreFields(fieldsRaw).join(",") : parseScoreFields(fieldsRaw ?? "").join(",");
|
|
7349
|
+
if (fields.length > 2e3) throw new Error("save_score_config: scoreFields is too long");
|
|
7350
|
+
const nameRaw = pickInput(input, "coursewareName", "courseware_name");
|
|
7351
|
+
const lessonRaw = pickInput(input, "lessonId", "lesson_id");
|
|
7352
|
+
let existing = null;
|
|
7353
|
+
try {
|
|
7354
|
+
const row = db2.prepare(`SELECT * FROM ${SCORE_CONFIG_TABLE} WHERE courseware_id = ?`).get(coursewareId);
|
|
7355
|
+
if (row) existing = normalizeScoreConfig(row);
|
|
7356
|
+
} catch (e) {
|
|
7357
|
+
throw new Error("save_score_config: courseware_score_config table is missing (migration 004 not applied)");
|
|
7358
|
+
}
|
|
7359
|
+
const rawFull = toNumber(pickInput(input, "rawFullScore", "raw_full_score"));
|
|
7360
|
+
const targetFull = toNumber(pickInput(input, "targetFullScore", "target_full_score"));
|
|
7361
|
+
const weight = toNumber(pickInput(input, "weightPercentage", "weight_percentage"));
|
|
7362
|
+
if (rawFull !== void 0 && rawFull !== null && rawFull <= 0) throw new Error("save_score_config: rawFullScore must be > 0");
|
|
7363
|
+
if (targetFull !== void 0 && targetFull !== null && targetFull <= 0)
|
|
7364
|
+
throw new Error("save_score_config: targetFullScore must be > 0");
|
|
7365
|
+
if (weight !== void 0 && weight !== null && (weight < 0 || weight > 1e3))
|
|
7366
|
+
throw new Error("save_score_config: weightPercentage must be within [0, 1000]");
|
|
7367
|
+
const next = {
|
|
7368
|
+
courseware_id: coursewareId,
|
|
7369
|
+
courseware_name: nameRaw === void 0 ? existing?.courseware_name ?? (coursewareId === GLOBAL_SCORE_CONFIG_KEY ? "\u5168\u5C40\u9ED8\u8BA4\u7B56\u7565" : null) : nameRaw === null ? null : String(nameRaw).slice(0, 200),
|
|
7370
|
+
score_policy: policyRaw === void 0 || policyRaw === null || String(policyRaw).trim() === "" ? existing?.score_policy ?? DEFAULT_SCORE_POLICY : normalizeScorePolicy(policyRaw),
|
|
7371
|
+
score_fields: fields || (fieldsRaw === void 0 ? existing?.score_fields ?? "" : ""),
|
|
7372
|
+
raw_full_score: rawFull ?? existing?.raw_full_score ?? 100,
|
|
7373
|
+
target_full_score: targetFull ?? existing?.target_full_score ?? 100,
|
|
7374
|
+
weight_percentage: weight ?? existing?.weight_percentage ?? 100,
|
|
7375
|
+
lesson_id: lessonRaw === void 0 ? existing?.lesson_id ?? null : lessonRaw === null ? null : String(lessonRaw).slice(0, 128),
|
|
7376
|
+
updated_at: Date.now()
|
|
7377
|
+
};
|
|
7378
|
+
db2.prepare(
|
|
7379
|
+
`INSERT INTO ${SCORE_CONFIG_TABLE}
|
|
7380
|
+
(courseware_id, courseware_name, score_policy, score_fields, raw_full_score, target_full_score, weight_percentage, lesson_id, updated_at)
|
|
7381
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7382
|
+
ON CONFLICT(courseware_id) DO UPDATE SET
|
|
7383
|
+
courseware_name = excluded.courseware_name,
|
|
7384
|
+
score_policy = excluded.score_policy,
|
|
7385
|
+
score_fields = excluded.score_fields,
|
|
7386
|
+
raw_full_score = excluded.raw_full_score,
|
|
7387
|
+
target_full_score = excluded.target_full_score,
|
|
7388
|
+
weight_percentage = excluded.weight_percentage,
|
|
7389
|
+
lesson_id = excluded.lesson_id,
|
|
7390
|
+
updated_at = excluded.updated_at`
|
|
7391
|
+
).run(
|
|
7392
|
+
next.courseware_id,
|
|
7393
|
+
next.courseware_name,
|
|
7394
|
+
next.score_policy,
|
|
7395
|
+
next.score_fields,
|
|
7396
|
+
next.raw_full_score,
|
|
7397
|
+
next.target_full_score,
|
|
7398
|
+
next.weight_percentage,
|
|
7399
|
+
next.lesson_id,
|
|
7400
|
+
next.updated_at
|
|
7401
|
+
);
|
|
7402
|
+
return next;
|
|
7403
|
+
}
|
|
7404
|
+
function resolveCoursewareIdFromAttempt(db2, attemptId) {
|
|
7405
|
+
if (!attemptId) return null;
|
|
7406
|
+
try {
|
|
7407
|
+
const row = db2.prepare("SELECT courseware_id FROM courseware_attempt WHERE id = ?").get(attemptId);
|
|
7408
|
+
return row && row.courseware_id ? String(row.courseware_id) : null;
|
|
7409
|
+
} catch (e) {
|
|
7410
|
+
return null;
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7413
|
+
function collectScoreSamples(db2, attemptId, fields) {
|
|
7414
|
+
if (!attemptId) return [];
|
|
7415
|
+
let rows = [];
|
|
7416
|
+
try {
|
|
7417
|
+
rows = db2.prepare("SELECT event_type, payload_json, created_at FROM submission_raw WHERE attempt_id = ? ORDER BY created_at ASC").all(attemptId) || [];
|
|
7418
|
+
} catch (e) {
|
|
7419
|
+
return [];
|
|
7420
|
+
}
|
|
7421
|
+
const samples = [];
|
|
7422
|
+
for (const row of rows) {
|
|
7423
|
+
let payload = null;
|
|
7424
|
+
try {
|
|
7425
|
+
payload = JSON.parse(row?.payload_json ?? "null");
|
|
7426
|
+
} catch (e) {
|
|
7427
|
+
payload = null;
|
|
7428
|
+
}
|
|
7429
|
+
if (payload === null || payload === void 0) continue;
|
|
7430
|
+
const value = pickScoreFromPayload(payload, fields);
|
|
7431
|
+
if (value === null) continue;
|
|
7432
|
+
samples.push({
|
|
7433
|
+
value,
|
|
7434
|
+
at: toNumber(row?.created_at) ?? 0,
|
|
7435
|
+
eventType: String(row?.event_type ?? "")
|
|
7436
|
+
});
|
|
7437
|
+
}
|
|
7438
|
+
return samples;
|
|
7439
|
+
}
|
|
7440
|
+
function aggregateAttemptScore(db2, attemptId, options = {}) {
|
|
7441
|
+
const coursewareId = options.coursewareId === void 0 ? resolveCoursewareIdFromAttempt(db2, attemptId) : options.coursewareId;
|
|
7442
|
+
const resolved = resolveScoreConfig(db2, coursewareId);
|
|
7443
|
+
const hasExplicitConfig = resolved.source !== "builtin";
|
|
7444
|
+
const policyOverride = options.policy;
|
|
7445
|
+
const policy = policyOverride === void 0 || policyOverride === null || String(policyOverride).trim() === "" ? resolved.config.score_policy : normalizeScorePolicy(policyOverride);
|
|
7446
|
+
const scoreFields = options.scoreFields === void 0 ? resolved.config.score_fields : options.scoreFields;
|
|
7447
|
+
const rawFull = toNumber(options.rawFullScore);
|
|
7448
|
+
const targetFull = toNumber(options.targetFullScore);
|
|
7449
|
+
const weight = toNumber(options.weightPercentage);
|
|
7450
|
+
const config = {
|
|
7451
|
+
...resolved.config,
|
|
7452
|
+
score_policy: policy,
|
|
7453
|
+
score_fields: typeof scoreFields === "string" ? scoreFields : parseScoreFields(scoreFields).join(","),
|
|
7454
|
+
raw_full_score: rawFull !== null && rawFull > 0 ? rawFull : resolved.config.raw_full_score,
|
|
7455
|
+
target_full_score: targetFull !== null && targetFull > 0 ? targetFull : resolved.config.target_full_score,
|
|
7456
|
+
weight_percentage: weight !== null ? clamp(weight, 0, 1e3) : resolved.config.weight_percentage
|
|
7457
|
+
};
|
|
7458
|
+
const sampleDetails = collectScoreSamples(db2, attemptId, scoreFields);
|
|
7459
|
+
const samples = sampleDetails.map((s) => s.value);
|
|
7460
|
+
const rawAggregate = aggregateScores(samples, policy);
|
|
7461
|
+
let normalized = null;
|
|
7462
|
+
if (rawAggregate !== null) {
|
|
7463
|
+
if (!hasExplicitConfig) {
|
|
7464
|
+
normalized = rawAggregate;
|
|
7465
|
+
} else {
|
|
7466
|
+
const scaled = rawAggregate / config.raw_full_score * config.target_full_score;
|
|
7467
|
+
normalized = round2(clamp(scaled, 0, config.target_full_score));
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
return {
|
|
7471
|
+
policy,
|
|
7472
|
+
config,
|
|
7473
|
+
hasExplicitConfig,
|
|
7474
|
+
samples,
|
|
7475
|
+
sampleDetails,
|
|
7476
|
+
rawAggregate,
|
|
7477
|
+
normalized,
|
|
7478
|
+
finalScore: normalized,
|
|
7479
|
+
weightPercentage: config.weight_percentage
|
|
7480
|
+
};
|
|
7481
|
+
}
|
|
7482
|
+
function describeAggregation(aggregation) {
|
|
7483
|
+
return {
|
|
7484
|
+
policy: aggregation.policy,
|
|
7485
|
+
sample_count: aggregation.samples.length,
|
|
7486
|
+
samples: aggregation.samples.slice(-50),
|
|
7487
|
+
raw_aggregate: aggregation.rawAggregate,
|
|
7488
|
+
normalized: aggregation.normalized,
|
|
7489
|
+
raw_full_score: aggregation.config.raw_full_score,
|
|
7490
|
+
target_full_score: aggregation.config.target_full_score,
|
|
7491
|
+
weight_percentage: aggregation.weightPercentage,
|
|
7492
|
+
configured: aggregation.hasExplicitConfig,
|
|
7493
|
+
aggregated_at: Date.now()
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
|
|
6609
7497
|
// packages/plugins/builtin.ts
|
|
7498
|
+
var nativeRuntimeScriptRegistry = null;
|
|
6610
7499
|
function copyFolderSync(src, dest) {
|
|
6611
7500
|
if (import_fs3.default.cpSync) {
|
|
6612
7501
|
import_fs3.default.cpSync(src, dest, { recursive: true });
|
|
@@ -6647,6 +7536,21 @@ var BuiltinPlugin = {
|
|
|
6647
7536
|
const pluginHost = await ctx.resolve(IPluginHostToken);
|
|
6648
7537
|
const lifecycleManager = await ctx.resolve(IPluginLifecycleManagerToken);
|
|
6649
7538
|
const distributionManager = await ctx.resolve(IPluginDistributionManagerToken);
|
|
7539
|
+
try {
|
|
7540
|
+
const runtimeScriptRegistry = await ctx.resolve(ICoursewareRuntimeScriptRegistryToken);
|
|
7541
|
+
if (runtimeScriptRegistry && typeof runtimeScriptRegistry.register === "function") {
|
|
7542
|
+
runtimeScriptRegistry.register(SCORE_MONITOR_SCRIPT_OWNER, {
|
|
7543
|
+
id: SCORE_MONITOR_SCRIPT_ID,
|
|
7544
|
+
description: "\u5E73\u53F0\u539F\u751F\u5206\u6570\u53D8\u91CF\u76D1\u89C6\u5668\uFF1A\u91C7\u96C6\u8BFE\u4EF6\u5185\u4EE3\u8868\u5206\u6570\u7684\u53D8\u91CF\u5E76\u6301\u7EED\u4E0A\u62A5\u4E3A\u6210\u7EE9\u6837\u672C",
|
|
7545
|
+
source: SCORE_MONITOR_SCRIPT,
|
|
7546
|
+
position: "body-end",
|
|
7547
|
+
priority: 200
|
|
7548
|
+
});
|
|
7549
|
+
nativeRuntimeScriptRegistry = runtimeScriptRegistry;
|
|
7550
|
+
}
|
|
7551
|
+
} catch (err) {
|
|
7552
|
+
console.warn("[builtin] \u6CE8\u518C\u539F\u751F\u5206\u6570\u53D8\u91CF\u76D1\u89C6\u5668\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", err.message);
|
|
7553
|
+
}
|
|
6650
7554
|
const createLessonCmdType = "lesson.create";
|
|
6651
7555
|
await actionRegistry.register({
|
|
6652
7556
|
id: "core-lesson-create",
|
|
@@ -7975,6 +8879,17 @@ var BuiltinPlugin = {
|
|
|
7975
8879
|
attemptId
|
|
7976
8880
|
);
|
|
7977
8881
|
}
|
|
8882
|
+
let aggregation = null;
|
|
8883
|
+
try {
|
|
8884
|
+
const attemptRow = db2.prepare("SELECT courseware_id FROM courseware_attempt WHERE id = ?").get(attemptId);
|
|
8885
|
+
aggregation = aggregateAttemptScore(db2, attemptId, {
|
|
8886
|
+
coursewareId: attemptRow?.courseware_id ?? null
|
|
8887
|
+
});
|
|
8888
|
+
} catch (aggErr) {
|
|
8889
|
+
console.warn("[courseware.submit_attempt] score aggregation failed, fallback to latest score:", aggErr);
|
|
8890
|
+
}
|
|
8891
|
+
const aggregatedScore = aggregation && aggregation.finalScore !== null ? aggregation.finalScore : null;
|
|
8892
|
+
const aggregationExtra = aggregation ? { score_aggregation: describeAggregation(aggregation) } : {};
|
|
7978
8893
|
const existing = db2.prepare("SELECT * FROM submission_result WHERE attempt_id = ?").get(attemptId);
|
|
7979
8894
|
if (!existing) {
|
|
7980
8895
|
db2.prepare(
|
|
@@ -7982,13 +8897,13 @@ var BuiltinPlugin = {
|
|
|
7982
8897
|
).run(
|
|
7983
8898
|
"res_" + import_crypto3.default.randomBytes(8).toString("hex"),
|
|
7984
8899
|
attemptId,
|
|
7985
|
-
score !== void 0 ? score : null,
|
|
8900
|
+
aggregatedScore !== null ? aggregatedScore : score !== void 0 ? score : null,
|
|
7986
8901
|
comment || null,
|
|
7987
8902
|
completion !== void 0 ? completion : null,
|
|
7988
|
-
JSON.stringify(extra)
|
|
8903
|
+
JSON.stringify({ ...extra, ...aggregationExtra })
|
|
7989
8904
|
);
|
|
7990
8905
|
} else {
|
|
7991
|
-
const finalScore = score !== void 0 ? score : existing.score;
|
|
8906
|
+
const finalScore = aggregatedScore !== null ? aggregatedScore : score !== void 0 ? score : existing.score;
|
|
7992
8907
|
const finalComment = comment || existing.comment;
|
|
7993
8908
|
const finalCompletion = completion !== void 0 ? completion : existing.completion;
|
|
7994
8909
|
let mergedExtra = {};
|
|
@@ -7996,7 +8911,7 @@ var BuiltinPlugin = {
|
|
|
7996
8911
|
mergedExtra = JSON.parse(existing.extra_json || "{}");
|
|
7997
8912
|
} catch (e) {
|
|
7998
8913
|
}
|
|
7999
|
-
mergedExtra = { ...mergedExtra, ...extra };
|
|
8914
|
+
mergedExtra = { ...mergedExtra, ...extra, ...aggregationExtra };
|
|
8000
8915
|
db2.prepare(
|
|
8001
8916
|
"UPDATE submission_result SET score = ?, comment = ?, completion = ?, extra_json = ? WHERE attempt_id = ?"
|
|
8002
8917
|
).run(finalScore, finalComment, finalCompletion, JSON.stringify(mergedExtra), attemptId);
|
|
@@ -8012,31 +8927,158 @@ var BuiltinPlugin = {
|
|
|
8012
8927
|
return { success: true };
|
|
8013
8928
|
}
|
|
8014
8929
|
});
|
|
8015
|
-
const
|
|
8930
|
+
const getScoreConfigCmdType = "courseware.get_score_config";
|
|
8016
8931
|
await actionRegistry.register({
|
|
8017
|
-
id: "core-courseware-get-
|
|
8018
|
-
commandType:
|
|
8019
|
-
description: "
|
|
8932
|
+
id: "core-courseware-get-score-config",
|
|
8933
|
+
commandType: getScoreConfigCmdType,
|
|
8934
|
+
description: "\u8BFB\u53D6\u4E92\u52A8\u8BFE\u4EF6\u7684\u6210\u7EE9\u5F52\u96C6\u914D\u7F6E\uFF08\u7B56\u7565 / \u5B57\u6BB5 / \u6EE1\u5206 / \u6743\u91CD\uFF09",
|
|
8020
8935
|
capabilityRequired: "lesson:read",
|
|
8021
8936
|
inputSchema: {
|
|
8022
8937
|
type: "OBJECT",
|
|
8023
8938
|
properties: {
|
|
8024
|
-
|
|
8025
|
-
}
|
|
8026
|
-
required: ["attemptId"]
|
|
8939
|
+
coursewareId: { type: "STRING", description: "\u8BFE\u4EF6 ID\uFF08\u7701\u7565\u6216\u4F20 '*' \u65F6\u8BFB\u53D6\u5168\u5C40\u9ED8\u8BA4\u914D\u7F6E\uFF09" }
|
|
8940
|
+
}
|
|
8027
8941
|
}
|
|
8028
8942
|
});
|
|
8029
|
-
await commandBus.registerHandler(
|
|
8943
|
+
await commandBus.registerHandler(getScoreConfigCmdType, {
|
|
8030
8944
|
async execute(command) {
|
|
8031
|
-
const payload = command.payload;
|
|
8032
|
-
const
|
|
8033
|
-
|
|
8034
|
-
return rows;
|
|
8945
|
+
const payload = command.payload || {};
|
|
8946
|
+
const resolved = resolveScoreConfig(db2, payload.coursewareId ?? payload.courseware_id ?? null);
|
|
8947
|
+
return { success: true, source: resolved.source, ...resolved.config };
|
|
8035
8948
|
}
|
|
8036
8949
|
});
|
|
8037
|
-
const
|
|
8950
|
+
const saveScoreConfigCmdType = "courseware.save_score_config";
|
|
8038
8951
|
await actionRegistry.register({
|
|
8039
|
-
id: "core-courseware-
|
|
8952
|
+
id: "core-courseware-save-score-config",
|
|
8953
|
+
commandType: saveScoreConfigCmdType,
|
|
8954
|
+
description: "\u4FDD\u5B58\u4E92\u52A8\u8BFE\u4EF6\u7684\u6210\u7EE9\u5F52\u96C6\u914D\u7F6E",
|
|
8955
|
+
capabilityRequired: "lesson:write",
|
|
8956
|
+
inputSchema: {
|
|
8957
|
+
type: "OBJECT",
|
|
8958
|
+
properties: {
|
|
8959
|
+
coursewareId: { type: "STRING", description: "\u8BFE\u4EF6 ID\uFF08\u4F20 '*' \u8868\u793A\u5168\u5C40\u9ED8\u8BA4\u7B56\u7565\uFF09" },
|
|
8960
|
+
coursewareName: { type: "STRING", description: "\u8BFE\u4EF6\u540D\u79F0\uFF08\u4FBF\u4E8E\u8BC6\u522B\uFF09" },
|
|
8961
|
+
scorePolicy: { type: "STRING", description: "LATEST | MAX | AVERAGE | FIRST" },
|
|
8962
|
+
scoreFields: { type: "STRING", description: "\u5206\u6570\u6240\u5728\u5B57\u6BB5\u8DEF\u5F84\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u5982 score,data.points" },
|
|
8963
|
+
rawFullScore: { type: "NUMBER", description: "\u8BFE\u4EF6\u539F\u59CB\u6EE1\u5206" },
|
|
8964
|
+
targetFullScore: { type: "NUMBER", description: "\u5F52\u96C6\u540E\u7684\u76EE\u6807\u6EE1\u5206" },
|
|
8965
|
+
weightPercentage: { type: "NUMBER", description: "\u8BE5\u8BFE\u4EF6\u5728\u603B\u8BC4\u4E2D\u7684\u6743\u91CD\uFF08\u767E\u5206\u6BD4\uFF09" },
|
|
8966
|
+
lessonId: { type: "STRING", description: "\u5173\u8054\u8BFE\u65F6 ID\uFF08\u53EF\u9009\uFF09" }
|
|
8967
|
+
},
|
|
8968
|
+
required: ["coursewareId"]
|
|
8969
|
+
}
|
|
8970
|
+
});
|
|
8971
|
+
await commandBus.registerHandler(saveScoreConfigCmdType, {
|
|
8972
|
+
async execute(command) {
|
|
8973
|
+
const saved = saveScoreConfig(db2, command.payload || {});
|
|
8974
|
+
await eventBus.publish({
|
|
8975
|
+
id: (0, import_uuid6.v7)(),
|
|
8976
|
+
type: "courseware.score_config_saved",
|
|
8977
|
+
source: "builtin.courseware",
|
|
8978
|
+
payload: { ...saved },
|
|
8979
|
+
timestamp: Date.now(),
|
|
8980
|
+
correlationId: command.id
|
|
8981
|
+
});
|
|
8982
|
+
return { success: true, ...saved };
|
|
8983
|
+
}
|
|
8984
|
+
});
|
|
8985
|
+
const listScoreConfigsCmdType = "courseware.list_score_configs";
|
|
8986
|
+
await actionRegistry.register({
|
|
8987
|
+
id: "core-courseware-list-score-configs",
|
|
8988
|
+
commandType: listScoreConfigsCmdType,
|
|
8989
|
+
description: "\u5217\u51FA\u6240\u6709\u5DF2\u914D\u7F6E\u7684\u8BFE\u4EF6\u6210\u7EE9\u5F52\u96C6\u7B56\u7565",
|
|
8990
|
+
capabilityRequired: "lesson:read",
|
|
8991
|
+
inputSchema: { type: "OBJECT", properties: {} }
|
|
8992
|
+
});
|
|
8993
|
+
await commandBus.registerHandler(listScoreConfigsCmdType, {
|
|
8994
|
+
async execute() {
|
|
8995
|
+
return { success: true, configs: listScoreConfigs(db2) };
|
|
8996
|
+
}
|
|
8997
|
+
});
|
|
8998
|
+
const regradeCmdType = "courseware.regrade_attempts";
|
|
8999
|
+
await actionRegistry.register({
|
|
9000
|
+
id: "core-courseware-regrade-attempts",
|
|
9001
|
+
commandType: regradeCmdType,
|
|
9002
|
+
description: "\u6309\u5F53\u524D\u6210\u7EE9\u5F52\u96C6\u7B56\u7565\u91CD\u7B97\u65E2\u6709 attempt \u7684\u5B98\u65B9\u6210\u7EE9\uFF08\u6539\u914D\u7F6E\u540E\u56DE\u586B\uFF09",
|
|
9003
|
+
capabilityRequired: "lesson:write",
|
|
9004
|
+
inputSchema: {
|
|
9005
|
+
type: "OBJECT",
|
|
9006
|
+
properties: {
|
|
9007
|
+
attemptId: { type: "STRING", description: "\u5355\u4E2A attempt ID\uFF08\u4E0E coursewareId \u4E8C\u9009\u4E00\uFF09" },
|
|
9008
|
+
coursewareId: { type: "STRING", description: "\u6309\u8BFE\u4EF6\u6279\u91CF\u91CD\u7B97\uFF08\u6700\u591A 2000 \u4E2A attempt\uFF09" },
|
|
9009
|
+
limit: { type: "NUMBER", description: "\u6279\u91CF\u91CD\u7B97\u4E0A\u9650\uFF0C\u9ED8\u8BA4 500" }
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
});
|
|
9013
|
+
await commandBus.registerHandler(regradeCmdType, {
|
|
9014
|
+
async execute(command) {
|
|
9015
|
+
const payload = command.payload || {};
|
|
9016
|
+
const attemptId = typeof payload.attemptId === "string" ? payload.attemptId.trim() : "";
|
|
9017
|
+
const coursewareId = typeof payload.coursewareId === "string" ? payload.coursewareId.trim() : "";
|
|
9018
|
+
if (!attemptId && !coursewareId) {
|
|
9019
|
+
throw new Error("courseware.regrade_attempts: attemptId or coursewareId is required");
|
|
9020
|
+
}
|
|
9021
|
+
const requestedLimit = Number(payload.limit);
|
|
9022
|
+
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 2e3) : 500;
|
|
9023
|
+
let attemptIds = [];
|
|
9024
|
+
if (attemptId) {
|
|
9025
|
+
attemptIds = [attemptId];
|
|
9026
|
+
} else {
|
|
9027
|
+
const rows = db2.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? ORDER BY started_at DESC LIMIT ?").all(coursewareId, limit);
|
|
9028
|
+
attemptIds = (rows || []).map((r) => String(r.id));
|
|
9029
|
+
}
|
|
9030
|
+
const regraded = [];
|
|
9031
|
+
let failed = 0;
|
|
9032
|
+
for (const id of attemptIds) {
|
|
9033
|
+
try {
|
|
9034
|
+
const agg = aggregateAttemptScore(db2, id, { coursewareId: coursewareId || null });
|
|
9035
|
+
if (agg.finalScore === null) continue;
|
|
9036
|
+
const current = db2.prepare("SELECT * FROM submission_result WHERE attempt_id = ?").get(id);
|
|
9037
|
+
if (!current) continue;
|
|
9038
|
+
let mergedExtra = {};
|
|
9039
|
+
try {
|
|
9040
|
+
mergedExtra = JSON.parse(current.extra_json || "{}");
|
|
9041
|
+
} catch (e) {
|
|
9042
|
+
}
|
|
9043
|
+
mergedExtra = { ...mergedExtra, score_aggregation: describeAggregation(agg) };
|
|
9044
|
+
db2.prepare("UPDATE submission_result SET score = ?, extra_json = ? WHERE attempt_id = ?").run(
|
|
9045
|
+
agg.finalScore,
|
|
9046
|
+
JSON.stringify(mergedExtra),
|
|
9047
|
+
id
|
|
9048
|
+
);
|
|
9049
|
+
regraded.push({ attemptId: id, score: agg.finalScore, samples: agg.samples.length, policy: agg.policy });
|
|
9050
|
+
} catch (e) {
|
|
9051
|
+
failed += 1;
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
9054
|
+
return { success: true, scanned: attemptIds.length, regraded: regraded.length, failed, results: regraded };
|
|
9055
|
+
}
|
|
9056
|
+
});
|
|
9057
|
+
const getAttemptRawDataCmdType = "courseware.get_attempt_raw_data";
|
|
9058
|
+
await actionRegistry.register({
|
|
9059
|
+
id: "core-courseware-get-attempt-raw-data",
|
|
9060
|
+
commandType: getAttemptRawDataCmdType,
|
|
9061
|
+
description: "Get all captured raw events/submits for a student attempt",
|
|
9062
|
+
capabilityRequired: "lesson:read",
|
|
9063
|
+
inputSchema: {
|
|
9064
|
+
type: "OBJECT",
|
|
9065
|
+
properties: {
|
|
9066
|
+
attemptId: { type: "STRING", description: "ID of the attempt to query" }
|
|
9067
|
+
},
|
|
9068
|
+
required: ["attemptId"]
|
|
9069
|
+
}
|
|
9070
|
+
});
|
|
9071
|
+
await commandBus.registerHandler(getAttemptRawDataCmdType, {
|
|
9072
|
+
async execute(command) {
|
|
9073
|
+
const payload = command.payload;
|
|
9074
|
+
const { attemptId } = payload;
|
|
9075
|
+
const rows = db2.prepare("SELECT * FROM submission_raw WHERE attempt_id = ? ORDER BY created_at ASC").all(attemptId);
|
|
9076
|
+
return rows;
|
|
9077
|
+
}
|
|
9078
|
+
});
|
|
9079
|
+
const listCoursewareCmdType = "courseware.list";
|
|
9080
|
+
await actionRegistry.register({
|
|
9081
|
+
id: "core-courseware-list",
|
|
8040
9082
|
commandType: listCoursewareCmdType,
|
|
8041
9083
|
description: "List all uploaded interactive coursewares",
|
|
8042
9084
|
capabilityRequired: "lesson:read",
|
|
@@ -8090,6 +9132,14 @@ var BuiltinPlugin = {
|
|
|
8090
9132
|
});
|
|
8091
9133
|
},
|
|
8092
9134
|
deactivate: async () => {
|
|
9135
|
+
try {
|
|
9136
|
+
if (nativeRuntimeScriptRegistry && typeof nativeRuntimeScriptRegistry.clear === "function") {
|
|
9137
|
+
nativeRuntimeScriptRegistry.clear(SCORE_MONITOR_SCRIPT_OWNER);
|
|
9138
|
+
nativeRuntimeScriptRegistry = null;
|
|
9139
|
+
}
|
|
9140
|
+
} catch (err) {
|
|
9141
|
+
console.warn("[builtin] \u56DE\u6536\u539F\u751F\u5206\u6570\u53D8\u91CF\u76D1\u89C6\u5668\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", err.message);
|
|
9142
|
+
}
|
|
8093
9143
|
}
|
|
8094
9144
|
};
|
|
8095
9145
|
|
|
@@ -8295,6 +9345,27 @@ var AiPlannerPlugin = {
|
|
|
8295
9345
|
// packages/plugins/assignment-eval.ts
|
|
8296
9346
|
var import_uuid8 = require("uuid");
|
|
8297
9347
|
var import_plugin_sdk6 = require("@openlearn/plugin-sdk");
|
|
9348
|
+
var PRIVILEGED_ROLES = ["teacher", "administrator", "admin"];
|
|
9349
|
+
function parseActorId(actorId) {
|
|
9350
|
+
const match = /^user:([^:]+):(.+)$/.exec(String(actorId || ""));
|
|
9351
|
+
if (!match) return null;
|
|
9352
|
+
return { userId: match[1], role: match[2] };
|
|
9353
|
+
}
|
|
9354
|
+
function isPrivilegedActor(actorId) {
|
|
9355
|
+
const actor = parseActorId(actorId);
|
|
9356
|
+
if (!actor) return true;
|
|
9357
|
+
return PRIVILEGED_ROLES.includes(actor.role);
|
|
9358
|
+
}
|
|
9359
|
+
function actorUserId(actorId) {
|
|
9360
|
+
return parseActorId(actorId)?.userId ?? actorId ?? null;
|
|
9361
|
+
}
|
|
9362
|
+
function assertStudentOwnership(actorId, studentId, action) {
|
|
9363
|
+
if (isPrivilegedActor(actorId)) return;
|
|
9364
|
+
const actor = parseActorId(actorId);
|
|
9365
|
+
if (!actor || actor.userId !== studentId) {
|
|
9366
|
+
throw new Error(`Access Denied: Students can only ${action} their own assignment`);
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
8298
9369
|
var AssignmentEvalPlugin = {
|
|
8299
9370
|
manifest: {
|
|
8300
9371
|
id: "@openlearn/plugin-assignment-eval",
|
|
@@ -8305,140 +9376,632 @@ var AssignmentEvalPlugin = {
|
|
|
8305
9376
|
"@openlearn/core:ICommandBusService@^1.0.0",
|
|
8306
9377
|
"@openlearn/core:IActionRegistryService@^1.0.0",
|
|
8307
9378
|
"@openlearn/core:IDatabase@^1.0.0",
|
|
8308
|
-
"@openlearn/core:ISemesterGradeService@^1.0.0"
|
|
9379
|
+
"@openlearn/core:ISemesterGradeService@^1.0.0",
|
|
9380
|
+
"@openlearn/core:IEventBusService@^1.0.0"
|
|
8309
9381
|
],
|
|
8310
|
-
capabilitiesProposed: ["
|
|
9382
|
+
capabilitiesProposed: ["assignment:read", "assignment:submit", "assignment:review", "assignment:manage"],
|
|
8311
9383
|
engines: { openlearn: ">=0.2.5" }
|
|
8312
9384
|
},
|
|
8313
9385
|
activate: async (ctx) => {
|
|
8314
9386
|
const commandBus = ctx.services.commandBus;
|
|
8315
9387
|
const actionRegistry = ctx.services.actionRegistry;
|
|
9388
|
+
const eventBus = ctx.services.eventBus;
|
|
8316
9389
|
const db2 = await ctx.resolve(import_plugin_sdk6.IDatabaseToken);
|
|
8317
9390
|
const gradeService = await ctx.resolve(import_plugin_sdk6.ISemesterGradeServiceToken);
|
|
9391
|
+
const OWNED_COMMAND_TYPES = [
|
|
9392
|
+
"assignment.create",
|
|
9393
|
+
"assignment.list",
|
|
9394
|
+
"assignment.get",
|
|
9395
|
+
"assignment.submit",
|
|
9396
|
+
"assignment.peer_review",
|
|
9397
|
+
"assignment.assign_peer_reviews",
|
|
9398
|
+
"assignment.grade"
|
|
9399
|
+
];
|
|
9400
|
+
const OWNED_ACTION_IDS = [
|
|
9401
|
+
"eval-assignment-create",
|
|
9402
|
+
"eval-assignment-list",
|
|
9403
|
+
"eval-assignment-get",
|
|
9404
|
+
"eval-assignment-submit",
|
|
9405
|
+
"eval-assignment-peer-review",
|
|
9406
|
+
"eval-assignment-assign-peer-reviews",
|
|
9407
|
+
"eval-assignment-grade"
|
|
9408
|
+
];
|
|
9409
|
+
for (const type of OWNED_COMMAND_TYPES) {
|
|
9410
|
+
commandBus.unregisterHandler(type);
|
|
9411
|
+
}
|
|
9412
|
+
for (const id of OWNED_ACTION_IDS) {
|
|
9413
|
+
actionRegistry.unregister(id);
|
|
9414
|
+
}
|
|
8318
9415
|
commandBus.unregisterHandler("assignment.submit");
|
|
8319
9416
|
commandBus.unregisterHandler("assignment.grade");
|
|
9417
|
+
actionRegistry.unregister("core-assignment-create");
|
|
8320
9418
|
actionRegistry.unregister("core-assignment-submit");
|
|
8321
9419
|
actionRegistry.unregister("core-assignment-grade");
|
|
8322
|
-
const
|
|
9420
|
+
const loadAssignment = (assignmentId) => db2.prepare("SELECT * FROM plugin_assignments WHERE id = ?").get(assignmentId);
|
|
9421
|
+
const resolveAssignment = (assignmentId, lessonId) => {
|
|
9422
|
+
if (assignmentId) {
|
|
9423
|
+
const row = loadAssignment(assignmentId);
|
|
9424
|
+
if (!row) throw new Error(`Assignment not found: ${assignmentId}`);
|
|
9425
|
+
return row;
|
|
9426
|
+
}
|
|
9427
|
+
if (lessonId) {
|
|
9428
|
+
const existing = db2.prepare("SELECT * FROM plugin_assignments WHERE lesson_id = ? ORDER BY created_at ASC LIMIT 1").get(lessonId);
|
|
9429
|
+
if (existing) return existing;
|
|
9430
|
+
const now = Date.now();
|
|
9431
|
+
const autoId = "asg-" + (0, import_uuid8.v7)();
|
|
9432
|
+
db2.prepare(
|
|
9433
|
+
`INSERT INTO plugin_assignments
|
|
9434
|
+
(id, class_id, lesson_id, element_id, title, description, status, created_by, created_at, updated_at)
|
|
9435
|
+
VALUES (?, ?, ?, ?, ?, ?, 'published', ?, ?, ?)`
|
|
9436
|
+
).run(autoId, null, lessonId, null, "\u672C\u8BFE\u4F5C\u4E1A", "\u7531\u8BFE\u65F6\u4F5C\u4E1A\u5165\u53E3\u81EA\u52A8\u521B\u5EFA", null, now, now);
|
|
9437
|
+
return loadAssignment(autoId);
|
|
9438
|
+
}
|
|
9439
|
+
throw new Error("Missing required params: assignmentId or lessonId");
|
|
9440
|
+
};
|
|
9441
|
+
const latestVersionOfSubmission = (submissionId) => db2.prepare(
|
|
9442
|
+
"SELECT id, version, text_content, link_url, submitted_at, is_late FROM plugin_submission_versions WHERE submission_id = ? ORDER BY version DESC LIMIT 1"
|
|
9443
|
+
).get(submissionId);
|
|
9444
|
+
const filesOfVersion = (versionId) => db2.prepare(
|
|
9445
|
+
"SELECT id, original_name, size, mime FROM plugin_assignment_files WHERE version_id = ? AND deleted_at IS NULL ORDER BY uploaded_at ASC"
|
|
9446
|
+
).all(versionId);
|
|
9447
|
+
const buildPeerReviewTasks = (assignmentId, reviewerId) => {
|
|
9448
|
+
const tasks = db2.prepare(
|
|
9449
|
+
"SELECT id, submission_id, anonymous, status, due_at, created_at FROM plugin_peer_review_tasks WHERE assignment_id = ? AND reviewer_id = ? ORDER BY created_at ASC"
|
|
9450
|
+
).all(assignmentId, reviewerId);
|
|
9451
|
+
return tasks.map((task) => {
|
|
9452
|
+
const version = latestVersionOfSubmission(task.submission_id);
|
|
9453
|
+
const review = db2.prepare(
|
|
9454
|
+
"SELECT score, comment, status, updated_at FROM plugin_peer_reviews WHERE submission_id = ? AND reviewer_id = ?"
|
|
9455
|
+
).get(task.submission_id, reviewerId);
|
|
9456
|
+
const submissionUpdatedAt = db2.prepare("SELECT updated_at FROM plugin_submissions WHERE id = ?").get(task.submission_id)?.updated_at ?? null;
|
|
9457
|
+
return {
|
|
9458
|
+
taskId: task.id,
|
|
9459
|
+
submissionId: task.submission_id,
|
|
9460
|
+
status: task.status,
|
|
9461
|
+
anonymous: Number(task.anonymous) === 1,
|
|
9462
|
+
dueAt: task.due_at ?? null,
|
|
9463
|
+
createdAt: task.created_at,
|
|
9464
|
+
// 我评完之后作者又改过提交 → 提醒复核
|
|
9465
|
+
stale: Boolean(
|
|
9466
|
+
review?.status === "submitted" && submissionUpdatedAt && Number(review.updated_at || 0) < Number(submissionUpdatedAt)
|
|
9467
|
+
),
|
|
9468
|
+
review: review ? { score: review.score, comment: review.comment || "", submittedAt: review.updated_at ?? null } : null,
|
|
9469
|
+
submission: version ? {
|
|
9470
|
+
version: version.version,
|
|
9471
|
+
textContent: version.text_content || "",
|
|
9472
|
+
linkUrl: version.link_url || "",
|
|
9473
|
+
submittedAt: version.submitted_at,
|
|
9474
|
+
isLate: Number(version.is_late) === 1,
|
|
9475
|
+
files: filesOfVersion(version.id)
|
|
9476
|
+
} : null
|
|
9477
|
+
};
|
|
9478
|
+
});
|
|
9479
|
+
};
|
|
9480
|
+
const buildPeerProgress = (assignmentId) => {
|
|
9481
|
+
const nameOf = (studentId) => {
|
|
9482
|
+
try {
|
|
9483
|
+
const row = db2.prepare("SELECT name FROM students WHERE id = ?").get(studentId);
|
|
9484
|
+
return row?.name || studentId;
|
|
9485
|
+
} catch {
|
|
9486
|
+
return studentId;
|
|
9487
|
+
}
|
|
9488
|
+
};
|
|
9489
|
+
const tasks = db2.prepare("SELECT reviewer_id, status FROM plugin_peer_review_tasks WHERE assignment_id = ?").all(assignmentId);
|
|
9490
|
+
const byReviewer = /* @__PURE__ */ new Map();
|
|
9491
|
+
for (const task of tasks) {
|
|
9492
|
+
const entry = byReviewer.get(task.reviewer_id) || { studentId: task.reviewer_id, name: nameOf(task.reviewer_id), pending: 0, submitted: 0 };
|
|
9493
|
+
if (task.status === "submitted") entry.submitted += 1;
|
|
9494
|
+
else entry.pending += 1;
|
|
9495
|
+
byReviewer.set(task.reviewer_id, entry);
|
|
9496
|
+
}
|
|
9497
|
+
const flags = [];
|
|
9498
|
+
for (const entry of byReviewer.values()) {
|
|
9499
|
+
if (entry.pending > 0) {
|
|
9500
|
+
flags.push({
|
|
9501
|
+
type: "peer_review_pending",
|
|
9502
|
+
reviewerId: entry.studentId,
|
|
9503
|
+
detail: `${entry.name} \u8FD8\u6709 ${entry.pending} \u4EFD\u4E92\u8BC4\u672A\u5B8C\u6210`
|
|
9504
|
+
});
|
|
9505
|
+
}
|
|
9506
|
+
if (entry.submitted >= 2) {
|
|
9507
|
+
const full = db2.prepare(
|
|
9508
|
+
"SELECT COUNT(*) AS c FROM plugin_peer_reviews WHERE assignment_id = ? AND reviewer_id = ? AND score >= 100"
|
|
9509
|
+
).get(assignmentId, entry.studentId).c;
|
|
9510
|
+
if (full >= entry.submitted) {
|
|
9511
|
+
flags.push({
|
|
9512
|
+
type: "all_full_marks",
|
|
9513
|
+
reviewerId: entry.studentId,
|
|
9514
|
+
detail: `${entry.name} \u7684 ${entry.submitted} \u4EFD\u4E92\u8BC4\u5168\u90E8\u7ED9\u4E86\u6EE1\u5206\uFF0C\u5EFA\u8BAE\u590D\u6838`
|
|
9515
|
+
});
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
}
|
|
9519
|
+
const gaps = db2.prepare(
|
|
9520
|
+
`SELECT r.reviewer_id AS reviewerId, r.score AS peerScore, g.teacher_score AS teacherScore, s.student_id AS authorId
|
|
9521
|
+
FROM plugin_peer_reviews r
|
|
9522
|
+
JOIN plugin_grades g ON g.submission_id = r.submission_id
|
|
9523
|
+
JOIN plugin_submissions s ON s.id = r.submission_id
|
|
9524
|
+
WHERE r.assignment_id = ? AND g.teacher_score IS NOT NULL`
|
|
9525
|
+
).all(assignmentId);
|
|
9526
|
+
for (const gap of gaps) {
|
|
9527
|
+
if (Math.abs(Number(gap.peerScore) - Number(gap.teacherScore)) <= 25) continue;
|
|
9528
|
+
flags.push({
|
|
9529
|
+
type: "score_gap",
|
|
9530
|
+
reviewerId: gap.reviewerId,
|
|
9531
|
+
detail: `${nameOf(gap.reviewerId)} \u7ED9 ${nameOf(gap.authorId)} \u7684\u4E92\u8BC4\u5206 ${gap.peerScore}\uFF0C\u4E0E\u6559\u5E08\u8BC4\u5206 ${gap.teacherScore} \u76F8\u5DEE\u8F83\u5927`
|
|
9532
|
+
});
|
|
9533
|
+
}
|
|
9534
|
+
const submissionCount = db2.prepare("SELECT COUNT(*) AS c FROM plugin_submissions WHERE assignment_id = ?").get(assignmentId).c;
|
|
9535
|
+
const completed = tasks.filter((t) => t.status === "submitted").length;
|
|
9536
|
+
return {
|
|
9537
|
+
submissions: submissionCount,
|
|
9538
|
+
tasks: tasks.length,
|
|
9539
|
+
completed,
|
|
9540
|
+
pending: tasks.length - completed,
|
|
9541
|
+
reviewers: Array.from(byReviewer.values()).sort((a, b) => b.pending - a.pending),
|
|
9542
|
+
flags
|
|
9543
|
+
};
|
|
9544
|
+
};
|
|
9545
|
+
const publishEvent2 = async (type, payload, correlationId) => {
|
|
9546
|
+
if (!eventBus || typeof eventBus.publish !== "function") return;
|
|
9547
|
+
try {
|
|
9548
|
+
await eventBus.publish({
|
|
9549
|
+
id: (0, import_uuid8.v7)(),
|
|
9550
|
+
type,
|
|
9551
|
+
source: "assignment-eval",
|
|
9552
|
+
payload,
|
|
9553
|
+
timestamp: Date.now(),
|
|
9554
|
+
correlationId
|
|
9555
|
+
});
|
|
9556
|
+
} catch (err) {
|
|
9557
|
+
ctx.log?.warn?.(`[assignment-eval] \u53D1\u5E03\u4E8B\u4EF6 ${type} \u5931\u8D25: ${err?.message}`);
|
|
9558
|
+
}
|
|
9559
|
+
};
|
|
9560
|
+
await actionRegistry.register({
|
|
9561
|
+
id: "eval-assignment-create",
|
|
9562
|
+
commandType: "assignment.create",
|
|
9563
|
+
description: "\u521B\u5EFA\u6216\u66F4\u65B0\u4F5C\u4E1A\u5B9E\u4F53\uFF08\u53EF\u540C\u65F6\u7ED1\u5B9A\u8BFE\u65F6\u3001\u73ED\u7EA7\u4E0E\u767D\u677F\u5143\u7D20\uFF09",
|
|
9564
|
+
capabilityRequired: "assignment:manage",
|
|
9565
|
+
inputSchema: {
|
|
9566
|
+
type: "OBJECT",
|
|
9567
|
+
properties: {
|
|
9568
|
+
assignmentId: { type: "STRING", description: "\u5DF2\u5B58\u5728\u5219\u66F4\u65B0\uFF0C\u7F3A\u7701\u4E3A\u65B0\u5EFA" },
|
|
9569
|
+
title: { type: "STRING", description: "\u4F5C\u4E1A\u6807\u9898" },
|
|
9570
|
+
description: { type: "STRING", description: "\u4F5C\u4E1A\u8BF4\u660E" },
|
|
9571
|
+
instructions: { type: "STRING", description: "\u4F5C\u7B54\u8981\u6C42" },
|
|
9572
|
+
lessonId: { type: "STRING", description: "\u5173\u8054\u8BFE\u65F6 ID" },
|
|
9573
|
+
classId: { type: "STRING", description: "\u5173\u8054\u73ED\u7EA7 ID" },
|
|
9574
|
+
elementId: { type: "STRING", description: "\u767D\u677F\u6559\u5B66\u5BF9\u8C61 ID" },
|
|
9575
|
+
dueAt: { type: "NUMBER", description: "\u622A\u6B62\u65F6\u95F4\uFF08\u6BEB\u79D2\u65F6\u95F4\u6233\uFF09" },
|
|
9576
|
+
allowLate: { type: "BOOLEAN", description: "\u662F\u5426\u5141\u8BB8\u8FDF\u4EA4" },
|
|
9577
|
+
allowText: { type: "BOOLEAN", description: "\u662F\u5426\u5141\u8BB8\u6587\u672C\u4F5C\u7B54" },
|
|
9578
|
+
allowLink: { type: "BOOLEAN", description: "\u662F\u5426\u5141\u8BB8\u94FE\u63A5\u4F5C\u7B54" },
|
|
9579
|
+
maxFiles: { type: "NUMBER", description: "\u6700\u591A\u6587\u4EF6\u6570" },
|
|
9580
|
+
maxFileSize: { type: "NUMBER", description: "\u5355\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\uFF08\u5B57\u8282\uFF09" },
|
|
9581
|
+
peerReviewCount: { type: "NUMBER", description: "\u6BCF\u4EBA\u9700\u8981\u4E92\u8BC4\u7684\u4EFD\u6570" },
|
|
9582
|
+
teacherWeight: { type: "NUMBER", description: "\u6559\u5E08\u8BC4\u5206\u6743\u91CD" },
|
|
9583
|
+
peerWeight: { type: "NUMBER", description: "\u4E92\u8BC4\u5E73\u5747\u5206\u6743\u91CD" },
|
|
9584
|
+
status: { type: "STRING", description: "published / draft / archived" }
|
|
9585
|
+
},
|
|
9586
|
+
required: ["title"]
|
|
9587
|
+
}
|
|
9588
|
+
});
|
|
9589
|
+
await commandBus.registerHandler("assignment.create", {
|
|
9590
|
+
async execute(command) {
|
|
9591
|
+
const payload = command.payload;
|
|
9592
|
+
const now = Date.now();
|
|
9593
|
+
const teacherWeight = payload.teacherWeight === void 0 ? 0.6 : Number(payload.teacherWeight);
|
|
9594
|
+
const peerWeight = payload.peerWeight === void 0 ? 0.4 : Number(payload.peerWeight);
|
|
9595
|
+
if (Math.abs(teacherWeight + peerWeight - 1) > 1e-3) {
|
|
9596
|
+
throw new Error("Access Denied: The sum of teacherWeight and peerWeight must equal 1.0");
|
|
9597
|
+
}
|
|
9598
|
+
const assignmentId = payload.assignmentId || "asg-" + (0, import_uuid8.v7)();
|
|
9599
|
+
const existing = loadAssignment(assignmentId);
|
|
9600
|
+
if (existing) {
|
|
9601
|
+
db2.prepare(
|
|
9602
|
+
`UPDATE plugin_assignments SET
|
|
9603
|
+
class_id = ?, lesson_id = ?, element_id = ?, title = ?, description = ?, instructions = ?,
|
|
9604
|
+
due_at = ?, allow_late = ?, allow_text = ?, allow_link = ?, max_files = ?, max_file_size = ?,
|
|
9605
|
+
peer_review_count = ?, teacher_weight = ?, peer_weight = ?, status = ?, updated_at = ?
|
|
9606
|
+
WHERE id = ?`
|
|
9607
|
+
).run(
|
|
9608
|
+
payload.classId ?? existing.class_id,
|
|
9609
|
+
payload.lessonId ?? existing.lesson_id,
|
|
9610
|
+
payload.elementId ?? existing.element_id,
|
|
9611
|
+
payload.title ?? existing.title,
|
|
9612
|
+
payload.description ?? null,
|
|
9613
|
+
payload.instructions ?? null,
|
|
9614
|
+
payload.dueAt ?? existing.due_at,
|
|
9615
|
+
payload.allowLate === void 0 ? existing.allow_late : payload.allowLate ? 1 : 0,
|
|
9616
|
+
payload.allowText === void 0 ? existing.allow_text : payload.allowText ? 1 : 0,
|
|
9617
|
+
payload.allowLink === void 0 ? existing.allow_link : payload.allowLink ? 1 : 0,
|
|
9618
|
+
payload.maxFiles ?? existing.max_files,
|
|
9619
|
+
payload.maxFileSize ?? existing.max_file_size,
|
|
9620
|
+
payload.peerReviewCount ?? existing.peer_review_count,
|
|
9621
|
+
teacherWeight,
|
|
9622
|
+
peerWeight,
|
|
9623
|
+
payload.status ?? existing.status,
|
|
9624
|
+
now,
|
|
9625
|
+
assignmentId
|
|
9626
|
+
);
|
|
9627
|
+
} else {
|
|
9628
|
+
db2.prepare(
|
|
9629
|
+
`INSERT INTO plugin_assignments
|
|
9630
|
+
(id, class_id, lesson_id, element_id, title, description, instructions, due_at, allow_late, allow_text,
|
|
9631
|
+
allow_link, max_files, max_file_size, peer_review_count, teacher_weight, peer_weight, status,
|
|
9632
|
+
created_by, created_at, updated_at)
|
|
9633
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
9634
|
+
).run(
|
|
9635
|
+
assignmentId,
|
|
9636
|
+
payload.classId ?? null,
|
|
9637
|
+
payload.lessonId ?? null,
|
|
9638
|
+
payload.elementId ?? null,
|
|
9639
|
+
payload.title,
|
|
9640
|
+
payload.description ?? null,
|
|
9641
|
+
payload.instructions ?? null,
|
|
9642
|
+
payload.dueAt ?? null,
|
|
9643
|
+
payload.allowLate === false ? 0 : 1,
|
|
9644
|
+
payload.allowText === false ? 0 : 1,
|
|
9645
|
+
payload.allowLink ? 1 : 0,
|
|
9646
|
+
payload.maxFiles ?? 10,
|
|
9647
|
+
payload.maxFileSize ?? 20971520,
|
|
9648
|
+
payload.peerReviewCount ?? 2,
|
|
9649
|
+
teacherWeight,
|
|
9650
|
+
peerWeight,
|
|
9651
|
+
payload.status ?? "published",
|
|
9652
|
+
actorUserId(command.actorId),
|
|
9653
|
+
now,
|
|
9654
|
+
now
|
|
9655
|
+
);
|
|
9656
|
+
}
|
|
9657
|
+
return { success: true, assignmentId, created: !existing };
|
|
9658
|
+
}
|
|
9659
|
+
});
|
|
9660
|
+
await actionRegistry.register({
|
|
9661
|
+
id: "eval-assignment-list",
|
|
9662
|
+
commandType: "assignment.list",
|
|
9663
|
+
description: "\u6309\u8BFE\u65F6 / \u73ED\u7EA7 / \u767D\u677F\u5143\u7D20 / \u5B66\u751F\u5217\u51FA\u4F5C\u4E1A",
|
|
9664
|
+
capabilityRequired: "assignment:read",
|
|
9665
|
+
inputSchema: {
|
|
9666
|
+
type: "OBJECT",
|
|
9667
|
+
properties: {
|
|
9668
|
+
lessonId: { type: "STRING" },
|
|
9669
|
+
classId: { type: "STRING" },
|
|
9670
|
+
elementId: { type: "STRING" },
|
|
9671
|
+
studentId: { type: "STRING", description: "\u9644\u5E26\u8BE5\u5B66\u751F\u7684\u63D0\u4EA4\u6982\u8981" },
|
|
9672
|
+
includeArchived: { type: "BOOLEAN" }
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
});
|
|
9676
|
+
await commandBus.registerHandler("assignment.list", {
|
|
9677
|
+
async execute(command) {
|
|
9678
|
+
const payload = command.payload;
|
|
9679
|
+
const where = [];
|
|
9680
|
+
const params = [];
|
|
9681
|
+
if (payload.lessonId) {
|
|
9682
|
+
where.push("lesson_id = ?");
|
|
9683
|
+
params.push(payload.lessonId);
|
|
9684
|
+
}
|
|
9685
|
+
if (payload.classId) {
|
|
9686
|
+
where.push("class_id = ?");
|
|
9687
|
+
params.push(payload.classId);
|
|
9688
|
+
}
|
|
9689
|
+
if (payload.elementId) {
|
|
9690
|
+
where.push("element_id = ?");
|
|
9691
|
+
params.push(payload.elementId);
|
|
9692
|
+
}
|
|
9693
|
+
if (!payload.includeArchived) {
|
|
9694
|
+
where.push("status != 'archived'");
|
|
9695
|
+
}
|
|
9696
|
+
const clause = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
9697
|
+
const rows = db2.prepare(`SELECT * FROM plugin_assignments ${clause} ORDER BY created_at DESC LIMIT 200`).all(...params);
|
|
9698
|
+
const studentId = payload.studentId;
|
|
9699
|
+
if (!studentId) return { success: true, assignments: rows };
|
|
9700
|
+
const assignments = rows.map((row) => {
|
|
9701
|
+
const submission = db2.prepare("SELECT id, version, file_path, updated_at FROM plugin_submissions WHERE assignment_id = ? AND student_id = ?").get(row.id, studentId);
|
|
9702
|
+
const grade = submission ? db2.prepare("SELECT calculated_final_score, status FROM plugin_grades WHERE submission_id = ?").get(submission.id) : void 0;
|
|
9703
|
+
return { ...row, submission: submission || null, grade: grade || null };
|
|
9704
|
+
});
|
|
9705
|
+
return { success: true, assignments };
|
|
9706
|
+
}
|
|
9707
|
+
});
|
|
9708
|
+
await actionRegistry.register({
|
|
9709
|
+
id: "eval-assignment-get",
|
|
9710
|
+
commandType: "assignment.get",
|
|
9711
|
+
description: "\u8BFB\u53D6\u5355\u4E2A\u4F5C\u4E1A\u8BE6\u60C5\uFF08\u542B\u63D0\u4EA4\u7248\u672C\u4E0E\u7EDF\u8BA1\uFF09",
|
|
9712
|
+
capabilityRequired: "assignment:read",
|
|
9713
|
+
inputSchema: {
|
|
9714
|
+
type: "OBJECT",
|
|
9715
|
+
properties: {
|
|
9716
|
+
assignmentId: { type: "STRING" },
|
|
9717
|
+
studentId: { type: "STRING", description: "\u9644\u5E26\u8BE5\u5B66\u751F\u7684\u63D0\u4EA4\u4E0E\u7248\u672C\u5386\u53F2" }
|
|
9718
|
+
},
|
|
9719
|
+
required: ["assignmentId"]
|
|
9720
|
+
}
|
|
9721
|
+
});
|
|
9722
|
+
await commandBus.registerHandler("assignment.get", {
|
|
9723
|
+
async execute(command) {
|
|
9724
|
+
const { assignmentId, studentId } = command.payload;
|
|
9725
|
+
const assignment = loadAssignment(assignmentId);
|
|
9726
|
+
if (!assignment) throw new Error(`Assignment not found: ${assignmentId}`);
|
|
9727
|
+
const submissions = db2.prepare("SELECT id, student_id, version, updated_at FROM plugin_submissions WHERE assignment_id = ?").all(assignmentId);
|
|
9728
|
+
const grades = db2.prepare("SELECT submission_id, calculated_final_score, status FROM plugin_grades WHERE assignment_id = ?").all(assignmentId);
|
|
9729
|
+
const gradedIds = new Set(grades.filter((g) => g.status === "confirmed").map((g) => g.submission_id));
|
|
9730
|
+
const result = {
|
|
9731
|
+
success: true,
|
|
9732
|
+
assignment,
|
|
9733
|
+
stats: {
|
|
9734
|
+
submissionCount: submissions.length,
|
|
9735
|
+
gradedCount: gradedIds.size,
|
|
9736
|
+
pendingPeerReviews: db2.prepare("SELECT COUNT(*) AS c FROM plugin_peer_review_tasks WHERE assignment_id = ? AND status = 'pending'").get(assignmentId).c
|
|
9737
|
+
}
|
|
9738
|
+
};
|
|
9739
|
+
if (studentId) {
|
|
9740
|
+
const submission = db2.prepare("SELECT * FROM plugin_submissions WHERE assignment_id = ? AND student_id = ?").get(assignmentId, studentId);
|
|
9741
|
+
if (submission) {
|
|
9742
|
+
result.submission = submission;
|
|
9743
|
+
result.versions = db2.prepare("SELECT * FROM plugin_submission_versions WHERE submission_id = ? ORDER BY version DESC").all(submission.id);
|
|
9744
|
+
result.grade = db2.prepare("SELECT * FROM plugin_grades WHERE submission_id = ?").get(submission.id) || null;
|
|
9745
|
+
} else {
|
|
9746
|
+
result.submission = null;
|
|
9747
|
+
result.versions = [];
|
|
9748
|
+
result.grade = null;
|
|
9749
|
+
}
|
|
9750
|
+
result.myPeerReviewTasks = db2.prepare("SELECT * FROM plugin_peer_review_tasks WHERE assignment_id = ? AND reviewer_id = ?").all(assignmentId, studentId);
|
|
9751
|
+
result.peerReviewTasks = buildPeerReviewTasks(assignmentId, studentId);
|
|
9752
|
+
}
|
|
9753
|
+
if (command.payload.includePeerProgress) {
|
|
9754
|
+
result.peerProgress = buildPeerProgress(assignmentId);
|
|
9755
|
+
}
|
|
9756
|
+
return result;
|
|
9757
|
+
}
|
|
9758
|
+
});
|
|
8323
9759
|
await actionRegistry.register({
|
|
8324
9760
|
id: "eval-assignment-submit",
|
|
8325
|
-
commandType:
|
|
8326
|
-
description: "\u5B66\u751F\u63D0\u4EA4\u4F5C\u4E1A\u4F5C\u54C1\uFF0C\u652F\u6301\u591A\u6B21\
|
|
8327
|
-
capabilityRequired: "
|
|
9761
|
+
commandType: "assignment.submit",
|
|
9762
|
+
description: "\u5B66\u751F\u63D0\u4EA4\u4F5C\u4E1A\u4F5C\u54C1\uFF0C\u652F\u6301\u591A\u6B21\u63D0\u4EA4\u5E76\u4FDD\u7559\u7248\u672C\u5386\u53F2",
|
|
9763
|
+
capabilityRequired: "assignment:submit",
|
|
8328
9764
|
inputSchema: {
|
|
8329
9765
|
type: "OBJECT",
|
|
8330
9766
|
properties: {
|
|
8331
|
-
|
|
9767
|
+
assignmentId: { type: "STRING", description: "\u4F5C\u4E1A\u5B9E\u4F53 ID\uFF08\u63A8\u8350\uFF09" },
|
|
9768
|
+
lessonId: { type: "STRING", description: "\u65E7\u7248\u8BFE\u65F6\u4F5C\u4E1A\u5165\u53E3\uFF1A\u4EC5\u4F20\u8BFE\u65F6 ID \u65F6\u81EA\u52A8\u5F52\u5C5E\u5230\u8BE5\u8BFE\u65F6\u7684\u4F5C\u4E1A" },
|
|
8332
9769
|
studentId: { type: "STRING", description: "\u63D0\u4EA4\u4F5C\u54C1\u7684\u5B66\u751F ID" },
|
|
8333
|
-
filePath: { type: "STRING", description: "\
|
|
9770
|
+
filePath: { type: "STRING", description: "\u65E7\u7248\u517C\u5BB9\uFF1A\u6587\u672C\u5F62\u5F0F\u7684\u6587\u4EF6\u8DEF\u5F84\u63CF\u8FF0" },
|
|
9771
|
+
fileIds: { type: "ARRAY", items: { type: "STRING" }, description: "\u5DF2\u4E0A\u4F20\u6587\u4EF6\u7684\u5143\u6570\u636E ID \u5217\u8868" },
|
|
9772
|
+
textContent: { type: "STRING", description: "\u6587\u672C\u4F5C\u7B54\u5185\u5BB9" },
|
|
9773
|
+
linkUrl: { type: "STRING", description: "\u94FE\u63A5\u4F5C\u7B54\u5185\u5BB9" }
|
|
8334
9774
|
},
|
|
8335
|
-
required: ["
|
|
9775
|
+
required: ["studentId"]
|
|
8336
9776
|
}
|
|
8337
9777
|
});
|
|
8338
|
-
await commandBus.registerHandler(
|
|
9778
|
+
await commandBus.registerHandler("assignment.submit", {
|
|
8339
9779
|
async execute(command) {
|
|
8340
|
-
const
|
|
8341
|
-
|
|
8342
|
-
|
|
9780
|
+
const payload = command.payload;
|
|
9781
|
+
const { studentId, textContent, linkUrl, filePath } = payload;
|
|
9782
|
+
if (!studentId) {
|
|
9783
|
+
throw new Error("Missing required params: studentId");
|
|
9784
|
+
}
|
|
9785
|
+
assertStudentOwnership(command.actorId, studentId, "submit");
|
|
9786
|
+
const assignment = resolveAssignment(payload.assignmentId, payload.lessonId);
|
|
9787
|
+
const now = Date.now();
|
|
9788
|
+
const isLate = assignment.due_at !== null && now > Number(assignment.due_at) ? 1 : 0;
|
|
9789
|
+
if (isLate && !assignment.allow_late) {
|
|
9790
|
+
throw new Error("Access Denied: Assignment is closed (late submission is not allowed)");
|
|
9791
|
+
}
|
|
9792
|
+
if (textContent && !assignment.allow_text) {
|
|
9793
|
+
throw new Error("Access Denied: Text answers are not allowed for this assignment");
|
|
8343
9794
|
}
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
9795
|
+
if (linkUrl && !assignment.allow_link) {
|
|
9796
|
+
throw new Error("Access Denied: Link answers are not allowed for this assignment");
|
|
9797
|
+
}
|
|
9798
|
+
const fileIds = Array.isArray(payload.fileIds) ? payload.fileIds.filter((f) => typeof f === "string") : [];
|
|
9799
|
+
if (fileIds.length > Number(assignment.max_files)) {
|
|
9800
|
+
throw new Error(`Access Denied: At most ${assignment.max_files} files are allowed`);
|
|
9801
|
+
}
|
|
9802
|
+
const existing = db2.prepare("SELECT id, version FROM plugin_submissions WHERE assignment_id = ? AND student_id = ?").get(assignment.id, studentId);
|
|
9803
|
+
const submissionId = existing?.id || "sub-" + (0, import_uuid8.v7)();
|
|
9804
|
+
const version = existing ? existing.version + 1 : 1;
|
|
9805
|
+
const versionId = "sv-" + (0, import_uuid8.v7)();
|
|
8347
9806
|
if (existing) {
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
filePath,
|
|
8352
|
-
version,
|
|
8353
|
-
Date.now(),
|
|
8354
|
-
submissionId
|
|
8355
|
-
);
|
|
9807
|
+
db2.prepare(
|
|
9808
|
+
"UPDATE plugin_submissions SET lesson_id = ?, file_path = ?, version = ?, updated_at = ? WHERE id = ?"
|
|
9809
|
+
).run(assignment.lesson_id ?? null, filePath ?? null, version, now, submissionId);
|
|
8356
9810
|
} else {
|
|
8357
|
-
submissionId = "sub-" + (0, import_uuid8.v7)();
|
|
8358
9811
|
db2.prepare(
|
|
8359
|
-
|
|
8360
|
-
|
|
9812
|
+
`INSERT INTO plugin_submissions (id, assignment_id, lesson_id, student_id, file_path, version, created_at, updated_at)
|
|
9813
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
9814
|
+
).run(submissionId, assignment.id, assignment.lesson_id ?? null, studentId, filePath ?? null, version, now, now);
|
|
8361
9815
|
}
|
|
8362
|
-
|
|
9816
|
+
if (fileIds.length > 0) {
|
|
9817
|
+
const placeholders = fileIds.map(() => "?").join(", ");
|
|
9818
|
+
db2.prepare(
|
|
9819
|
+
`UPDATE plugin_assignment_files SET submission_id = ?, version_id = ?
|
|
9820
|
+
WHERE assignment_id = ? AND student_id = ? AND id IN (${placeholders})`
|
|
9821
|
+
).run(submissionId, versionId, assignment.id, studentId, ...fileIds);
|
|
9822
|
+
}
|
|
9823
|
+
const attached = db2.prepare("SELECT id, original_name, size, mime FROM plugin_assignment_files WHERE version_id = ?").all(versionId);
|
|
9824
|
+
const filesJson = JSON.stringify(
|
|
9825
|
+
attached.length > 0 ? attached.map((f) => ({ fileId: f.id, name: f.original_name, size: f.size, mime: f.mime })) : filePath ? [{ name: String(filePath), path: String(filePath) }] : []
|
|
9826
|
+
);
|
|
9827
|
+
db2.prepare(
|
|
9828
|
+
`INSERT INTO plugin_submission_versions
|
|
9829
|
+
(id, submission_id, assignment_id, student_id, version, files_json, text_content, link_url, is_late, submitted_at)
|
|
9830
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
9831
|
+
).run(versionId, submissionId, assignment.id, studentId, version, filesJson, textContent ?? null, linkUrl ?? null, isLate, now);
|
|
9832
|
+
await publishEvent2(
|
|
9833
|
+
"assignment.submitted",
|
|
9834
|
+
{
|
|
9835
|
+
assignmentId: assignment.id,
|
|
9836
|
+
lessonId: assignment.lesson_id,
|
|
9837
|
+
classId: assignment.class_id,
|
|
9838
|
+
studentId,
|
|
9839
|
+
submissionId,
|
|
9840
|
+
version,
|
|
9841
|
+
isLate: isLate === 1
|
|
9842
|
+
},
|
|
9843
|
+
command.id
|
|
9844
|
+
);
|
|
9845
|
+
return { success: true, submissionId, version, versionId, assignmentId: assignment.id, isLate: isLate === 1 };
|
|
8363
9846
|
}
|
|
8364
9847
|
});
|
|
8365
|
-
const peerReviewCmd = "assignment.peer_review";
|
|
8366
9848
|
await actionRegistry.register({
|
|
8367
9849
|
id: "eval-assignment-peer-review",
|
|
8368
|
-
commandType:
|
|
8369
|
-
description: "\u5B66\u751F\u5BF9\
|
|
8370
|
-
capabilityRequired: "
|
|
9850
|
+
commandType: "assignment.peer_review",
|
|
9851
|
+
description: "\u5B66\u751F\u5BF9\u5206\u914D\u5230\u7684\u540C\u5B66\u4F5C\u4E1A\u8FDB\u884C\u4E92\u8BC4\u8BC4\u5206",
|
|
9852
|
+
capabilityRequired: "assignment:review",
|
|
8371
9853
|
inputSchema: {
|
|
8372
9854
|
type: "OBJECT",
|
|
8373
9855
|
properties: {
|
|
8374
9856
|
submissionId: { type: "STRING", description: "\u88AB\u8BC4\u4EF7\u7684\u4F5C\u4E1A\u63D0\u4EA4\u7269 ID" },
|
|
8375
9857
|
reviewerId: { type: "STRING", description: "\u6267\u884C\u8BC4\u4EF7\u7684\u5B66\u751F ID" },
|
|
8376
9858
|
score: { type: "INTEGER", description: "\u4E92\u8BC4\u5206\u6570 (0-100)" },
|
|
8377
|
-
comment: { type: "STRING", description: "\u4E92\u8BC4\u610F\u89C1" }
|
|
9859
|
+
comment: { type: "STRING", description: "\u4E92\u8BC4\u610F\u89C1" },
|
|
9860
|
+
taskId: { type: "STRING", description: "\u4E92\u8BC4\u4EFB\u52A1 ID\uFF08\u5206\u914D\u5F0F\u4E92\u8BC4\u65F6\u63D0\u4F9B\uFF09" }
|
|
8378
9861
|
},
|
|
8379
9862
|
required: ["submissionId", "reviewerId", "score"]
|
|
8380
9863
|
}
|
|
8381
9864
|
});
|
|
8382
|
-
await commandBus.registerHandler(
|
|
9865
|
+
await commandBus.registerHandler("assignment.peer_review", {
|
|
8383
9866
|
async execute(command) {
|
|
8384
|
-
const { submissionId, reviewerId, score, comment } = command.payload;
|
|
9867
|
+
const { submissionId, reviewerId, score, comment, taskId } = command.payload;
|
|
8385
9868
|
if (!submissionId || !reviewerId || score === void 0) {
|
|
8386
9869
|
throw new Error("Missing required params: submissionId, reviewerId, score");
|
|
8387
9870
|
}
|
|
9871
|
+
assertStudentOwnership(command.actorId, reviewerId, "review");
|
|
8388
9872
|
const parsedScore = Math.round(Number(score));
|
|
8389
9873
|
if (isNaN(parsedScore) || parsedScore < 0 || parsedScore > 100) {
|
|
8390
9874
|
throw new Error("Access Denied: Score must be between 0 and 100");
|
|
8391
9875
|
}
|
|
8392
|
-
const submission = db2.prepare("SELECT student_id FROM plugin_submissions WHERE id = ?").get(submissionId);
|
|
9876
|
+
const submission = db2.prepare("SELECT id, assignment_id, student_id FROM plugin_submissions WHERE id = ?").get(submissionId);
|
|
8393
9877
|
if (!submission) {
|
|
8394
9878
|
throw new Error(`Submission not found: ${submissionId}`);
|
|
8395
9879
|
}
|
|
8396
9880
|
if (submission.student_id === reviewerId) {
|
|
8397
9881
|
throw new Error("Access Denied: Students are not allowed to evaluate their own assignments");
|
|
8398
9882
|
}
|
|
9883
|
+
const task = db2.prepare("SELECT id, due_at, status FROM plugin_peer_review_tasks WHERE submission_id = ? AND reviewer_id = ?").get(submissionId, reviewerId);
|
|
9884
|
+
if (!task && submission.assignment_id) {
|
|
9885
|
+
const taskCount = db2.prepare("SELECT COUNT(*) AS c FROM plugin_peer_review_tasks WHERE assignment_id = ?").get(submission.assignment_id).c;
|
|
9886
|
+
if (taskCount > 0) {
|
|
9887
|
+
throw new Error("Access Denied: This submission is not assigned to you for peer review");
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
const assignmentRow = submission.assignment_id ? loadAssignment(submission.assignment_id) : void 0;
|
|
9891
|
+
const resolvedTaskId = taskId || task?.id || null;
|
|
9892
|
+
const dueAt = task?.due_at ?? assignmentRow?.peer_review_due_at ?? null;
|
|
9893
|
+
if (dueAt !== null && dueAt !== void 0 && Date.now() > Number(dueAt)) {
|
|
9894
|
+
throw new Error("Access Denied: The peer review deadline has passed");
|
|
9895
|
+
}
|
|
9896
|
+
const now = Date.now();
|
|
8399
9897
|
const reviewId = "rev-" + (0, import_uuid8.v7)();
|
|
8400
9898
|
db2.prepare(
|
|
8401
9899
|
`
|
|
8402
|
-
INSERT INTO plugin_peer_reviews
|
|
8403
|
-
|
|
9900
|
+
INSERT INTO plugin_peer_reviews
|
|
9901
|
+
(id, submission_id, reviewer_id, score, comment, created_at, assignment_id, task_id, anonymous, status, updated_at)
|
|
9902
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 'submitted', ?)
|
|
8404
9903
|
ON CONFLICT(submission_id, reviewer_id) DO UPDATE SET
|
|
8405
9904
|
score = excluded.score,
|
|
8406
9905
|
comment = excluded.comment,
|
|
8407
|
-
created_at = excluded.created_at
|
|
9906
|
+
created_at = excluded.created_at,
|
|
9907
|
+
assignment_id = excluded.assignment_id,
|
|
9908
|
+
task_id = excluded.task_id,
|
|
9909
|
+
status = 'submitted',
|
|
9910
|
+
updated_at = excluded.updated_at
|
|
8408
9911
|
`
|
|
8409
|
-
).run(
|
|
9912
|
+
).run(
|
|
9913
|
+
reviewId,
|
|
9914
|
+
submissionId,
|
|
9915
|
+
reviewerId,
|
|
9916
|
+
parsedScore,
|
|
9917
|
+
comment || "",
|
|
9918
|
+
now,
|
|
9919
|
+
submission.assignment_id,
|
|
9920
|
+
resolvedTaskId,
|
|
9921
|
+
now
|
|
9922
|
+
);
|
|
9923
|
+
if (task) {
|
|
9924
|
+
db2.prepare("UPDATE plugin_peer_review_tasks SET status = 'submitted' WHERE id = ?").run(task.id);
|
|
9925
|
+
}
|
|
8410
9926
|
return { success: true, reviewId };
|
|
8411
9927
|
}
|
|
8412
9928
|
});
|
|
8413
|
-
|
|
9929
|
+
await actionRegistry.register({
|
|
9930
|
+
id: "eval-assignment-assign-peer-reviews",
|
|
9931
|
+
commandType: "assignment.assign_peer_reviews",
|
|
9932
|
+
description: "\u4E3A\u4F5C\u4E1A\u7684\u6BCF\u4EFD\u63D0\u4EA4\u968F\u673A\u5206\u914D\u4E92\u8BC4\u4EBA\uFF08\u53CC\u76F2\uFF0C\u6392\u9664\u672C\u4EBA\uFF09",
|
|
9933
|
+
capabilityRequired: "assignment:manage",
|
|
9934
|
+
inputSchema: {
|
|
9935
|
+
type: "OBJECT",
|
|
9936
|
+
properties: {
|
|
9937
|
+
assignmentId: { type: "STRING", description: "\u4F5C\u4E1A\u5B9E\u4F53 ID" },
|
|
9938
|
+
reviewerCount: { type: "NUMBER", description: "\u6BCF\u4EFD\u63D0\u4EA4\u5206\u914D\u51E0\u540D\u4E92\u8BC4\u4EBA\uFF08\u9ED8\u8BA4\u53D6\u4F5C\u4E1A\u914D\u7F6E\uFF09" },
|
|
9939
|
+
dueAt: { type: "NUMBER", description: "\u4E92\u8BC4\u622A\u6B62\u65F6\u95F4\uFF08\u6BEB\u79D2\u65F6\u95F4\u6233\uFF09" }
|
|
9940
|
+
},
|
|
9941
|
+
required: ["assignmentId"]
|
|
9942
|
+
}
|
|
9943
|
+
});
|
|
9944
|
+
await commandBus.registerHandler("assignment.assign_peer_reviews", {
|
|
9945
|
+
async execute(command) {
|
|
9946
|
+
const { assignmentId, dueAt } = command.payload;
|
|
9947
|
+
const assignment = loadAssignment(assignmentId);
|
|
9948
|
+
if (!assignment) throw new Error(`Assignment not found: ${assignmentId}`);
|
|
9949
|
+
const reviewerCount = Math.max(1, Math.min(10, Number(command.payload.reviewerCount ?? assignment.peer_review_count) || 1));
|
|
9950
|
+
const submissions = db2.prepare("SELECT id, student_id FROM plugin_submissions WHERE assignment_id = ? ORDER BY created_at ASC").all(assignmentId);
|
|
9951
|
+
const pools = /* @__PURE__ */ new Map();
|
|
9952
|
+
let created = 0;
|
|
9953
|
+
const now = Date.now();
|
|
9954
|
+
for (const submission of submissions) {
|
|
9955
|
+
const candidates = submissions.filter((s) => s.student_id !== submission.student_id);
|
|
9956
|
+
if (candidates.length === 0) continue;
|
|
9957
|
+
candidates.sort((a, b) => (pools.get(a.student_id) || 0) - (pools.get(b.student_id) || 0));
|
|
9958
|
+
const picked = candidates.slice(0, Math.min(reviewerCount, candidates.length));
|
|
9959
|
+
for (const candidate of picked) {
|
|
9960
|
+
const taskId = "prt-" + (0, import_uuid8.v7)();
|
|
9961
|
+
const result = db2.prepare(
|
|
9962
|
+
`INSERT INTO plugin_peer_review_tasks
|
|
9963
|
+
(id, assignment_id, submission_id, reviewer_id, anonymous, status, due_at, created_at)
|
|
9964
|
+
VALUES (?, ?, ?, ?, 1, 'pending', ?, ?)
|
|
9965
|
+
ON CONFLICT(submission_id, reviewer_id) DO NOTHING`
|
|
9966
|
+
).run(taskId, assignmentId, submission.id, candidate.student_id, dueAt ?? assignment.peer_review_due_at ?? null, now);
|
|
9967
|
+
if (result.changes > 0) {
|
|
9968
|
+
created += 1;
|
|
9969
|
+
pools.set(candidate.student_id, (pools.get(candidate.student_id) || 0) + 1);
|
|
9970
|
+
}
|
|
9971
|
+
}
|
|
9972
|
+
}
|
|
9973
|
+
if (dueAt !== void 0 && dueAt !== null) {
|
|
9974
|
+
db2.prepare("UPDATE plugin_assignments SET peer_review_due_at = ?, updated_at = ? WHERE id = ?").run(
|
|
9975
|
+
Number(dueAt),
|
|
9976
|
+
now,
|
|
9977
|
+
assignmentId
|
|
9978
|
+
);
|
|
9979
|
+
}
|
|
9980
|
+
return { success: true, created, submissions: submissions.length };
|
|
9981
|
+
}
|
|
9982
|
+
});
|
|
8414
9983
|
await actionRegistry.register({
|
|
8415
9984
|
id: "eval-assignment-grade",
|
|
8416
|
-
commandType:
|
|
9985
|
+
commandType: "assignment.grade",
|
|
8417
9986
|
description: "\u6559\u5E08\u5BF9\u5DF2\u63D0\u4EA4\u7684\u4F5C\u4E1A\u8FDB\u884C\u7EC8\u8BC4\u6253\u5206\uFF0C\u5E76\u53EF\u52A8\u6001\u8BBE\u5B9A\u6298\u7B97\u6743\u91CD\u4E0E\u53D1\u5E03\u6210\u7EE9",
|
|
8418
|
-
capabilityRequired: "
|
|
9987
|
+
capabilityRequired: "assignment:manage",
|
|
8419
9988
|
inputSchema: {
|
|
8420
9989
|
type: "OBJECT",
|
|
8421
9990
|
properties: {
|
|
8422
9991
|
submissionId: { type: "STRING", description: "\u88AB\u8BC4\u4EF7\u7684\u4F5C\u4E1A\u63D0\u4EA4\u7269 ID" },
|
|
8423
9992
|
teacherScore: { type: "INTEGER", description: "\u6559\u5E08\u7ED9\u51FA\u7684\u5E73\u65F6\u5206 (0-100)" },
|
|
8424
9993
|
teacherComment: { type: "STRING", description: "\u6559\u5E08\u8BC4\u8BED\u53CD\u9988" },
|
|
8425
|
-
teacherWeight: { type: "NUMBER", description: "\u6559\u5E08\u6253\u5206\u6743\u91CD (\u9ED8\u8BA4
|
|
8426
|
-
peerWeight: { type: "NUMBER", description: "\u5B66\u751F\u4E92\u8BC4\u5E73\u5747\u5206\u6743\u91CD (\u9ED8\u8BA4
|
|
9994
|
+
teacherWeight: { type: "NUMBER", description: "\u6559\u5E08\u6253\u5206\u6743\u91CD (\u9ED8\u8BA4\u53D6\u4F5C\u4E1A\u914D\u7F6E)" },
|
|
9995
|
+
peerWeight: { type: "NUMBER", description: "\u5B66\u751F\u4E92\u8BC4\u5E73\u5747\u5206\u6743\u91CD (\u9ED8\u8BA4\u53D6\u4F5C\u4E1A\u914D\u7F6E)" },
|
|
8427
9996
|
status: { type: "STRING", description: "\u72B6\u6001\uFF0Cconfirmed \u4E3A\u786E\u8BA4\u5E76\u540C\u6B65\uFF0Cdraft \u4E3A\u8349\u7A3F" }
|
|
8428
9997
|
},
|
|
8429
9998
|
required: ["submissionId", "teacherScore"]
|
|
8430
9999
|
}
|
|
8431
10000
|
});
|
|
8432
|
-
await commandBus.registerHandler(
|
|
10001
|
+
await commandBus.registerHandler("assignment.grade", {
|
|
8433
10002
|
async execute(command) {
|
|
8434
|
-
const
|
|
8435
|
-
|
|
8436
|
-
teacherScore,
|
|
8437
|
-
teacherComment,
|
|
8438
|
-
teacherWeight = 0.6,
|
|
8439
|
-
peerWeight = 0.4,
|
|
8440
|
-
status = "draft"
|
|
8441
|
-
} = command.payload;
|
|
10003
|
+
const payload = command.payload;
|
|
10004
|
+
const { submissionId, teacherScore, teacherComment, status = "draft" } = payload;
|
|
8442
10005
|
if (!submissionId || teacherScore === void 0) {
|
|
8443
10006
|
throw new Error("Missing required params: submissionId, teacherScore");
|
|
8444
10007
|
}
|
|
@@ -8446,29 +10009,34 @@ var AssignmentEvalPlugin = {
|
|
|
8446
10009
|
if (isNaN(parsedTeacherScore) || parsedTeacherScore < 0 || parsedTeacherScore > 100) {
|
|
8447
10010
|
throw new Error("Access Denied: Teacher score must be between 0 and 100");
|
|
8448
10011
|
}
|
|
8449
|
-
const
|
|
8450
|
-
if (Math.abs(totalWeight - 1) > 1e-3) {
|
|
8451
|
-
throw new Error("Access Denied: The sum of teacherWeight and peerWeight must equal 1.0");
|
|
8452
|
-
}
|
|
8453
|
-
const submission = db2.prepare("SELECT lesson_id, student_id FROM plugin_submissions WHERE id = ?").get(submissionId);
|
|
10012
|
+
const submission = db2.prepare("SELECT id, assignment_id, lesson_id, student_id FROM plugin_submissions WHERE id = ?").get(submissionId);
|
|
8454
10013
|
if (!submission) {
|
|
8455
10014
|
throw new Error(`Submission not found: ${submissionId}`);
|
|
8456
10015
|
}
|
|
10016
|
+
const assignment = submission.assignment_id ? loadAssignment(submission.assignment_id) : void 0;
|
|
10017
|
+
const teacherWeight = payload.teacherWeight === void 0 ? Number(assignment?.teacher_weight ?? 0.6) : Number(payload.teacherWeight);
|
|
10018
|
+
const peerWeight = payload.peerWeight === void 0 ? Number(assignment?.peer_weight ?? 0.4) : Number(payload.peerWeight);
|
|
10019
|
+
const totalWeight = teacherWeight + peerWeight;
|
|
10020
|
+
if (Math.abs(totalWeight - 1) > 1e-3) {
|
|
10021
|
+
throw new Error("Access Denied: The sum of teacherWeight and peerWeight must equal 1.0");
|
|
10022
|
+
}
|
|
8457
10023
|
const reviews = db2.prepare("SELECT score FROM plugin_peer_reviews WHERE submission_id = ?").all(submissionId);
|
|
8458
|
-
let peerAverageScore =
|
|
10024
|
+
let peerAverageScore = null;
|
|
8459
10025
|
let calculatedFinalScore = parsedTeacherScore;
|
|
8460
10026
|
if (reviews.length > 0) {
|
|
8461
10027
|
const sum = reviews.reduce((acc, r) => acc + r.score, 0);
|
|
8462
10028
|
peerAverageScore = sum / reviews.length;
|
|
8463
10029
|
calculatedFinalScore = Math.round(parsedTeacherScore * teacherWeight + peerAverageScore * peerWeight);
|
|
8464
10030
|
}
|
|
10031
|
+
const now = Date.now();
|
|
8465
10032
|
const gradeId = "grd-" + (0, import_uuid8.v7)();
|
|
8466
10033
|
db2.prepare(
|
|
8467
10034
|
`
|
|
8468
10035
|
INSERT INTO plugin_grades (
|
|
8469
|
-
id, submission_id, teacher_score, teacher_comment, teacher_weight, peer_weight,
|
|
10036
|
+
id, submission_id, teacher_score, teacher_comment, teacher_weight, peer_weight,
|
|
10037
|
+
calculated_final_score, status, graded_at, assignment_id, peer_average_score, source, published_at, graded_by
|
|
8470
10038
|
)
|
|
8471
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10039
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'teacher', ?, ?)
|
|
8472
10040
|
ON CONFLICT(submission_id) DO UPDATE SET
|
|
8473
10041
|
teacher_score = excluded.teacher_score,
|
|
8474
10042
|
teacher_comment = excluded.teacher_comment,
|
|
@@ -8476,7 +10044,11 @@ var AssignmentEvalPlugin = {
|
|
|
8476
10044
|
peer_weight = excluded.peer_weight,
|
|
8477
10045
|
calculated_final_score = excluded.calculated_final_score,
|
|
8478
10046
|
status = excluded.status,
|
|
8479
|
-
graded_at = excluded.graded_at
|
|
10047
|
+
graded_at = excluded.graded_at,
|
|
10048
|
+
assignment_id = excluded.assignment_id,
|
|
10049
|
+
peer_average_score = excluded.peer_average_score,
|
|
10050
|
+
published_at = excluded.published_at,
|
|
10051
|
+
graded_by = excluded.graded_by
|
|
8480
10052
|
`
|
|
8481
10053
|
).run(
|
|
8482
10054
|
gradeId,
|
|
@@ -8487,12 +10059,74 @@ var AssignmentEvalPlugin = {
|
|
|
8487
10059
|
peerWeight,
|
|
8488
10060
|
calculatedFinalScore,
|
|
8489
10061
|
status,
|
|
8490
|
-
|
|
10062
|
+
now,
|
|
10063
|
+
submission.assignment_id,
|
|
10064
|
+
peerAverageScore,
|
|
10065
|
+
status === "confirmed" ? now : null,
|
|
10066
|
+
actorUserId(command.actorId)
|
|
8491
10067
|
);
|
|
8492
10068
|
if (status === "confirmed") {
|
|
8493
|
-
|
|
10069
|
+
let projectedByHost = false;
|
|
10070
|
+
if (submission.lesson_id) {
|
|
10071
|
+
try {
|
|
10072
|
+
await gradeService.saveSemesterGrade(submission.lesson_id, submission.student_id, calculatedFinalScore);
|
|
10073
|
+
projectedByHost = true;
|
|
10074
|
+
} catch (err) {
|
|
10075
|
+
ctx.log?.warn?.(`[assignment-eval] \u540C\u6B65\u5B66\u671F\u6210\u7EE9\u5931\u8D25: ${err?.message}`);
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
10078
|
+
if (!projectedByHost && assignment && assignment.class_id) {
|
|
10079
|
+
db2.prepare(
|
|
10080
|
+
`INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at)
|
|
10081
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
10082
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
10083
|
+
class_id = excluded.class_id,
|
|
10084
|
+
lesson_id = excluded.lesson_id,
|
|
10085
|
+
title = excluded.title,
|
|
10086
|
+
description = excluded.description`
|
|
10087
|
+
).run(assignment.id, assignment.class_id, assignment.lesson_id, assignment.title, "", "", now);
|
|
10088
|
+
db2.prepare(
|
|
10089
|
+
`INSERT INTO assignment_submissions
|
|
10090
|
+
(assignment_id, student_id, content, score, feedback, submitted_at, graded_at, status)
|
|
10091
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'graded')
|
|
10092
|
+
ON CONFLICT(assignment_id, student_id) DO UPDATE SET
|
|
10093
|
+
score = excluded.score,
|
|
10094
|
+
feedback = excluded.feedback,
|
|
10095
|
+
graded_at = excluded.graded_at,
|
|
10096
|
+
status = 'graded'`
|
|
10097
|
+
).run(
|
|
10098
|
+
assignment.id,
|
|
10099
|
+
submission.student_id,
|
|
10100
|
+
"",
|
|
10101
|
+
calculatedFinalScore,
|
|
10102
|
+
teacherComment || "",
|
|
10103
|
+
now,
|
|
10104
|
+
now
|
|
10105
|
+
);
|
|
10106
|
+
}
|
|
10107
|
+
await publishEvent2(
|
|
10108
|
+
"assignment.graded",
|
|
10109
|
+
{
|
|
10110
|
+
assignmentId: submission.assignment_id,
|
|
10111
|
+
lessonId: submission.lesson_id,
|
|
10112
|
+
studentId: submission.student_id,
|
|
10113
|
+
submissionId,
|
|
10114
|
+
score: calculatedFinalScore,
|
|
10115
|
+
teacherScore: parsedTeacherScore,
|
|
10116
|
+
peerAverageScore,
|
|
10117
|
+
feedback: teacherComment || ""
|
|
10118
|
+
},
|
|
10119
|
+
command.id
|
|
10120
|
+
);
|
|
8494
10121
|
}
|
|
8495
|
-
return {
|
|
10122
|
+
return {
|
|
10123
|
+
success: true,
|
|
10124
|
+
calculatedFinalScore,
|
|
10125
|
+
peerAverageScore,
|
|
10126
|
+
teacherWeight,
|
|
10127
|
+
peerWeight,
|
|
10128
|
+
status
|
|
10129
|
+
};
|
|
8496
10130
|
}
|
|
8497
10131
|
});
|
|
8498
10132
|
},
|
|
@@ -8775,6 +10409,9 @@ var AuthSessionBridgeService = class {
|
|
|
8775
10409
|
}
|
|
8776
10410
|
};
|
|
8777
10411
|
|
|
10412
|
+
// packages/core/kernel/index.ts
|
|
10413
|
+
init_courseware_runtime_script_registry();
|
|
10414
|
+
|
|
8778
10415
|
// packages/core/plugin-host/index.ts
|
|
8779
10416
|
var import_uuid13 = require("uuid");
|
|
8780
10417
|
var import_fs5 = __toESM(require("fs"), 1);
|
|
@@ -8867,7 +10504,19 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
8867
10504
|
const rawManifest = JSON.parse(manifestJson);
|
|
8868
10505
|
rawManifest.main ||= "index.js";
|
|
8869
10506
|
const manifest = manifestSchema.parse(rawManifest);
|
|
8870
|
-
|
|
10507
|
+
let entryFile = zip.file(manifest.main);
|
|
10508
|
+
let resolvedMain = manifest.main;
|
|
10509
|
+
if (!entryFile && manifest.main.startsWith("dist/")) {
|
|
10510
|
+
const fallback = manifest.main.slice("dist/".length);
|
|
10511
|
+
const fallbackEntry = zip.file(fallback);
|
|
10512
|
+
if (fallbackEntry) {
|
|
10513
|
+
console.warn(
|
|
10514
|
+
`[install-utils] manifest.main "${manifest.main}" not found in ZIP; falling back to "${fallback}". Plugin author should update build script to drop the dist/ prefix.`
|
|
10515
|
+
);
|
|
10516
|
+
entryFile = fallbackEntry;
|
|
10517
|
+
resolvedMain = fallback;
|
|
10518
|
+
}
|
|
10519
|
+
}
|
|
8871
10520
|
if (!entryFile) {
|
|
8872
10521
|
throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
|
|
8873
10522
|
}
|
|
@@ -8884,7 +10533,7 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
8884
10533
|
fs4.writeFileSync(filePath, content);
|
|
8885
10534
|
}
|
|
8886
10535
|
const bundledCode = await bundlePlugin(entryCode, tmpDir);
|
|
8887
|
-
return { manifest, bundledCode, entryFileName:
|
|
10536
|
+
return { manifest, bundledCode, entryFileName: resolvedMain };
|
|
8888
10537
|
} finally {
|
|
8889
10538
|
try {
|
|
8890
10539
|
fs4.rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -9354,7 +11003,7 @@ var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
|
9354
11003
|
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
9355
11004
|
var import_node_url = require("node:url");
|
|
9356
11005
|
var import_meta2 = {};
|
|
9357
|
-
var FALLBACK_VERSION = "0.3.
|
|
11006
|
+
var FALLBACK_VERSION = "0.3.21";
|
|
9358
11007
|
function readPlatformVersion() {
|
|
9359
11008
|
const bases = [];
|
|
9360
11009
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
@@ -9682,11 +11331,11 @@ var PluginDistributionManager = class {
|
|
|
9682
11331
|
return;
|
|
9683
11332
|
}
|
|
9684
11333
|
const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
|
|
9685
|
-
const
|
|
9686
|
-
if (!
|
|
11334
|
+
const fs18 = await import("node:fs");
|
|
11335
|
+
if (!fs18.existsSync(filePath)) {
|
|
9687
11336
|
throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
|
|
9688
11337
|
}
|
|
9689
|
-
const code =
|
|
11338
|
+
const code = fs18.readFileSync(filePath, "utf-8");
|
|
9690
11339
|
await this.pluginHost.reloadPlugin(pluginId, code);
|
|
9691
11340
|
}
|
|
9692
11341
|
async updateFromZip(zipBuffer, options = {}) {
|
|
@@ -9910,7 +11559,7 @@ var PluginHost = class {
|
|
|
9910
11559
|
/**
|
|
9911
11560
|
* 判断指定插件的某个请求路径是否为流式路由 (SSE)
|
|
9912
11561
|
*/
|
|
9913
|
-
isStreamRoute(pluginIdOrManifestId, method,
|
|
11562
|
+
isStreamRoute(pluginIdOrManifestId, method, path24) {
|
|
9914
11563
|
const upperMethod = method.toUpperCase();
|
|
9915
11564
|
const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
|
|
9916
11565
|
const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
|
|
@@ -9919,7 +11568,7 @@ var PluginHost = class {
|
|
|
9919
11568
|
for (const r of manifest.api.routes) {
|
|
9920
11569
|
if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
|
|
9921
11570
|
const compiled = compileRoutePattern(r.path);
|
|
9922
|
-
if (compiled.regex.test(
|
|
11571
|
+
if (compiled.regex.test(path24)) {
|
|
9923
11572
|
return true;
|
|
9924
11573
|
}
|
|
9925
11574
|
}
|
|
@@ -9927,14 +11576,14 @@ var PluginHost = class {
|
|
|
9927
11576
|
}
|
|
9928
11577
|
if (!instance) return false;
|
|
9929
11578
|
if (instance.context?.http) {
|
|
9930
|
-
return instance.context.http.isStream(upperMethod,
|
|
11579
|
+
return instance.context.http.isStream(upperMethod, path24);
|
|
9931
11580
|
}
|
|
9932
11581
|
if (instance.workerRef) {
|
|
9933
11582
|
const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
|
|
9934
11583
|
for (const r of routes) {
|
|
9935
11584
|
if (r.method === upperMethod && r.isStream) {
|
|
9936
11585
|
const compiled = compileRoutePattern(r.pattern);
|
|
9937
|
-
if (compiled.regex.test(
|
|
11586
|
+
if (compiled.regex.test(path24)) {
|
|
9938
11587
|
return true;
|
|
9939
11588
|
}
|
|
9940
11589
|
}
|
|
@@ -10912,6 +12561,7 @@ var PluginHost = class {
|
|
|
10912
12561
|
} catch (termErr) {
|
|
10913
12562
|
console.error(`[PluginHost] Worker termination error for "${pluginId}":`, termErr);
|
|
10914
12563
|
} finally {
|
|
12564
|
+
this.resourceTracker.disposeAll(pluginId);
|
|
10915
12565
|
this.pluginStates.set(pluginId, "inactive" /* INACTIVE */);
|
|
10916
12566
|
this.pluginInstances.delete(pluginId);
|
|
10917
12567
|
this.db.prepare("UPDATE plugins SET status = ? WHERE id = ?").run("inactive", pluginId);
|
|
@@ -10998,6 +12648,7 @@ var PluginHost = class {
|
|
|
10998
12648
|
await this.deactivatePlugin(pluginId);
|
|
10999
12649
|
}
|
|
11000
12650
|
}
|
|
12651
|
+
this.resourceTracker.disposeAll(pluginId);
|
|
11001
12652
|
const state = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
|
|
11002
12653
|
const row = this.db.prepare("SELECT manifest, file_path FROM plugins WHERE id = ?").get(pluginId);
|
|
11003
12654
|
if (state === "uninstalled" /* UNINSTALLED */ && !row) {
|
|
@@ -11031,6 +12682,12 @@ var PluginHost = class {
|
|
|
11031
12682
|
} catch (e) {
|
|
11032
12683
|
console.warn(`[PluginHost] Failed to drop plugin tables for "${pluginId}":`, e);
|
|
11033
12684
|
}
|
|
12685
|
+
try {
|
|
12686
|
+
const capService = await this.serviceRegistry.resolve(ICapabilityServiceToken);
|
|
12687
|
+
await capService.revokeAll(`plugin:${manifestId}`);
|
|
12688
|
+
} catch (capErr) {
|
|
12689
|
+
console.warn(`[PluginHost] Failed to revoke capabilities for "${pluginId}":`, capErr);
|
|
12690
|
+
}
|
|
11034
12691
|
if (this.expressApp && this._registeredRoutes.has(manifestId)) {
|
|
11035
12692
|
try {
|
|
11036
12693
|
const route = this._registeredRoutes.get(manifestId);
|
|
@@ -11186,8 +12843,8 @@ var PluginHost = class {
|
|
|
11186
12843
|
dependencies: manifest.dependencies
|
|
11187
12844
|
};
|
|
11188
12845
|
import_fs5.default.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
|
|
11189
|
-
const { execSync
|
|
11190
|
-
|
|
12846
|
+
const { execSync } = await import("node:child_process");
|
|
12847
|
+
execSync(
|
|
11191
12848
|
"npm install --production --no-audit --no-fund --legacy-peer-deps --ignore-scripts --registry=https://registry.npmmirror.com",
|
|
11192
12849
|
{
|
|
11193
12850
|
cwd: pluginDir,
|
|
@@ -11213,8 +12870,8 @@ var PluginHost = class {
|
|
|
11213
12870
|
if (import_fs5.default.existsSync(deployScriptPath)) {
|
|
11214
12871
|
console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
|
|
11215
12872
|
try {
|
|
11216
|
-
const { execSync
|
|
11217
|
-
|
|
12873
|
+
const { execSync } = await import("node:child_process");
|
|
12874
|
+
execSync(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
|
|
11218
12875
|
console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
|
|
11219
12876
|
} catch (deployErr) {
|
|
11220
12877
|
console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
|
|
@@ -11435,8 +13092,8 @@ var PluginHost = class {
|
|
|
11435
13092
|
),
|
|
11436
13093
|
"utf-8"
|
|
11437
13094
|
);
|
|
11438
|
-
const { execSync
|
|
11439
|
-
|
|
13095
|
+
const { execSync } = await import("node:child_process");
|
|
13096
|
+
execSync(
|
|
11440
13097
|
"npm install --production --no-audit --no-fund --legacy-peer-deps --registry=https://registry.npmmirror.com",
|
|
11441
13098
|
{
|
|
11442
13099
|
cwd: pluginDir,
|
|
@@ -11944,8 +13601,8 @@ var HotReloadController = class {
|
|
|
11944
13601
|
const timer = setTimeout(async () => {
|
|
11945
13602
|
this.debounceTimers.delete(pluginId);
|
|
11946
13603
|
try {
|
|
11947
|
-
const
|
|
11948
|
-
const newSourceCode = await
|
|
13604
|
+
const fs18 = await import("fs/promises");
|
|
13605
|
+
const newSourceCode = await fs18.readFile(filePath, "utf-8");
|
|
11949
13606
|
console.log(`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`);
|
|
11950
13607
|
await this.pluginHost.reloadPlugin(pluginId, newSourceCode);
|
|
11951
13608
|
} catch (err) {
|
|
@@ -18385,6 +20042,7 @@ var Kernel = class {
|
|
|
18385
20042
|
getServiceRegistryKernel: async () => this.platformServiceRegistryKernel
|
|
18386
20043
|
});
|
|
18387
20044
|
this.serviceRegistry.register(IAuthSessionBridgeToken, new AuthSessionBridgeService(this.db));
|
|
20045
|
+
this.serviceRegistry.register(ICoursewareRuntimeScriptRegistryToken, new CoursewareRuntimeScriptRegistry());
|
|
18388
20046
|
this.commandBus.setInterceptor(async (command) => {
|
|
18389
20047
|
const action = this.actionRegistry.getActionByCommandType(command.type);
|
|
18390
20048
|
if (action) {
|
|
@@ -18867,6 +20525,43 @@ function setupPresence({ io, eventBus, lookupStudentClassIds }) {
|
|
|
18867
20525
|
}
|
|
18868
20526
|
}
|
|
18869
20527
|
);
|
|
20528
|
+
socket.on(
|
|
20529
|
+
"student-client-error",
|
|
20530
|
+
(data) => {
|
|
20531
|
+
if (!data || !data.studentId || !data.error) return;
|
|
20532
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
20533
|
+
console.warn(`[Presence Security] Student ${session.userId} attempted to report error as ${data.studentId}`);
|
|
20534
|
+
return;
|
|
20535
|
+
}
|
|
20536
|
+
const studentName = data.studentName || onlineStudents.get(data.studentId)?.name || data.studentId;
|
|
20537
|
+
const lessonId = data.lessonId || activeStudentLessons.get(data.studentId) || null;
|
|
20538
|
+
const classId = data.classId || null;
|
|
20539
|
+
console.warn(
|
|
20540
|
+
`[Client Diagnostics] Student ${data.studentId} (${studentName}) reported error [${data.error.type || "runtime"}]: ${data.error.message || data.error.title} (Lesson: ${lessonId || "N/A"})`
|
|
20541
|
+
);
|
|
20542
|
+
eventBus.publish({
|
|
20543
|
+
id: `evt_err_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
20544
|
+
type: "student.client_error",
|
|
20545
|
+
source: "student_client",
|
|
20546
|
+
payload: {
|
|
20547
|
+
studentId: data.studentId,
|
|
20548
|
+
studentName,
|
|
20549
|
+
lessonId,
|
|
20550
|
+
classId,
|
|
20551
|
+
error: data.error
|
|
20552
|
+
},
|
|
20553
|
+
timestamp: data.error.timestamp || Date.now(),
|
|
20554
|
+
correlationId: lessonId || void 0
|
|
20555
|
+
});
|
|
20556
|
+
io.emit("student-error-alert", {
|
|
20557
|
+
studentId: data.studentId,
|
|
20558
|
+
studentName,
|
|
20559
|
+
lessonId,
|
|
20560
|
+
classId,
|
|
20561
|
+
error: data.error
|
|
20562
|
+
});
|
|
20563
|
+
}
|
|
20564
|
+
);
|
|
18870
20565
|
socket.on(
|
|
18871
20566
|
"teacher-broadcast-fullscreen",
|
|
18872
20567
|
(data) => {
|
|
@@ -19338,6 +21033,7 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
19338
21033
|
};
|
|
19339
21034
|
|
|
19340
21035
|
// server/middleware/auth.ts
|
|
21036
|
+
var SESSION_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
19341
21037
|
function getValidSession(token) {
|
|
19342
21038
|
let sessionRow;
|
|
19343
21039
|
try {
|
|
@@ -19356,11 +21052,21 @@ function getValidSession(token) {
|
|
|
19356
21052
|
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
19357
21053
|
return null;
|
|
19358
21054
|
}
|
|
19359
|
-
|
|
19360
|
-
|
|
21055
|
+
if (!sessionRow.updated_at || now - sessionRow.updated_at > SESSION_REFRESH_INTERVAL_MS) {
|
|
21056
|
+
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
21057
|
+
}
|
|
21058
|
+
let session;
|
|
21059
|
+
try {
|
|
21060
|
+
session = JSON.parse(sessionRow.session_data);
|
|
21061
|
+
} catch {
|
|
21062
|
+
return null;
|
|
21063
|
+
}
|
|
19361
21064
|
if (session && !session.userId && session.studentId) {
|
|
19362
21065
|
session.userId = session.studentId;
|
|
19363
21066
|
}
|
|
21067
|
+
if (session.role && !["teacher", "student", "administrator", "admin"].includes(session.role)) {
|
|
21068
|
+
session.role = "anonymous";
|
|
21069
|
+
}
|
|
19364
21070
|
return session;
|
|
19365
21071
|
}
|
|
19366
21072
|
function getCookieToken(req) {
|
|
@@ -19385,8 +21091,10 @@ function getActorId(req) {
|
|
|
19385
21091
|
if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
|
|
19386
21092
|
role = "administrator";
|
|
19387
21093
|
}
|
|
21094
|
+
const rawUserId = session.userId || session.studentId || "demo";
|
|
21095
|
+
const safeUserId = String(rawUserId).replace(/[:\s]/g, "_").slice(0, 64);
|
|
19388
21096
|
if (role) {
|
|
19389
|
-
return `user:${
|
|
21097
|
+
return `user:${safeUserId}:${role}`;
|
|
19390
21098
|
}
|
|
19391
21099
|
return "anonymous";
|
|
19392
21100
|
} catch {
|
|
@@ -19932,6 +21640,7 @@ function detectPromptInjection(input) {
|
|
|
19932
21640
|
// server/routes/shared.ts
|
|
19933
21641
|
var import_path7 = __toESM(require("path"), 1);
|
|
19934
21642
|
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
21643
|
+
init_interfaces();
|
|
19935
21644
|
|
|
19936
21645
|
// server/utils/bridge-sdk.ts
|
|
19937
21646
|
var BRIDGE_SDK_CODE = `(function() {
|
|
@@ -20711,7 +22420,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20711
22420
|
|
|
20712
22421
|
// server/routes/shared.ts
|
|
20713
22422
|
function validateMagicBytes(buffer, fileName) {
|
|
20714
|
-
const
|
|
22423
|
+
const MAGIC_BYTES2 = {
|
|
20715
22424
|
".pdf": [[37, 80, 68, 70]],
|
|
20716
22425
|
// %PDF
|
|
20717
22426
|
".pptx": [[80, 75, 3, 4]],
|
|
@@ -20727,11 +22436,34 @@ function validateMagicBytes(buffer, fileName) {
|
|
|
20727
22436
|
// RIFF
|
|
20728
22437
|
};
|
|
20729
22438
|
const ext = import_path7.default.extname(fileName || "").toLowerCase();
|
|
20730
|
-
const signatures =
|
|
22439
|
+
const signatures = MAGIC_BYTES2[ext];
|
|
20731
22440
|
if (!signatures) return true;
|
|
20732
22441
|
return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
|
|
20733
22442
|
}
|
|
20734
22443
|
var BLOCKED_EXTENSIONS = [".exe", ".sh", ".bat", ".cmd", ".dll", ".so", ".dylib", ".scr", ".msi", ".ps1"];
|
|
22444
|
+
function collectCoursewareRuntimeScripts(cwInfo) {
|
|
22445
|
+
const empty = { head: "", bodyEnd: "" };
|
|
22446
|
+
try {
|
|
22447
|
+
const registry = kernelContainer?.serviceRegistry?.resolve?.(ICoursewareRuntimeScriptRegistryToken);
|
|
22448
|
+
if (!registry || typeof registry.list !== "function") return empty;
|
|
22449
|
+
const scripts = registry.list({ id: cwInfo.id, uuid: cwInfo.uuid }) || [];
|
|
22450
|
+
if (!scripts.length) return empty;
|
|
22451
|
+
let head = "";
|
|
22452
|
+
let bodyEnd = "";
|
|
22453
|
+
for (const script of scripts) {
|
|
22454
|
+
if (!script || typeof script.source !== "string" || !script.source.trim()) continue;
|
|
22455
|
+
const tag = `
|
|
22456
|
+
<!-- Courseware Runtime Script (${script.owner}/${script.id}) -->
|
|
22457
|
+
<script>${script.source}</script>`;
|
|
22458
|
+
if (script.position === "head") head += tag;
|
|
22459
|
+
else bodyEnd += tag;
|
|
22460
|
+
}
|
|
22461
|
+
return { head, bodyEnd };
|
|
22462
|
+
} catch (err) {
|
|
22463
|
+
console.warn("[injectLmsSdk] \u8BFB\u53D6\u8BFE\u4EF6\u8FD0\u884C\u65F6\u811A\u672C\u6269\u5C55\u70B9\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", err.message);
|
|
22464
|
+
return empty;
|
|
22465
|
+
}
|
|
22466
|
+
}
|
|
20735
22467
|
function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
20736
22468
|
const token = getCookieToken(req);
|
|
20737
22469
|
let studentInfo = {
|
|
@@ -20789,6 +22521,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20789
22521
|
}
|
|
20790
22522
|
studentInfo.attempt_id = attempt.id;
|
|
20791
22523
|
}
|
|
22524
|
+
const runtimeScripts = collectCoursewareRuntimeScripts(cwInfo);
|
|
20792
22525
|
const injection = `
|
|
20793
22526
|
<!-- LMS Courseware SDK Inject -->
|
|
20794
22527
|
<script>
|
|
@@ -20798,7 +22531,7 @@ window.__LMS_COURSEWARE__ = {
|
|
|
20798
22531
|
name: ${JSON.stringify(cwInfo.name)}
|
|
20799
22532
|
};
|
|
20800
22533
|
</script>
|
|
20801
|
-
<script>${BRIDGE_SDK_CODE}</script
|
|
22534
|
+
<script>${BRIDGE_SDK_CODE}</script>${runtimeScripts.head}
|
|
20802
22535
|
`;
|
|
20803
22536
|
let html = htmlContent;
|
|
20804
22537
|
html = html.replace(
|
|
@@ -20826,6 +22559,13 @@ window.__LMS_COURSEWARE__ = {
|
|
|
20826
22559
|
} else {
|
|
20827
22560
|
html = injection + html;
|
|
20828
22561
|
}
|
|
22562
|
+
if (runtimeScripts.bodyEnd) {
|
|
22563
|
+
if (/<\/body>/i.test(html)) {
|
|
22564
|
+
html = html.replace(/<\/body>/i, `${runtimeScripts.bodyEnd}</body>`);
|
|
22565
|
+
} else {
|
|
22566
|
+
html += runtimeScripts.bodyEnd;
|
|
22567
|
+
}
|
|
22568
|
+
}
|
|
20829
22569
|
return html;
|
|
20830
22570
|
}
|
|
20831
22571
|
var generateStudentNumber = (db2) => {
|
|
@@ -21150,7 +22890,7 @@ var import_path9 = __toESM(require("path"), 1);
|
|
|
21150
22890
|
var import_fs9 = __toESM(require("fs"), 1);
|
|
21151
22891
|
function registerResourcesRoutes(ctx) {
|
|
21152
22892
|
const { app } = ctx;
|
|
21153
|
-
app.get("/api/resources", (req, res) => {
|
|
22893
|
+
app.get("/api/resources", requireAuth(), (req, res) => {
|
|
21154
22894
|
try {
|
|
21155
22895
|
const resources = kernelContainer.db.prepare("SELECT id, name, type, created_at FROM system_resources ORDER BY created_at DESC").all();
|
|
21156
22896
|
res.json(resources);
|
|
@@ -21158,7 +22898,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
21158
22898
|
sendSafeError(res, e);
|
|
21159
22899
|
}
|
|
21160
22900
|
});
|
|
21161
|
-
app.get("/api/resources/:id", (req, res) => {
|
|
22901
|
+
app.get("/api/resources/:id", requireAuth(), (req, res) => {
|
|
21162
22902
|
try {
|
|
21163
22903
|
const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
|
|
21164
22904
|
if (!resource) return res.status(404).send("Resource not found");
|
|
@@ -21258,7 +22998,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
21258
22998
|
sendSafeError(res, e);
|
|
21259
22999
|
}
|
|
21260
23000
|
});
|
|
21261
|
-
app.get("/api/resources/:id/*", (req, res) => {
|
|
23001
|
+
app.get("/api/resources/:id/*", requireAuth(), (req, res) => {
|
|
21262
23002
|
try {
|
|
21263
23003
|
const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
|
|
21264
23004
|
if (!resource) return res.status(404).send("Resource not found");
|
|
@@ -21490,7 +23230,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21490
23230
|
app.get("/api/courseware", requireAuth(), async (req, res) => {
|
|
21491
23231
|
try {
|
|
21492
23232
|
const actorId = getActorId(req) || "system";
|
|
21493
|
-
const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId);
|
|
23233
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId, { silent: true });
|
|
21494
23234
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
21495
23235
|
res.json(result);
|
|
21496
23236
|
} catch (e) {
|
|
@@ -21613,13 +23353,21 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21613
23353
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
21614
23354
|
).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
|
|
21615
23355
|
const extracted = extractScoreCommentCompletion(payload);
|
|
21616
|
-
const score = extracted.score;
|
|
21617
23356
|
const comment = extracted.comment;
|
|
21618
23357
|
const completion = extracted.completion;
|
|
21619
|
-
|
|
23358
|
+
let aggregatedScore = null;
|
|
23359
|
+
let aggregationExtra = {};
|
|
23360
|
+
try {
|
|
23361
|
+
const aggregation = aggregateAttemptScore(kernelContainer.db, attemptId);
|
|
23362
|
+
if (aggregation.finalScore !== null) aggregatedScore = aggregation.finalScore;
|
|
23363
|
+
aggregationExtra = { score_aggregation: describeAggregation(aggregation) };
|
|
23364
|
+
} catch (aggErr) {
|
|
23365
|
+
console.warn("[courseware.log] score aggregation failed, fallback to latest score:", aggErr);
|
|
23366
|
+
}
|
|
23367
|
+
if (aggregatedScore !== null || comment !== void 0 || completion !== void 0) {
|
|
21620
23368
|
let parsedScore = null;
|
|
21621
|
-
if (score !== void 0 && score !== null) {
|
|
21622
|
-
const num = parseFloat(score);
|
|
23369
|
+
if (extracted.score !== void 0 && extracted.score !== null) {
|
|
23370
|
+
const num = parseFloat(extracted.score);
|
|
21623
23371
|
if (!isNaN(num)) {
|
|
21624
23372
|
parsedScore = num;
|
|
21625
23373
|
}
|
|
@@ -21638,13 +23386,16 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21638
23386
|
).run(
|
|
21639
23387
|
"res_" + import_crypto8.default.randomBytes(8).toString("hex"),
|
|
21640
23388
|
attemptId,
|
|
21641
|
-
parsedScore,
|
|
23389
|
+
aggregatedScore !== null ? aggregatedScore : parsedScore,
|
|
21642
23390
|
comment || null,
|
|
21643
23391
|
parsedCompletion,
|
|
21644
|
-
JSON.stringify(
|
|
23392
|
+
JSON.stringify({
|
|
23393
|
+
...payload && typeof payload === "object" ? payload : {},
|
|
23394
|
+
...aggregationExtra
|
|
23395
|
+
})
|
|
21645
23396
|
);
|
|
21646
23397
|
} else {
|
|
21647
|
-
const finalScore = parsedScore !== null ? parsedScore : existing.score;
|
|
23398
|
+
const finalScore = aggregatedScore !== null ? aggregatedScore : parsedScore !== null ? parsedScore : existing.score;
|
|
21648
23399
|
const finalComment = comment || existing.comment;
|
|
21649
23400
|
const finalCompletion = parsedCompletion !== null ? parsedCompletion : existing.completion;
|
|
21650
23401
|
let mergedExtra = {};
|
|
@@ -21655,6 +23406,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21655
23406
|
if (payload && typeof payload === "object") {
|
|
21656
23407
|
mergedExtra = { ...mergedExtra, ...payload };
|
|
21657
23408
|
}
|
|
23409
|
+
mergedExtra = { ...mergedExtra, ...aggregationExtra };
|
|
21658
23410
|
kernelContainer.db.prepare(
|
|
21659
23411
|
"UPDATE submission_result SET score = ?, comment = ?, completion = ?, extra_json = ? WHERE attempt_id = ?"
|
|
21660
23412
|
).run(finalScore, finalComment, finalCompletion, JSON.stringify(mergedExtra), attemptId);
|
|
@@ -21707,17 +23459,17 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21707
23459
|
parsedCompletion = num;
|
|
21708
23460
|
}
|
|
21709
23461
|
}
|
|
21710
|
-
const
|
|
23462
|
+
const normalizedActorId = getActorId(req);
|
|
23463
|
+
const actorId = normalizedActorId && normalizedActorId !== "anonymous" ? normalizedActorId : `user:${session.userId || session.studentId || "student"}:${session.role || "student"}`;
|
|
23464
|
+
const payload = { attemptId };
|
|
23465
|
+
if (parsedScore !== null) payload.score = parsedScore;
|
|
23466
|
+
if (parsedCompletion !== null) payload.completion = parsedCompletion;
|
|
23467
|
+
if (comment !== void 0 && comment !== null) payload.comment = comment;
|
|
23468
|
+
if (status !== void 0 && status !== null) payload.status = status;
|
|
23469
|
+
if (extra && typeof extra === "object" && !Array.isArray(extra)) payload.extra = extra;
|
|
21711
23470
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
21712
23471
|
"courseware.submit_attempt",
|
|
21713
|
-
|
|
21714
|
-
attemptId,
|
|
21715
|
-
score: parsedScore,
|
|
21716
|
-
comment,
|
|
21717
|
-
completion: parsedCompletion,
|
|
21718
|
-
status,
|
|
21719
|
-
extra
|
|
21720
|
-
},
|
|
23472
|
+
payload,
|
|
21721
23473
|
actorId
|
|
21722
23474
|
);
|
|
21723
23475
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
@@ -21727,11 +23479,65 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21727
23479
|
sendSafeError(res, e);
|
|
21728
23480
|
}
|
|
21729
23481
|
});
|
|
21730
|
-
app.
|
|
23482
|
+
app.post("/api/courseware/attempts/:attemptId/adopt", (req, res) => {
|
|
21731
23483
|
try {
|
|
21732
|
-
const
|
|
21733
|
-
|
|
21734
|
-
|
|
23484
|
+
const { attemptId } = req.params;
|
|
23485
|
+
const token = getCookieToken(req);
|
|
23486
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
23487
|
+
if (!session) {
|
|
23488
|
+
return res.status(401).json({ error: "Authentication required to adopt an attempt" });
|
|
23489
|
+
}
|
|
23490
|
+
const db2 = kernelContainer.db;
|
|
23491
|
+
const attemptRow = db2.prepare("SELECT id, courseware_id, student_id, status FROM courseware_attempt WHERE id = ?").get(attemptId);
|
|
23492
|
+
if (!attemptRow) {
|
|
23493
|
+
return res.status(404).json({ error: "Attempt not found" });
|
|
23494
|
+
}
|
|
23495
|
+
if (session.role === "teacher" || session.role === "administrator") {
|
|
23496
|
+
return res.json({ attemptId, adopted: false, reused: true, role: session.role });
|
|
23497
|
+
}
|
|
23498
|
+
const studentId = session.userId || session.studentId;
|
|
23499
|
+
if (!studentId) {
|
|
23500
|
+
return res.status(400).json({ error: "Session has no student identity" });
|
|
23501
|
+
}
|
|
23502
|
+
if (attemptRow.student_id === studentId) {
|
|
23503
|
+
return res.json({ attemptId, adopted: false, reused: true });
|
|
23504
|
+
}
|
|
23505
|
+
const UNOWNED_OWNERS = ["guest", "teacher", "teacher_preview", ""];
|
|
23506
|
+
if (UNOWNED_OWNERS.includes(attemptRow.student_id)) {
|
|
23507
|
+
const info = db2.prepare(
|
|
23508
|
+
"UPDATE courseware_attempt SET student_id = ? WHERE id = ? AND student_id IN ('guest','teacher','teacher_preview','')"
|
|
23509
|
+
).run(studentId, attemptId);
|
|
23510
|
+
if (info.changes > 0) {
|
|
23511
|
+
io.emit("courseware-attempt-updated", { attemptId, type: "adopt" });
|
|
23512
|
+
return res.json({ attemptId, adopted: true, reused: true });
|
|
23513
|
+
}
|
|
23514
|
+
}
|
|
23515
|
+
let own = db2.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(attemptRow.courseware_id, studentId, "active");
|
|
23516
|
+
if (!own) {
|
|
23517
|
+
const newId = "att_" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
23518
|
+
db2.prepare(
|
|
23519
|
+
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
23520
|
+
).run(newId, attemptRow.courseware_id, studentId, Date.now(), "active");
|
|
23521
|
+
own = { id: newId };
|
|
23522
|
+
}
|
|
23523
|
+
io.emit("courseware-attempt-updated", { attemptId: own.id, type: "adopt" });
|
|
23524
|
+
return res.json({
|
|
23525
|
+
attemptId: own.id,
|
|
23526
|
+
adopted: false,
|
|
23527
|
+
reused: true,
|
|
23528
|
+
reason: "attempt-owned-by-another-student"
|
|
23529
|
+
});
|
|
23530
|
+
} catch (e) {
|
|
23531
|
+
sendSafeError(res, e);
|
|
23532
|
+
}
|
|
23533
|
+
});
|
|
23534
|
+
app.get("/api/courseware/attempts", requireAuth(), (req, res) => {
|
|
23535
|
+
try {
|
|
23536
|
+
const session = req.session;
|
|
23537
|
+
const isStaff = session?.role === "teacher" || session?.role === "administrator" || session?.subRole === "administrator";
|
|
23538
|
+
const coursewareUuid = typeof req.query.coursewareUuid === "string" ? req.query.coursewareUuid.trim() : "";
|
|
23539
|
+
const baseSql = `
|
|
23540
|
+
SELECT a.id as attemptId, a.started_at, a.finished_at, a.status,
|
|
21735
23541
|
cw.name as coursewareName, cw.uuid as coursewareUuid,
|
|
21736
23542
|
COALESCE(s.name, CASE WHEN a.student_id = 'teacher' THEN 'Teacher (Test)' WHEN a.student_id = 'guest' THEN 'Guest Student' ELSE a.student_id END) as studentName,
|
|
21737
23543
|
a.student_id as studentId,
|
|
@@ -21739,26 +23545,40 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21739
23545
|
(
|
|
21740
23546
|
SELECT COUNT(*) FROM assignment_submissions sub
|
|
21741
23547
|
JOIN assignments ast ON sub.assignment_id = ast.id
|
|
21742
|
-
WHERE sub.student_id = a.student_id
|
|
23548
|
+
WHERE sub.student_id = a.student_id
|
|
21743
23549
|
AND ast.title = '\u4E92\u52A8\u8BFE\u4EF6: ' || cw.name
|
|
21744
23550
|
) as isPromoted
|
|
21745
23551
|
FROM courseware_attempt a
|
|
21746
23552
|
JOIN courseware cw ON a.courseware_id = cw.id
|
|
21747
23553
|
LEFT JOIN students s ON a.student_id = s.id
|
|
21748
23554
|
LEFT JOIN submission_result r ON a.id = r.attempt_id
|
|
21749
|
-
|
|
21750
|
-
`
|
|
21751
|
-
|
|
21752
|
-
|
|
23555
|
+
`;
|
|
23556
|
+
const sql = coursewareUuid ? `${baseSql} WHERE cw.uuid = ? ORDER BY a.started_at DESC` : `${baseSql} ORDER BY a.started_at DESC`;
|
|
23557
|
+
const stmt = kernelContainer.db.prepare(sql);
|
|
23558
|
+
const rows = coursewareUuid ? stmt.all(coursewareUuid) : stmt.all();
|
|
23559
|
+
if (isStaff) {
|
|
23560
|
+
res.json(rows);
|
|
23561
|
+
return;
|
|
23562
|
+
}
|
|
23563
|
+
res.json(
|
|
23564
|
+
rows.map((row) => {
|
|
23565
|
+
const sanitized = { ...row };
|
|
23566
|
+
delete sanitized.extra_json;
|
|
23567
|
+
delete sanitized.comment;
|
|
23568
|
+
return sanitized;
|
|
23569
|
+
})
|
|
23570
|
+
);
|
|
21753
23571
|
} catch (e) {
|
|
21754
23572
|
sendSafeError(res, e);
|
|
21755
23573
|
}
|
|
21756
23574
|
});
|
|
21757
|
-
app.post("/api/courseware/debug", (req, res) => {
|
|
23575
|
+
app.post("/api/courseware/debug", requireAuth(), (req, res) => {
|
|
21758
23576
|
try {
|
|
21759
23577
|
const { msg, url, student, courseware } = req.body;
|
|
21760
|
-
const
|
|
21761
|
-
|
|
23578
|
+
const safeMsg = typeof msg === "string" ? msg.slice(0, 2e3) : String(msg).slice(0, 2e3);
|
|
23579
|
+
const safeUrl = typeof url === "string" ? url.slice(0, 1e3) : String(url).slice(0, 1e3);
|
|
23580
|
+
const logMsg = `[CLIENT DEBUG] ${safeMsg} | URL: ${safeUrl} | Student: ${JSON.stringify(student)?.slice(0, 1e3)} | Courseware: ${JSON.stringify(courseware)?.slice(0, 1e3)}`;
|
|
23581
|
+
console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${safeMsg}`);
|
|
21762
23582
|
const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
|
|
21763
23583
|
import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
|
|
21764
23584
|
`);
|
|
@@ -21767,13 +23587,20 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21767
23587
|
sendSafeError(res, e);
|
|
21768
23588
|
}
|
|
21769
23589
|
});
|
|
21770
|
-
app.get("/api/courseware/attempts/:attemptId/raw", async (req, res) => {
|
|
23590
|
+
app.get("/api/courseware/attempts/:attemptId/raw", requireAuth(), async (req, res) => {
|
|
21771
23591
|
try {
|
|
21772
23592
|
const { attemptId } = req.params;
|
|
23593
|
+
const session = req.session;
|
|
23594
|
+
if (session.role !== "teacher" && session.role !== "administrator") {
|
|
23595
|
+
const owner = kernelContainer.db.prepare("SELECT student_id FROM courseware_attempt WHERE id = ?").get(attemptId);
|
|
23596
|
+
if (!owner || owner.student_id !== (session.userId || session.studentId)) {
|
|
23597
|
+
return res.status(403).json({ error: "Forbidden: Cannot read another student attempt" });
|
|
23598
|
+
}
|
|
23599
|
+
}
|
|
21773
23600
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
21774
23601
|
"courseware.get_attempt_raw_data",
|
|
21775
23602
|
{ attemptId },
|
|
21776
|
-
|
|
23603
|
+
getActorId(req)
|
|
21777
23604
|
);
|
|
21778
23605
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
21779
23606
|
res.json(result);
|
|
@@ -21781,9 +23608,16 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21781
23608
|
sendSafeError(res, e);
|
|
21782
23609
|
}
|
|
21783
23610
|
});
|
|
21784
|
-
app.get("/api/courseware/attempts/:attemptId/progress", (req, res) => {
|
|
23611
|
+
app.get("/api/courseware/attempts/:attemptId/progress", requireAuth(), (req, res) => {
|
|
21785
23612
|
try {
|
|
21786
23613
|
const { attemptId } = req.params;
|
|
23614
|
+
const session = req.session;
|
|
23615
|
+
if (session && session.role !== "teacher" && session.role !== "administrator" && session.subRole !== "administrator") {
|
|
23616
|
+
const owner = kernelContainer.db.prepare("SELECT student_id FROM courseware_attempt WHERE id = ?").get(attemptId);
|
|
23617
|
+
if (!owner || owner.student_id !== (session.userId || session.studentId)) {
|
|
23618
|
+
return res.status(403).json({ success: false, error: "Forbidden: Cannot read another student attempt" });
|
|
23619
|
+
}
|
|
23620
|
+
}
|
|
21787
23621
|
const result = kernelContainer.db.prepare("SELECT score, comment, completion, extra_json FROM submission_result WHERE attempt_id = ?").get(attemptId);
|
|
21788
23622
|
if (!result) {
|
|
21789
23623
|
return res.json({ progress: null });
|
|
@@ -21800,7 +23634,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21800
23634
|
sendSafeError(res, e);
|
|
21801
23635
|
}
|
|
21802
23636
|
});
|
|
21803
|
-
app.post("/api/courseware/attempts/:attemptId/promote", async (req, res) => {
|
|
23637
|
+
app.post("/api/courseware/attempts/:attemptId/promote", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
21804
23638
|
try {
|
|
21805
23639
|
const { attemptId } = req.params;
|
|
21806
23640
|
const { lessonId, classId } = req.body;
|
|
@@ -21833,24 +23667,25 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21833
23667
|
}
|
|
21834
23668
|
}
|
|
21835
23669
|
const assignmentTitle = `\u4E92\u52A8\u8BFE\u4EF6: ${coursewareName}`;
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
|
|
21844
|
-
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21848
|
-
|
|
21849
|
-
|
|
21850
|
-
|
|
21851
|
-
|
|
21852
|
-
|
|
21853
|
-
|
|
23670
|
+
const promoteTx = kernelContainer.db.transaction(() => {
|
|
23671
|
+
let assignment = kernelContainer.db.prepare("SELECT id FROM assignments WHERE class_id = ? AND lesson_id = ? AND title = ?").get(classId, lessonId, assignmentTitle);
|
|
23672
|
+
let assignmentId2 = assignment?.id;
|
|
23673
|
+
if (!assignmentId2) {
|
|
23674
|
+
assignmentId2 = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
23675
|
+
kernelContainer.db.prepare(
|
|
23676
|
+
"INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
23677
|
+
).run(
|
|
23678
|
+
assignmentId2,
|
|
23679
|
+
classId,
|
|
23680
|
+
lessonId,
|
|
23681
|
+
assignmentTitle,
|
|
23682
|
+
`\u6765\u81EA\u4E92\u52A8\u8BFE\u4EF6 [${coursewareName}] \u7684\u968F\u5802\u5B66\u4E60\u63D0\u4EA4\u6570\u636E\u8BB0\u5F55`,
|
|
23683
|
+
JSON.stringify({ type: "interactive_courseware", attemptId, coursewareUuid: attempt.courseware_uuid }),
|
|
23684
|
+
Date.now()
|
|
23685
|
+
);
|
|
23686
|
+
}
|
|
23687
|
+
kernelContainer.db.prepare(
|
|
23688
|
+
`
|
|
21854
23689
|
INSERT INTO assignment_submissions (assignment_id, student_id, content, score, feedback, submitted_at, graded_at, status)
|
|
21855
23690
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'graded')
|
|
21856
23691
|
ON CONFLICT(assignment_id, student_id) DO UPDATE SET
|
|
@@ -21861,24 +23696,27 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21861
23696
|
graded_at = excluded.graded_at,
|
|
21862
23697
|
status = 'graded'
|
|
21863
23698
|
`
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21868
|
-
|
|
21869
|
-
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
|
|
23699
|
+
).run(
|
|
23700
|
+
assignmentId2,
|
|
23701
|
+
studentId,
|
|
23702
|
+
attempt.extra_json || "{}",
|
|
23703
|
+
finalScore,
|
|
23704
|
+
`\u7531\u6559\u5E08\u5728\u8BFE\u5802\u4E2D\u4FDD\u5B58\u5F55\u5165\u3002\u8BFE\u4EF6\u5B8C\u6210\u5EA6: ${Math.round(completion * 100)}%\u3002\u8BFE\u4EF6\u539F\u59CB\u53CD\uFFFD?: ${attempt.comment || "\uFFFD?"}`,
|
|
23705
|
+
Date.now(),
|
|
23706
|
+
Date.now()
|
|
23707
|
+
);
|
|
23708
|
+
kernelContainer.db.prepare(
|
|
23709
|
+
`
|
|
21875
23710
|
INSERT INTO student_lesson_progress (student_id, lesson_id, completed, progress_percent, completed_segments, assigned_at)
|
|
21876
23711
|
VALUES (?, ?, 1, 100, '[]', ?)
|
|
21877
23712
|
ON CONFLICT(student_id, lesson_id) DO UPDATE SET
|
|
21878
23713
|
completed = 1,
|
|
21879
23714
|
progress_percent = 100
|
|
21880
23715
|
`
|
|
21881
|
-
|
|
23716
|
+
).run(studentId, lessonId, Date.now());
|
|
23717
|
+
return assignmentId2;
|
|
23718
|
+
});
|
|
23719
|
+
const assignmentId = promoteTx();
|
|
21882
23720
|
io.emit("student-progress-updated", {
|
|
21883
23721
|
studentId,
|
|
21884
23722
|
lessonId,
|
|
@@ -21913,10 +23751,20 @@ var import_path11 = __toESM(require("path"), 1);
|
|
|
21913
23751
|
var import_fs11 = __toESM(require("fs"), 1);
|
|
21914
23752
|
function registerBridgeRoutes(ctx) {
|
|
21915
23753
|
const { app } = ctx;
|
|
21916
|
-
app.get("/bridge.js", (
|
|
23754
|
+
app.get("/bridge.js", (req, res) => {
|
|
21917
23755
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
21918
23756
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
21919
|
-
|
|
23757
|
+
const cw = typeof req.query.cw === "string" ? req.query.cw : "";
|
|
23758
|
+
const cwName = typeof req.query.name === "string" ? req.query.name : "";
|
|
23759
|
+
let runtimeScripts = "";
|
|
23760
|
+
try {
|
|
23761
|
+
const scripts = collectCoursewareRuntimeScripts({ id: cw, name: cwName, uuid: cw });
|
|
23762
|
+
runtimeScripts = scripts.head + scripts.bodyEnd;
|
|
23763
|
+
} catch (e) {
|
|
23764
|
+
runtimeScripts = "";
|
|
23765
|
+
}
|
|
23766
|
+
res.send(runtimeScripts ? `${BRIDGE_SDK_CODE}
|
|
23767
|
+
${runtimeScripts}` : BRIDGE_SDK_CODE);
|
|
21920
23768
|
});
|
|
21921
23769
|
app.get("/runtime/:uuid", (req, res, next) => {
|
|
21922
23770
|
if (req.path.endsWith("/")) return next();
|
|
@@ -22166,7 +24014,7 @@ function requireWhiteboardWriteAccess() {
|
|
|
22166
24014
|
}
|
|
22167
24015
|
function registerLessonsRoutes(ctx) {
|
|
22168
24016
|
const { app, io } = ctx;
|
|
22169
|
-
app.get("/api/lessons", (req, res) => {
|
|
24017
|
+
app.get("/api/lessons", requireAuth(), (req, res) => {
|
|
22170
24018
|
const lessons = kernelContainer.db.prepare(
|
|
22171
24019
|
`
|
|
22172
24020
|
SELECT l.*, u.name as creator_name,
|
|
@@ -22178,7 +24026,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
22178
24026
|
).all();
|
|
22179
24027
|
res.json(lessons);
|
|
22180
24028
|
});
|
|
22181
|
-
app.get("/api/lessons/:lessonId/eval-submissions", (req, res) => {
|
|
24029
|
+
app.get("/api/lessons/:lessonId/eval-submissions", requireAuth(), (req, res) => {
|
|
22182
24030
|
try {
|
|
22183
24031
|
const { lessonId } = req.params;
|
|
22184
24032
|
const rows = kernelContainer.db.prepare(
|
|
@@ -22194,7 +24042,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
22194
24042
|
sendSafeError(res, err);
|
|
22195
24043
|
}
|
|
22196
24044
|
});
|
|
22197
|
-
app.get("/api/lessons/:lessonId/eval-grades", (req, res) => {
|
|
24045
|
+
app.get("/api/lessons/:lessonId/eval-grades", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22198
24046
|
try {
|
|
22199
24047
|
const { lessonId } = req.params;
|
|
22200
24048
|
const submissions = kernelContainer.db.prepare(
|
|
@@ -22244,7 +24092,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
22244
24092
|
sendSafeError(res, err);
|
|
22245
24093
|
}
|
|
22246
24094
|
});
|
|
22247
|
-
app.get("/api/eval-submissions/:submissionId/reviews", (req, res) => {
|
|
24095
|
+
app.get("/api/eval-submissions/:submissionId/reviews", requireAuth(), (req, res) => {
|
|
22248
24096
|
try {
|
|
22249
24097
|
const { submissionId } = req.params;
|
|
22250
24098
|
const rows = kernelContainer.db.prepare(
|
|
@@ -22260,7 +24108,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
22260
24108
|
sendSafeError(res, err);
|
|
22261
24109
|
}
|
|
22262
24110
|
});
|
|
22263
|
-
app.get("/api/lessons/:lessonId/students/:studentId/eval-status", (req, res) => {
|
|
24111
|
+
app.get("/api/lessons/:lessonId/students/:studentId/eval-status", requireAuth(), (req, res) => {
|
|
22264
24112
|
try {
|
|
22265
24113
|
const { lessonId, studentId } = req.params;
|
|
22266
24114
|
const submission = kernelContainer.db.prepare(
|
|
@@ -22361,7 +24209,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
22361
24209
|
sendSafeError(res, e);
|
|
22362
24210
|
}
|
|
22363
24211
|
});
|
|
22364
|
-
app.get("/api/lessons/:id/whiteboard", (req, res) => {
|
|
24212
|
+
app.get("/api/lessons/:id/whiteboard", requireAuth(), (req, res) => {
|
|
22365
24213
|
const id = req.params.id;
|
|
22366
24214
|
const elements = kernelContainer.db.prepare("SELECT * FROM whiteboard_elements WHERE lesson_id = ?").all(id);
|
|
22367
24215
|
if (!id.startsWith("assignment-") && !id.startsWith("snapshot-")) {
|
|
@@ -22519,6 +24367,18 @@ function registerLessonsRoutes(ctx) {
|
|
|
22519
24367
|
dataObj.submissions[studentId] = { answer, score, time: Date.now() };
|
|
22520
24368
|
kernelContainer.db.prepare("UPDATE whiteboard_elements SET data = ? WHERE id = ?").run(JSON.stringify(dataObj), elementId);
|
|
22521
24369
|
io.to(`lesson-${lessonId}`).emit("whiteboard-sync", { type: "element-updated", elementId });
|
|
24370
|
+
io.emit("whiteboard-quiz-answered", {
|
|
24371
|
+
lessonId,
|
|
24372
|
+
elementId,
|
|
24373
|
+
studentId,
|
|
24374
|
+
studentName: session.studentName || session.name || null,
|
|
24375
|
+
answer,
|
|
24376
|
+
score,
|
|
24377
|
+
isCorrect,
|
|
24378
|
+
time: Date.now(),
|
|
24379
|
+
correctAnswer: dataObj.correctAnswer || null,
|
|
24380
|
+
question: dataObj.question || null
|
|
24381
|
+
});
|
|
22522
24382
|
res.json({ success: true, isCorrect, score, studentId });
|
|
22523
24383
|
} catch (e) {
|
|
22524
24384
|
sendSafeError(res, e);
|
|
@@ -22592,11 +24452,14 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22592
24452
|
if (!ownership.allowed) {
|
|
22593
24453
|
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
22594
24454
|
}
|
|
22595
|
-
kernelContainer.db.
|
|
22596
|
-
|
|
22597
|
-
|
|
22598
|
-
|
|
22599
|
-
|
|
24455
|
+
const delTx = kernelContainer.db.transaction(() => {
|
|
24456
|
+
kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?").run(id);
|
|
24457
|
+
kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE lesson_id = ?").run(id);
|
|
24458
|
+
kernelContainer.db.prepare("DELETE FROM schedules WHERE lesson_id = ?").run(id);
|
|
24459
|
+
kernelContainer.db.prepare("DELETE FROM assignments WHERE lesson_id = ?").run(id);
|
|
24460
|
+
return kernelContainer.db.prepare("DELETE FROM lessons WHERE id = ?").run(id);
|
|
24461
|
+
});
|
|
24462
|
+
const result = delTx();
|
|
22600
24463
|
if (result.changes === 0) {
|
|
22601
24464
|
return res.status(404).json({ error: "Lesson not found" });
|
|
22602
24465
|
}
|
|
@@ -22605,7 +24468,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22605
24468
|
sendSafeError(res, e);
|
|
22606
24469
|
}
|
|
22607
24470
|
});
|
|
22608
|
-
app.get("/api/lessons/:id/stats", (req, res) => {
|
|
24471
|
+
app.get("/api/lessons/:id/stats", requireAuth(), (req, res) => {
|
|
22609
24472
|
try {
|
|
22610
24473
|
const { id } = req.params;
|
|
22611
24474
|
const whiteboardCount = kernelContainer.db.prepare("SELECT COUNT(*) as count FROM whiteboard_elements WHERE lesson_id = ?").get(id).count;
|
|
@@ -22629,26 +24492,29 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22629
24492
|
const newId = (0, import_uuid16.v7)();
|
|
22630
24493
|
const now = Date.now();
|
|
22631
24494
|
const newTitle = `\u526F\u672C-${original.title}`;
|
|
22632
|
-
kernelContainer.db.
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
24495
|
+
const cloneTx = kernelContainer.db.transaction(() => {
|
|
24496
|
+
kernelContainer.db.prepare(
|
|
24497
|
+
"INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
24498
|
+
).run(
|
|
24499
|
+
newId,
|
|
24500
|
+
newTitle,
|
|
24501
|
+
original.content,
|
|
24502
|
+
original.timeline,
|
|
24503
|
+
original.progress_mode,
|
|
24504
|
+
original.progress_conditions,
|
|
24505
|
+
creatorId,
|
|
24506
|
+
now,
|
|
24507
|
+
now
|
|
24508
|
+
);
|
|
24509
|
+
const whiteboardElements = kernelContainer.db.prepare("SELECT * FROM whiteboard_elements WHERE lesson_id = ?").all(id);
|
|
24510
|
+
const insertElement = kernelContainer.db.prepare(
|
|
24511
|
+
"INSERT INTO whiteboard_elements (id, lesson_id, type, data, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
24512
|
+
);
|
|
24513
|
+
for (const el of whiteboardElements) {
|
|
24514
|
+
insertElement.run((0, import_uuid16.v7)(), newId, el.type, el.data, now);
|
|
24515
|
+
}
|
|
24516
|
+
});
|
|
24517
|
+
cloneTx();
|
|
22652
24518
|
const cloned = kernelContainer.db.prepare(
|
|
22653
24519
|
`
|
|
22654
24520
|
SELECT l.*, u.name as creator_name, 0 as enrollment_count
|
|
@@ -22666,9 +24532,11 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22666
24532
|
|
|
22667
24533
|
// server/routes/workspace.ts
|
|
22668
24534
|
var import_path12 = __toESM(require("path"), 1);
|
|
24535
|
+
var DIAGNOSTIC_MIN_INTERVAL_MS = 1e3;
|
|
24536
|
+
var diagnosticLastReportAt = /* @__PURE__ */ new Map();
|
|
22669
24537
|
function registerWorkspaceRoutes(ctx) {
|
|
22670
24538
|
const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
|
|
22671
|
-
app.get("/api/events", requireAuth("administrator"), (req, res) => {
|
|
24539
|
+
app.get("/api/events", requireAuth("administrator", "teacher"), (req, res) => {
|
|
22672
24540
|
try {
|
|
22673
24541
|
const events = kernelContainer.db.prepare("SELECT * FROM events ORDER BY timestamp DESC LIMIT 50").all();
|
|
22674
24542
|
res.json(events);
|
|
@@ -22676,6 +24544,69 @@ function registerWorkspaceRoutes(ctx) {
|
|
|
22676
24544
|
sendSafeError(res, e);
|
|
22677
24545
|
}
|
|
22678
24546
|
});
|
|
24547
|
+
app.post("/api/diagnostics/report", requireAuth(), (req, res) => {
|
|
24548
|
+
try {
|
|
24549
|
+
const data = req.body || {};
|
|
24550
|
+
const session = req.session || {};
|
|
24551
|
+
const actorId = session.userId || session.studentId || "unknown";
|
|
24552
|
+
if (!data.studentId || !data.error) {
|
|
24553
|
+
return res.json({ success: true });
|
|
24554
|
+
}
|
|
24555
|
+
const isTeacherOrAdmin = session.role === "teacher" || session.role === "administrator";
|
|
24556
|
+
if (!isTeacherOrAdmin && actorId !== data.studentId) {
|
|
24557
|
+
console.warn(`[Diagnostics Security] ${actorId} attempted to report error as ${data.studentId}`);
|
|
24558
|
+
return res.status(403).json({ success: false, error: "Forbidden: studentId mismatch" });
|
|
24559
|
+
}
|
|
24560
|
+
const now = Date.now();
|
|
24561
|
+
const last = diagnosticLastReportAt.get(actorId) || 0;
|
|
24562
|
+
if (now - last < DIAGNOSTIC_MIN_INTERVAL_MS) {
|
|
24563
|
+
return res.status(429).json({ success: false, error: "Too many diagnostic reports" });
|
|
24564
|
+
}
|
|
24565
|
+
diagnosticLastReportAt.set(actorId, now);
|
|
24566
|
+
if (diagnosticLastReportAt.size > 5e3) {
|
|
24567
|
+
for (const [k, v] of diagnosticLastReportAt) {
|
|
24568
|
+
if (now - v > 6e4) diagnosticLastReportAt.delete(k);
|
|
24569
|
+
}
|
|
24570
|
+
}
|
|
24571
|
+
const studentId = String(data.studentId).slice(0, 64);
|
|
24572
|
+
const studentName = isTeacherOrAdmin ? String(data.studentName || studentId).slice(0, 100) : String(session.username || session.studentId || studentId).slice(0, 100);
|
|
24573
|
+
const errorPayload = {
|
|
24574
|
+
type: typeof data.error?.type === "string" ? data.error.type.slice(0, 64) : "runtime",
|
|
24575
|
+
message: typeof data.error?.message === "string" ? data.error.message.slice(0, 2e3) : void 0,
|
|
24576
|
+
title: typeof data.error?.title === "string" ? data.error.title.slice(0, 200) : void 0,
|
|
24577
|
+
timestamp: Number(data.error?.timestamp) || now
|
|
24578
|
+
};
|
|
24579
|
+
console.warn(
|
|
24580
|
+
`[Client Diagnostics HTTP] Student ${studentId} (${studentName}) reported error [${errorPayload.type}]: ${errorPayload.message || errorPayload.title || ""}`
|
|
24581
|
+
);
|
|
24582
|
+
kernelContainer.eventBus.publish({
|
|
24583
|
+
id: `evt_err_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
24584
|
+
type: "student.client_error",
|
|
24585
|
+
source: "student_client",
|
|
24586
|
+
payload: {
|
|
24587
|
+
studentId,
|
|
24588
|
+
studentName,
|
|
24589
|
+
lessonId: data.lessonId || null,
|
|
24590
|
+
classId: data.classId || null,
|
|
24591
|
+
error: errorPayload
|
|
24592
|
+
},
|
|
24593
|
+
timestamp: errorPayload.timestamp,
|
|
24594
|
+
correlationId: data.lessonId || void 0
|
|
24595
|
+
});
|
|
24596
|
+
if (ctx.io) {
|
|
24597
|
+
ctx.io.emit("student-error-alert", {
|
|
24598
|
+
studentId,
|
|
24599
|
+
studentName,
|
|
24600
|
+
lessonId: data.lessonId || null,
|
|
24601
|
+
classId: data.classId || null,
|
|
24602
|
+
error: errorPayload
|
|
24603
|
+
});
|
|
24604
|
+
}
|
|
24605
|
+
res.json({ success: true });
|
|
24606
|
+
} catch (e) {
|
|
24607
|
+
sendSafeError(res, e);
|
|
24608
|
+
}
|
|
24609
|
+
});
|
|
22679
24610
|
app.get("/api/mfe/remotes", (req, res) => {
|
|
22680
24611
|
try {
|
|
22681
24612
|
const name = req.query.name;
|
|
@@ -23062,7 +24993,7 @@ var import_crypto9 = __toESM(require("crypto"), 1);
|
|
|
23062
24993
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
23063
24994
|
function registerRosterRoutes(ctx) {
|
|
23064
24995
|
const { app, io, loginLimiter } = ctx;
|
|
23065
|
-
app.get("/api/classes", (req, res) => {
|
|
24996
|
+
app.get("/api/classes", requireAuth(), (req, res) => {
|
|
23066
24997
|
try {
|
|
23067
24998
|
const classes = kernelContainer.db.prepare(
|
|
23068
24999
|
`
|
|
@@ -23076,22 +25007,25 @@ function registerRosterRoutes(ctx) {
|
|
|
23076
25007
|
).all();
|
|
23077
25008
|
res.json(classes);
|
|
23078
25009
|
} catch (e) {
|
|
23079
|
-
res
|
|
25010
|
+
sendSafeError(res, e);
|
|
23080
25011
|
}
|
|
23081
25012
|
});
|
|
23082
|
-
app.get("/api/students", (req, res) => {
|
|
25013
|
+
app.get("/api/students", requireAuth(), (req, res) => {
|
|
23083
25014
|
try {
|
|
23084
|
-
const students = kernelContainer.db.prepare(
|
|
25015
|
+
const students = kernelContainer.db.prepare(
|
|
25016
|
+
"SELECT id, student_number, name, email, avatar, locked_lesson_id, private_notes, created_at FROM students ORDER BY created_at DESC"
|
|
25017
|
+
).all();
|
|
23085
25018
|
res.json(students);
|
|
23086
25019
|
} catch (e) {
|
|
23087
|
-
res
|
|
25020
|
+
sendSafeError(res, e);
|
|
23088
25021
|
}
|
|
23089
25022
|
});
|
|
23090
|
-
app.get("/api/classes/:id/students", (req, res) => {
|
|
25023
|
+
app.get("/api/classes/:id/students", requireAuth(), (req, res) => {
|
|
23091
25024
|
try {
|
|
23092
25025
|
const students = kernelContainer.db.prepare(
|
|
23093
25026
|
`
|
|
23094
|
-
SELECT s
|
|
25027
|
+
SELECT s.id, s.student_number, s.name, s.email, s.avatar, s.locked_lesson_id, s.private_notes, s.created_at, cs.joined_at
|
|
25028
|
+
FROM students s
|
|
23095
25029
|
INNER JOIN class_students cs ON s.id = cs.student_id
|
|
23096
25030
|
WHERE cs.class_id = ?
|
|
23097
25031
|
ORDER BY cs.joined_at DESC
|
|
@@ -23568,7 +25502,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23568
25502
|
sendSafeError(res, e);
|
|
23569
25503
|
}
|
|
23570
25504
|
});
|
|
23571
|
-
app.get("/api/students/:id/read_notifications", (req, res) => {
|
|
25505
|
+
app.get("/api/students/:id/read_notifications", requireAuth(), (req, res) => {
|
|
23572
25506
|
try {
|
|
23573
25507
|
const rows = kernelContainer.db.prepare("SELECT notification_id FROM student_read_notifications WHERE student_id = ?").all(req.params.id);
|
|
23574
25508
|
res.json(rows.map((r) => r.notification_id));
|
|
@@ -23690,7 +25624,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23690
25624
|
res.status(550).json({ error: e.message });
|
|
23691
25625
|
}
|
|
23692
25626
|
});
|
|
23693
|
-
app.get("/api/labs", (req, res) => {
|
|
25627
|
+
app.get("/api/labs", requireAuth(), (req, res) => {
|
|
23694
25628
|
try {
|
|
23695
25629
|
const labs = kernelContainer.db.prepare("SELECT * FROM computer_labs ORDER BY created_at DESC").all();
|
|
23696
25630
|
res.json(labs);
|
|
@@ -23726,7 +25660,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23726
25660
|
sendSafeError(res, e);
|
|
23727
25661
|
}
|
|
23728
25662
|
});
|
|
23729
|
-
app.get("/api/classes/:classId/seats", (req, res) => {
|
|
25663
|
+
app.get("/api/classes/:classId/seats", requireAuth(), (req, res) => {
|
|
23730
25664
|
try {
|
|
23731
25665
|
const classInfo = kernelContainer.db.prepare("SELECT lab_id FROM classes WHERE id = ?").get(req.params.classId);
|
|
23732
25666
|
const labId = classInfo ? classInfo.lab_id : null;
|
|
@@ -23811,11 +25745,8 @@ function registerRosterRoutes(ctx) {
|
|
|
23811
25745
|
sendSafeError(res, e);
|
|
23812
25746
|
}
|
|
23813
25747
|
});
|
|
23814
|
-
app.get("/api/students/:id/export", (req, res) => {
|
|
25748
|
+
app.get("/api/students/:id/export", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23815
25749
|
try {
|
|
23816
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23817
|
-
return res.status(403).json({ error: "Only teachers and administrators can export student data" });
|
|
23818
|
-
}
|
|
23819
25750
|
const studentId = req.params.id;
|
|
23820
25751
|
const student = kernelContainer.db.prepare("SELECT * FROM students WHERE id = ?").get(studentId);
|
|
23821
25752
|
if (!student) return res.status(404).json({ error: "Student not found" });
|
|
@@ -23875,10 +25806,16 @@ function registerRosterRoutes(ctx) {
|
|
|
23875
25806
|
sendSafeError(res, e);
|
|
23876
25807
|
}
|
|
23877
25808
|
});
|
|
23878
|
-
app.get("/api/students/:id/progress", (req, res) => {
|
|
25809
|
+
app.get("/api/students/:id/progress", requireAuth(), (req, res) => {
|
|
23879
25810
|
try {
|
|
25811
|
+
const session = req.session;
|
|
25812
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
25813
|
+
const currentUserId = session?.userId || session?.studentId;
|
|
23880
25814
|
const studentRow = kernelContainer.db.prepare("SELECT id, student_number FROM students WHERE id = ? OR student_number = ?").get(req.params.id, req.params.id);
|
|
23881
25815
|
const studentId = studentRow ? studentRow.id : req.params.id;
|
|
25816
|
+
if (!isPrivileged && currentUserId !== studentId && currentUserId !== (studentRow?.student_number ?? null)) {
|
|
25817
|
+
return res.status(403).json({ error: "Forbidden: Cannot read another student progress" });
|
|
25818
|
+
}
|
|
23882
25819
|
const progress = kernelContainer.db.prepare(
|
|
23883
25820
|
`
|
|
23884
25821
|
SELECT slp.*, l.title as lesson_title
|
|
@@ -23988,7 +25925,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23988
25925
|
sendSafeError(res, e);
|
|
23989
25926
|
}
|
|
23990
25927
|
});
|
|
23991
|
-
app.get("/api/classes/:id/progress", (req, res) => {
|
|
25928
|
+
app.get("/api/classes/:id/progress", requireAuth(), (req, res) => {
|
|
23992
25929
|
try {
|
|
23993
25930
|
const progress = kernelContainer.db.prepare(
|
|
23994
25931
|
`
|
|
@@ -24005,7 +25942,7 @@ function registerRosterRoutes(ctx) {
|
|
|
24005
25942
|
sendSafeError(res, e);
|
|
24006
25943
|
}
|
|
24007
25944
|
});
|
|
24008
|
-
app.get("/api/classes/:classId/lessons/:lessonId/progress", (req, res) => {
|
|
25945
|
+
app.get("/api/classes/:classId/lessons/:lessonId/progress", requireAuth(), (req, res) => {
|
|
24009
25946
|
try {
|
|
24010
25947
|
const progress = kernelContainer.db.prepare(
|
|
24011
25948
|
`
|
|
@@ -24035,7 +25972,7 @@ function registerRosterRoutes(ctx) {
|
|
|
24035
25972
|
sendSafeError(res, e);
|
|
24036
25973
|
}
|
|
24037
25974
|
});
|
|
24038
|
-
app.get("/api/classes/:classId/dashboard", (req, res) => {
|
|
25975
|
+
app.get("/api/classes/:classId/dashboard", requireAuth(), (req, res) => {
|
|
24039
25976
|
try {
|
|
24040
25977
|
const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
|
|
24041
25978
|
const recentSubmissions = kernelContainer.db.prepare(
|
|
@@ -24188,7 +26125,7 @@ function registerRosterRoutes(ctx) {
|
|
|
24188
26125
|
// server/routes/assignments.ts
|
|
24189
26126
|
function registerAssignmentsRoutes(ctx) {
|
|
24190
26127
|
const { app } = ctx;
|
|
24191
|
-
app.get("/api/classes/:classId/assignments", (req, res) => {
|
|
26128
|
+
app.get("/api/classes/:classId/assignments", requireAuth(), (req, res) => {
|
|
24192
26129
|
try {
|
|
24193
26130
|
const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
|
|
24194
26131
|
res.json(assignments);
|
|
@@ -24326,7 +26263,7 @@ Output pure JSON only without markdown code blocks, matching this structure:
|
|
|
24326
26263
|
sendSafeError(res, e);
|
|
24327
26264
|
}
|
|
24328
26265
|
});
|
|
24329
|
-
app.get("/api/assignments/:id/submissions", (req, res) => {
|
|
26266
|
+
app.get("/api/assignments/:id/submissions", requireAuth(), (req, res) => {
|
|
24330
26267
|
try {
|
|
24331
26268
|
const submissions = kernelContainer.db.prepare(
|
|
24332
26269
|
`
|
|
@@ -24441,10 +26378,389 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
24441
26378
|
);
|
|
24442
26379
|
}
|
|
24443
26380
|
|
|
26381
|
+
// server/routes/assignment-hub.ts
|
|
26382
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
26383
|
+
var import_fs13 = __toESM(require("fs"), 1);
|
|
26384
|
+
var import_crypto10 = __toESM(require("crypto"), 1);
|
|
26385
|
+
var import_express2 = __toESM(require("express"), 1);
|
|
26386
|
+
|
|
26387
|
+
// server/utils/upload.ts
|
|
26388
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
26389
|
+
var MAGIC_BYTES = {
|
|
26390
|
+
".pdf": [[37, 80, 68, 70]],
|
|
26391
|
+
".pptx": [[80, 75, 3, 4]],
|
|
26392
|
+
".zip": [[80, 75, 3, 4]],
|
|
26393
|
+
".jpg": [[255, 216, 255]],
|
|
26394
|
+
".jpeg": [[255, 216, 255]],
|
|
26395
|
+
".png": [[137, 80, 78, 71]],
|
|
26396
|
+
".gif": [[71, 73, 70, 56]],
|
|
26397
|
+
".webp": [[82, 73, 70, 70]]
|
|
26398
|
+
};
|
|
26399
|
+
var BLOCKED_EXTENSIONS2 = [".exe", ".sh", ".bat", ".cmd", ".dll", ".so", ".dylib", ".scr", ".msi", ".ps1"];
|
|
26400
|
+
function validateMagicBytes2(buffer, fileName) {
|
|
26401
|
+
const ext = import_path14.default.extname(fileName || "").toLowerCase();
|
|
26402
|
+
const signatures = MAGIC_BYTES[ext];
|
|
26403
|
+
if (!signatures) return true;
|
|
26404
|
+
return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
|
|
26405
|
+
}
|
|
26406
|
+
var SIZE_LIMITS = {
|
|
26407
|
+
courseware: 50 * 1024 * 1024,
|
|
26408
|
+
plugin: 10 * 1024 * 1024,
|
|
26409
|
+
assignment: 20 * 1024 * 1024,
|
|
26410
|
+
generic: 10 * 1024 * 1024
|
|
26411
|
+
};
|
|
26412
|
+
|
|
26413
|
+
// server/routes/assignment-hub.ts
|
|
26414
|
+
var ALLOWED_ASSIGNMENT_EXT = /* @__PURE__ */ new Set([
|
|
26415
|
+
".pdf",
|
|
26416
|
+
".doc",
|
|
26417
|
+
".docx",
|
|
26418
|
+
".ppt",
|
|
26419
|
+
".pptx",
|
|
26420
|
+
".xls",
|
|
26421
|
+
".xlsx",
|
|
26422
|
+
".csv",
|
|
26423
|
+
".txt",
|
|
26424
|
+
".md",
|
|
26425
|
+
".rtf",
|
|
26426
|
+
".odt",
|
|
26427
|
+
".odp",
|
|
26428
|
+
".ods",
|
|
26429
|
+
".png",
|
|
26430
|
+
".jpg",
|
|
26431
|
+
".jpeg",
|
|
26432
|
+
".gif",
|
|
26433
|
+
".webp",
|
|
26434
|
+
".bmp",
|
|
26435
|
+
".svg",
|
|
26436
|
+
".heic",
|
|
26437
|
+
".zip",
|
|
26438
|
+
".py",
|
|
26439
|
+
".js",
|
|
26440
|
+
".mjs",
|
|
26441
|
+
".cjs",
|
|
26442
|
+
".ts",
|
|
26443
|
+
".tsx",
|
|
26444
|
+
".jsx",
|
|
26445
|
+
".java",
|
|
26446
|
+
".c",
|
|
26447
|
+
".h",
|
|
26448
|
+
".cpp",
|
|
26449
|
+
".cs",
|
|
26450
|
+
".go",
|
|
26451
|
+
".rs",
|
|
26452
|
+
".rb",
|
|
26453
|
+
".php",
|
|
26454
|
+
".sql",
|
|
26455
|
+
".json",
|
|
26456
|
+
".xml",
|
|
26457
|
+
".html",
|
|
26458
|
+
".css",
|
|
26459
|
+
".ino"
|
|
26460
|
+
]);
|
|
26461
|
+
var ZIP_CONTAINER_EXT = /* @__PURE__ */ new Set([".zip", ".pptx", ".docx", ".xlsx", ".odt", ".odp", ".ods", ".epub"]);
|
|
26462
|
+
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
26463
|
+
function safeSegment(value) {
|
|
26464
|
+
const cleaned = String(value || "").replace(/[^A-Za-z0-9._-]/g, "");
|
|
26465
|
+
return cleaned.length > 0 ? cleaned.slice(0, 96) : "unknown";
|
|
26466
|
+
}
|
|
26467
|
+
function decodeFileName(raw) {
|
|
26468
|
+
const text = String(raw ?? "").trim();
|
|
26469
|
+
if (!text) return "";
|
|
26470
|
+
try {
|
|
26471
|
+
return decodeURIComponent(text);
|
|
26472
|
+
} catch {
|
|
26473
|
+
return text;
|
|
26474
|
+
}
|
|
26475
|
+
}
|
|
26476
|
+
function assignmentStorageDir(assignmentId, studentId) {
|
|
26477
|
+
return import_path15.default.join(process.cwd(), "storage", "assignments", safeSegment(assignmentId), safeSegment(studentId));
|
|
26478
|
+
}
|
|
26479
|
+
function registerAssignmentHubRoutes(ctx) {
|
|
26480
|
+
const { app } = ctx;
|
|
26481
|
+
const describeRequester = (req) => {
|
|
26482
|
+
const session = req.session;
|
|
26483
|
+
const role = session?.role;
|
|
26484
|
+
const isPrivileged = role === "teacher" || role === "administrator" || role === "admin";
|
|
26485
|
+
const studentId = session?.userId || session?.studentId || null;
|
|
26486
|
+
return { isPrivileged, studentId: studentId ? String(studentId) : null };
|
|
26487
|
+
};
|
|
26488
|
+
app.post("/api/assignments", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
26489
|
+
try {
|
|
26490
|
+
const cmd = kernelContainer.commandBus.createCommand("assignment.create", req.body || {}, getActorId(req));
|
|
26491
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26492
|
+
res.json(result);
|
|
26493
|
+
} catch (e) {
|
|
26494
|
+
sendSafeError(res, e);
|
|
26495
|
+
}
|
|
26496
|
+
});
|
|
26497
|
+
app.get("/api/assignments", requireAuth(), async (req, res) => {
|
|
26498
|
+
try {
|
|
26499
|
+
const payload = {
|
|
26500
|
+
lessonId: req.query.lessonId,
|
|
26501
|
+
classId: req.query.classId,
|
|
26502
|
+
elementId: req.query.elementId,
|
|
26503
|
+
includeArchived: req.query.includeArchived === "1" || req.query.includeArchived === "true"
|
|
26504
|
+
};
|
|
26505
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26506
|
+
const requestedStudent = typeof req.query.studentId === "string" ? req.query.studentId : void 0;
|
|
26507
|
+
if (isPrivileged && requestedStudent) {
|
|
26508
|
+
payload.studentId = requestedStudent;
|
|
26509
|
+
} else if (!isPrivileged && studentId) {
|
|
26510
|
+
payload.studentId = studentId;
|
|
26511
|
+
}
|
|
26512
|
+
const cmd = kernelContainer.commandBus.createCommand("assignment.list", payload, getActorId(req), {
|
|
26513
|
+
silent: true
|
|
26514
|
+
});
|
|
26515
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26516
|
+
res.json(result);
|
|
26517
|
+
} catch (e) {
|
|
26518
|
+
sendSafeError(res, e);
|
|
26519
|
+
}
|
|
26520
|
+
});
|
|
26521
|
+
app.get("/api/assignments/:assignmentId", requireAuth(), async (req, res) => {
|
|
26522
|
+
try {
|
|
26523
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26524
|
+
const payload = { assignmentId: req.params.assignmentId };
|
|
26525
|
+
const requestedStudent = typeof req.query.studentId === "string" ? req.query.studentId : void 0;
|
|
26526
|
+
if (isPrivileged && requestedStudent) {
|
|
26527
|
+
payload.studentId = requestedStudent;
|
|
26528
|
+
} else if (!isPrivileged && studentId) {
|
|
26529
|
+
payload.studentId = studentId;
|
|
26530
|
+
}
|
|
26531
|
+
if (isPrivileged) {
|
|
26532
|
+
payload.includePeerProgress = true;
|
|
26533
|
+
}
|
|
26534
|
+
const cmd = kernelContainer.commandBus.createCommand("assignment.get", payload, getActorId(req), { silent: true });
|
|
26535
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26536
|
+
res.json(result);
|
|
26537
|
+
} catch (e) {
|
|
26538
|
+
sendSafeError(res, e);
|
|
26539
|
+
}
|
|
26540
|
+
});
|
|
26541
|
+
app.post(
|
|
26542
|
+
"/api/assignments/:assignmentId/files",
|
|
26543
|
+
requireAuth(),
|
|
26544
|
+
import_express2.default.raw({ type: () => true, limit: MAX_FILE_SIZE + 1024 * 1024 }),
|
|
26545
|
+
async (req, res) => {
|
|
26546
|
+
try {
|
|
26547
|
+
const assignmentId = req.params.assignmentId;
|
|
26548
|
+
const { isPrivileged, studentId: sessionStudentId } = describeRequester(req);
|
|
26549
|
+
const headerStudent = decodeFileName(req.header("x-student-id") || "");
|
|
26550
|
+
const ownerStudentId = isPrivileged ? headerStudent || sessionStudentId : sessionStudentId;
|
|
26551
|
+
if (!ownerStudentId) {
|
|
26552
|
+
return res.status(400).json({ success: false, error: "Missing studentId" });
|
|
26553
|
+
}
|
|
26554
|
+
const assignment = kernelContainer.db.prepare("SELECT id, max_files, max_file_size, status FROM plugin_assignments WHERE id = ?").get(assignmentId);
|
|
26555
|
+
if (!assignment) {
|
|
26556
|
+
return res.status(404).json({ success: false, error: `Assignment not found: ${assignmentId}` });
|
|
26557
|
+
}
|
|
26558
|
+
const fileName = decodeFileName(req.header("x-file-name") || "");
|
|
26559
|
+
if (!fileName) {
|
|
26560
|
+
return res.status(400).json({ success: false, error: "Missing X-File-Name header" });
|
|
26561
|
+
}
|
|
26562
|
+
const ext = import_path15.default.extname(fileName).toLowerCase();
|
|
26563
|
+
if (!ALLOWED_ASSIGNMENT_EXT.has(ext)) {
|
|
26564
|
+
return res.status(400).json({ success: false, error: `File type not allowed: ${ext || "(none)"}` });
|
|
26565
|
+
}
|
|
26566
|
+
if (BLOCKED_EXTENSIONS2.includes(ext)) {
|
|
26567
|
+
return res.status(400).json({ success: false, error: `File type blocked: ${ext}` });
|
|
26568
|
+
}
|
|
26569
|
+
const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
|
|
26570
|
+
if (buffer.length === 0) {
|
|
26571
|
+
return res.status(400).json({ success: false, error: "Empty file body" });
|
|
26572
|
+
}
|
|
26573
|
+
const sizeLimit = Math.min(Number(assignment.max_file_size) || SIZE_LIMITS.assignment, MAX_FILE_SIZE);
|
|
26574
|
+
if (buffer.length > sizeLimit) {
|
|
26575
|
+
return res.status(413).json({ success: false, error: `File too large (limit ${sizeLimit} bytes)` });
|
|
26576
|
+
}
|
|
26577
|
+
if (ZIP_CONTAINER_EXT.has(ext) && !(buffer[0] === 80 && buffer[1] === 75)) {
|
|
26578
|
+
return res.status(400).json({ success: false, error: "File content does not match its extension" });
|
|
26579
|
+
}
|
|
26580
|
+
if (!validateMagicBytes2(buffer, fileName)) {
|
|
26581
|
+
return res.status(400).json({ success: false, error: "File content does not match its extension" });
|
|
26582
|
+
}
|
|
26583
|
+
const existingCount = kernelContainer.db.prepare(
|
|
26584
|
+
"SELECT COUNT(*) AS c FROM plugin_assignment_files WHERE assignment_id = ? AND student_id = ? AND deleted_at IS NULL"
|
|
26585
|
+
).get(assignmentId, ownerStudentId).c;
|
|
26586
|
+
if (existingCount >= (Number(assignment.max_files) || 10)) {
|
|
26587
|
+
return res.status(409).json({ success: false, error: `At most ${assignment.max_files} files are allowed` });
|
|
26588
|
+
}
|
|
26589
|
+
const dir = assignmentStorageDir(assignmentId, ownerStudentId);
|
|
26590
|
+
import_fs13.default.mkdirSync(dir, { recursive: true });
|
|
26591
|
+
const storedName = `${import_crypto10.default.randomUUID()}${ext}`;
|
|
26592
|
+
const storedPath = import_path15.default.join(dir, storedName);
|
|
26593
|
+
import_fs13.default.writeFileSync(storedPath, buffer);
|
|
26594
|
+
const fileId = "af-" + import_crypto10.default.randomUUID();
|
|
26595
|
+
const sha256 = import_crypto10.default.createHash("sha256").update(buffer).digest("hex");
|
|
26596
|
+
kernelContainer.db.prepare(
|
|
26597
|
+
`INSERT INTO plugin_assignment_files
|
|
26598
|
+
(id, assignment_id, submission_id, version_id, student_id, original_name, stored_path, size, mime, sha256, uploaded_at)
|
|
26599
|
+
VALUES (?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)`
|
|
26600
|
+
).run(
|
|
26601
|
+
fileId,
|
|
26602
|
+
assignmentId,
|
|
26603
|
+
ownerStudentId,
|
|
26604
|
+
fileName,
|
|
26605
|
+
import_path15.default.relative(process.cwd(), storedPath),
|
|
26606
|
+
buffer.length,
|
|
26607
|
+
req.header("content-type") || null,
|
|
26608
|
+
sha256,
|
|
26609
|
+
Date.now()
|
|
26610
|
+
);
|
|
26611
|
+
res.json({
|
|
26612
|
+
success: true,
|
|
26613
|
+
file: { id: fileId, name: fileName, size: buffer.length, sha256, studentId: ownerStudentId }
|
|
26614
|
+
});
|
|
26615
|
+
} catch (e) {
|
|
26616
|
+
sendSafeError(res, e);
|
|
26617
|
+
}
|
|
26618
|
+
}
|
|
26619
|
+
);
|
|
26620
|
+
app.get("/api/assignments/:assignmentId/files", requireAuth(), async (req, res) => {
|
|
26621
|
+
try {
|
|
26622
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26623
|
+
const rows = kernelContainer.db.prepare(
|
|
26624
|
+
`SELECT id, student_id, original_name, size, mime, sha256, uploaded_at, version_id
|
|
26625
|
+
FROM plugin_assignment_files
|
|
26626
|
+
WHERE assignment_id = ? AND deleted_at IS NULL
|
|
26627
|
+
ORDER BY uploaded_at DESC`
|
|
26628
|
+
).all(req.params.assignmentId);
|
|
26629
|
+
const files = isPrivileged ? rows : rows.filter((r) => r.student_id === studentId);
|
|
26630
|
+
res.json({ success: true, files });
|
|
26631
|
+
} catch (e) {
|
|
26632
|
+
sendSafeError(res, e);
|
|
26633
|
+
}
|
|
26634
|
+
});
|
|
26635
|
+
app.get("/api/assignments/:assignmentId/files/:fileId", requireAuth(), async (req, res) => {
|
|
26636
|
+
try {
|
|
26637
|
+
const file = kernelContainer.db.prepare(
|
|
26638
|
+
`SELECT id, student_id, original_name, stored_path, mime, deleted_at
|
|
26639
|
+
FROM plugin_assignment_files WHERE id = ? AND assignment_id = ?`
|
|
26640
|
+
).get(req.params.fileId, req.params.assignmentId);
|
|
26641
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26642
|
+
if (!file || file.deleted_at) {
|
|
26643
|
+
return res.status(404).json({ success: false, error: "File not found" });
|
|
26644
|
+
}
|
|
26645
|
+
if (!isPrivileged && file.student_id !== studentId) {
|
|
26646
|
+
const assigned = kernelContainer.db.prepare(
|
|
26647
|
+
`SELECT 1 AS ok FROM plugin_assignment_files f
|
|
26648
|
+
JOIN plugin_submission_versions v ON v.id = f.version_id
|
|
26649
|
+
JOIN plugin_peer_review_tasks t ON t.submission_id = v.submission_id
|
|
26650
|
+
WHERE f.id = ? AND t.reviewer_id = ? LIMIT 1`
|
|
26651
|
+
).get(req.params.fileId, studentId);
|
|
26652
|
+
if (!assigned) {
|
|
26653
|
+
return res.status(403).json({ success: false, error: "Forbidden: Cannot read another student file" });
|
|
26654
|
+
}
|
|
26655
|
+
}
|
|
26656
|
+
const absPath = import_path15.default.resolve(process.cwd(), file.stored_path);
|
|
26657
|
+
const root = import_path15.default.join(process.cwd(), "storage", "assignments");
|
|
26658
|
+
if (!absPath.startsWith(root) || !import_fs13.default.existsSync(absPath)) {
|
|
26659
|
+
return res.status(404).json({ success: false, error: "File not found on disk" });
|
|
26660
|
+
}
|
|
26661
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
26662
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
26663
|
+
res.download(absPath, file.original_name);
|
|
26664
|
+
} catch (e) {
|
|
26665
|
+
sendSafeError(res, e);
|
|
26666
|
+
}
|
|
26667
|
+
});
|
|
26668
|
+
app.delete("/api/assignments/:assignmentId/files/:fileId", requireAuth(), async (req, res) => {
|
|
26669
|
+
try {
|
|
26670
|
+
const file = kernelContainer.db.prepare(
|
|
26671
|
+
`SELECT id, student_id, version_id, deleted_at FROM plugin_assignment_files
|
|
26672
|
+
WHERE id = ? AND assignment_id = ?`
|
|
26673
|
+
).get(req.params.fileId, req.params.assignmentId);
|
|
26674
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26675
|
+
if (!file || file.deleted_at) {
|
|
26676
|
+
return res.status(404).json({ success: false, error: "File not found" });
|
|
26677
|
+
}
|
|
26678
|
+
if (!isPrivileged && file.student_id !== studentId) {
|
|
26679
|
+
return res.status(403).json({ success: false, error: "Forbidden: Cannot delete another student file" });
|
|
26680
|
+
}
|
|
26681
|
+
if (file.version_id) {
|
|
26682
|
+
return res.status(409).json({ success: false, error: "File already attached to a submission version" });
|
|
26683
|
+
}
|
|
26684
|
+
kernelContainer.db.prepare("UPDATE plugin_assignment_files SET deleted_at = ? WHERE id = ?").run(Date.now(), req.params.fileId);
|
|
26685
|
+
res.json({ success: true });
|
|
26686
|
+
} catch (e) {
|
|
26687
|
+
sendSafeError(res, e);
|
|
26688
|
+
}
|
|
26689
|
+
});
|
|
26690
|
+
app.post("/api/assignments/:assignmentId/submit", requireAuth(), async (req, res) => {
|
|
26691
|
+
try {
|
|
26692
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26693
|
+
const body = req.body || {};
|
|
26694
|
+
const targetStudentId = isPrivileged ? String(body.studentId || "") : String(studentId || "");
|
|
26695
|
+
if (!targetStudentId) {
|
|
26696
|
+
return res.status(400).json({ success: false, error: "Missing studentId" });
|
|
26697
|
+
}
|
|
26698
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
26699
|
+
"assignment.submit",
|
|
26700
|
+
{
|
|
26701
|
+
assignmentId: req.params.assignmentId,
|
|
26702
|
+
studentId: targetStudentId,
|
|
26703
|
+
fileIds: Array.isArray(body.fileIds) ? body.fileIds : [],
|
|
26704
|
+
textContent: body.textContent,
|
|
26705
|
+
linkUrl: body.linkUrl
|
|
26706
|
+
},
|
|
26707
|
+
getActorId(req)
|
|
26708
|
+
);
|
|
26709
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26710
|
+
res.json(result);
|
|
26711
|
+
} catch (e) {
|
|
26712
|
+
sendSafeError(res, e);
|
|
26713
|
+
}
|
|
26714
|
+
});
|
|
26715
|
+
app.post("/api/assignments/:assignmentId/peer-review", requireAuth(), async (req, res) => {
|
|
26716
|
+
try {
|
|
26717
|
+
const { isPrivileged, studentId } = describeRequester(req);
|
|
26718
|
+
const body = req.body || {};
|
|
26719
|
+
const reviewerId = isPrivileged ? String(body.reviewerId || body.studentId || "") : String(studentId || "");
|
|
26720
|
+
if (!reviewerId) {
|
|
26721
|
+
return res.status(400).json({ success: false, error: "Missing reviewerId" });
|
|
26722
|
+
}
|
|
26723
|
+
const submissionId = String(body.submissionId || "");
|
|
26724
|
+
if (!submissionId) {
|
|
26725
|
+
return res.status(400).json({ success: false, error: "Missing submissionId" });
|
|
26726
|
+
}
|
|
26727
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
26728
|
+
"assignment.peer_review",
|
|
26729
|
+
{
|
|
26730
|
+
submissionId,
|
|
26731
|
+
reviewerId,
|
|
26732
|
+
score: body.score,
|
|
26733
|
+
comment: body.comment,
|
|
26734
|
+
taskId: body.taskId
|
|
26735
|
+
},
|
|
26736
|
+
getActorId(req)
|
|
26737
|
+
);
|
|
26738
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26739
|
+
res.json(result);
|
|
26740
|
+
} catch (e) {
|
|
26741
|
+
sendSafeError(res, e);
|
|
26742
|
+
}
|
|
26743
|
+
});
|
|
26744
|
+
app.post("/api/assignments/:assignmentId/assign-peer-reviews", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
26745
|
+
try {
|
|
26746
|
+
const body = req.body || {};
|
|
26747
|
+
const cmd = kernelContainer.commandBus.createCommand(
|
|
26748
|
+
"assignment.assign_peer_reviews",
|
|
26749
|
+
{ assignmentId: req.params.assignmentId, reviewerCount: body.reviewerCount, dueAt: body.dueAt },
|
|
26750
|
+
getActorId(req)
|
|
26751
|
+
);
|
|
26752
|
+
const result = await kernelContainer.commandBus.execute(cmd);
|
|
26753
|
+
res.json(result);
|
|
26754
|
+
} catch (e) {
|
|
26755
|
+
sendSafeError(res, e);
|
|
26756
|
+
}
|
|
26757
|
+
});
|
|
26758
|
+
}
|
|
26759
|
+
|
|
24444
26760
|
// server/routes/schedules.ts
|
|
24445
26761
|
function registerSchedulesRoutes(ctx) {
|
|
24446
26762
|
const { app } = ctx;
|
|
24447
|
-
app.get("/api/schedules/today", (req, res) => {
|
|
26763
|
+
app.get("/api/schedules/today", requireAuth(), (req, res) => {
|
|
24448
26764
|
try {
|
|
24449
26765
|
const clientDate = req.query.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
24450
26766
|
const schedules = kernelContainer.db.prepare(
|
|
@@ -24469,10 +26785,10 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24469
26785
|
).all(clientDate, clientDate);
|
|
24470
26786
|
res.json({ success: true, schedules });
|
|
24471
26787
|
} catch (e) {
|
|
24472
|
-
res
|
|
26788
|
+
sendSafeError(res, e);
|
|
24473
26789
|
}
|
|
24474
26790
|
});
|
|
24475
|
-
app.get("/api/schedules", (req, res) => {
|
|
26791
|
+
app.get("/api/schedules", requireAuth(), (req, res) => {
|
|
24476
26792
|
try {
|
|
24477
26793
|
const schedules = kernelContainer.db.prepare(
|
|
24478
26794
|
`
|
|
@@ -24485,10 +26801,10 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24485
26801
|
).all();
|
|
24486
26802
|
res.json(schedules);
|
|
24487
26803
|
} catch (e) {
|
|
24488
|
-
res
|
|
26804
|
+
sendSafeError(res, e);
|
|
24489
26805
|
}
|
|
24490
26806
|
});
|
|
24491
|
-
app.get("/api/classes/:classId/schedules", (req, res) => {
|
|
26807
|
+
app.get("/api/classes/:classId/schedules", requireAuth(), (req, res) => {
|
|
24492
26808
|
try {
|
|
24493
26809
|
const schedules = kernelContainer.db.prepare(
|
|
24494
26810
|
`
|
|
@@ -24501,7 +26817,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24501
26817
|
).all(req.params.classId);
|
|
24502
26818
|
res.json(schedules);
|
|
24503
26819
|
} catch (e) {
|
|
24504
|
-
res
|
|
26820
|
+
sendSafeError(res, e);
|
|
24505
26821
|
}
|
|
24506
26822
|
});
|
|
24507
26823
|
app.post("/api/classes/:classId/schedules", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24536,7 +26852,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24536
26852
|
}
|
|
24537
26853
|
});
|
|
24538
26854
|
} catch (e) {
|
|
24539
|
-
res
|
|
26855
|
+
sendSafeError(res, e);
|
|
24540
26856
|
}
|
|
24541
26857
|
});
|
|
24542
26858
|
app.put("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24559,7 +26875,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24559
26875
|
);
|
|
24560
26876
|
res.json({ success: true });
|
|
24561
26877
|
} catch (e) {
|
|
24562
|
-
res
|
|
26878
|
+
sendSafeError(res, e);
|
|
24563
26879
|
}
|
|
24564
26880
|
});
|
|
24565
26881
|
app.delete("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24568,7 +26884,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24568
26884
|
kernelContainer.db.prepare("DELETE FROM attendance WHERE schedule_id = ?").run(req.params.scheduleId);
|
|
24569
26885
|
res.json({ success: true });
|
|
24570
26886
|
} catch (e) {
|
|
24571
|
-
res
|
|
26887
|
+
sendSafeError(res, e);
|
|
24572
26888
|
}
|
|
24573
26889
|
});
|
|
24574
26890
|
app.post("/api/classes/:classId/schedules/batch", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24597,7 +26913,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24597
26913
|
transaction(schedules);
|
|
24598
26914
|
res.json({ success: true, count: schedules.length });
|
|
24599
26915
|
} catch (e) {
|
|
24600
|
-
res
|
|
26916
|
+
sendSafeError(res, e);
|
|
24601
26917
|
}
|
|
24602
26918
|
});
|
|
24603
26919
|
app.post("/api/timetable/ocr", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
@@ -24760,7 +27076,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24760
27076
|
stack: e.stack,
|
|
24761
27077
|
cause: e.cause
|
|
24762
27078
|
});
|
|
24763
|
-
res
|
|
27079
|
+
sendSafeError(res, e);
|
|
24764
27080
|
}
|
|
24765
27081
|
});
|
|
24766
27082
|
}
|
|
@@ -24769,7 +27085,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
24769
27085
|
init_interfaces();
|
|
24770
27086
|
function registerGradingRoutes(ctx) {
|
|
24771
27087
|
const { app } = ctx;
|
|
24772
|
-
app.get("/api/classes/:classId/attendance-summary", (req, res) => {
|
|
27088
|
+
app.get("/api/classes/:classId/attendance-summary", requireAuth(), (req, res) => {
|
|
24773
27089
|
try {
|
|
24774
27090
|
const classId = req.params.classId;
|
|
24775
27091
|
const db2 = kernelContainer.db;
|
|
@@ -24880,10 +27196,10 @@ function registerGradingRoutes(ctx) {
|
|
|
24880
27196
|
}).sort((a, b) => a.date.localeCompare(b.date));
|
|
24881
27197
|
res.json(filtered);
|
|
24882
27198
|
} catch (e) {
|
|
24883
|
-
res
|
|
27199
|
+
sendSafeError(res, e);
|
|
24884
27200
|
}
|
|
24885
27201
|
});
|
|
24886
|
-
app.get("/api/schedules/:scheduleId/attendance", (req, res) => {
|
|
27202
|
+
app.get("/api/schedules/:scheduleId/attendance", requireAuth(), (req, res) => {
|
|
24887
27203
|
try {
|
|
24888
27204
|
const attendance = kernelContainer.db.prepare(
|
|
24889
27205
|
`
|
|
@@ -24895,7 +27211,7 @@ function registerGradingRoutes(ctx) {
|
|
|
24895
27211
|
).all(req.params.scheduleId);
|
|
24896
27212
|
res.json(attendance);
|
|
24897
27213
|
} catch (e) {
|
|
24898
|
-
res
|
|
27214
|
+
sendSafeError(res, e);
|
|
24899
27215
|
}
|
|
24900
27216
|
});
|
|
24901
27217
|
app.post("/api/schedules/:scheduleId/attendance", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24910,10 +27226,10 @@ function registerGradingRoutes(ctx) {
|
|
|
24910
27226
|
).run(req.params.scheduleId, studentId, status, Date.now());
|
|
24911
27227
|
res.json({ success: true });
|
|
24912
27228
|
} catch (e) {
|
|
24913
|
-
res
|
|
27229
|
+
sendSafeError(res, e);
|
|
24914
27230
|
}
|
|
24915
27231
|
});
|
|
24916
|
-
app.get("/api/classes/:classId/grade-weights", (req, res) => {
|
|
27232
|
+
app.get("/api/classes/:classId/grade-weights", requireAuth(), (req, res) => {
|
|
24917
27233
|
try {
|
|
24918
27234
|
const weights = kernelContainer.db.prepare("SELECT * FROM class_grade_weights WHERE class_id = ?").get(req.params.classId);
|
|
24919
27235
|
if (!weights) {
|
|
@@ -24927,7 +27243,7 @@ function registerGradingRoutes(ctx) {
|
|
|
24927
27243
|
}
|
|
24928
27244
|
res.json(weights);
|
|
24929
27245
|
} catch (e) {
|
|
24930
|
-
res
|
|
27246
|
+
sendSafeError(res, e);
|
|
24931
27247
|
}
|
|
24932
27248
|
});
|
|
24933
27249
|
app.post("/api/classes/:classId/grade-weights", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24955,15 +27271,15 @@ function registerGradingRoutes(ctx) {
|
|
|
24955
27271
|
).run(req.params.classId, att, prog, assign, ex, Date.now());
|
|
24956
27272
|
res.json({ success: true });
|
|
24957
27273
|
} catch (e) {
|
|
24958
|
-
res
|
|
27274
|
+
sendSafeError(res, e);
|
|
24959
27275
|
}
|
|
24960
27276
|
});
|
|
24961
|
-
app.get("/api/classes/:classId/exams", (req, res) => {
|
|
27277
|
+
app.get("/api/classes/:classId/exams", requireAuth(), (req, res) => {
|
|
24962
27278
|
try {
|
|
24963
27279
|
const exams = kernelContainer.db.prepare("SELECT * FROM exams WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
|
|
24964
27280
|
res.json(exams);
|
|
24965
27281
|
} catch (e) {
|
|
24966
|
-
res
|
|
27282
|
+
sendSafeError(res, e);
|
|
24967
27283
|
}
|
|
24968
27284
|
});
|
|
24969
27285
|
app.post("/api/classes/:classId/exams", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -24979,15 +27295,15 @@ function registerGradingRoutes(ctx) {
|
|
|
24979
27295
|
).run(examId, req.params.classId, title, description || "", max_score || 100, Date.now());
|
|
24980
27296
|
res.json({ success: true, examId });
|
|
24981
27297
|
} catch (e) {
|
|
24982
|
-
res
|
|
27298
|
+
sendSafeError(res, e);
|
|
24983
27299
|
}
|
|
24984
27300
|
});
|
|
24985
|
-
app.get("/api/exams/:examId/scores", (req, res) => {
|
|
27301
|
+
app.get("/api/exams/:examId/scores", requireAuth(), (req, res) => {
|
|
24986
27302
|
try {
|
|
24987
27303
|
const scores = kernelContainer.db.prepare("SELECT * FROM exam_scores WHERE exam_id = ?").all(req.params.examId);
|
|
24988
27304
|
res.json(scores);
|
|
24989
27305
|
} catch (e) {
|
|
24990
|
-
res
|
|
27306
|
+
sendSafeError(res, e);
|
|
24991
27307
|
}
|
|
24992
27308
|
});
|
|
24993
27309
|
app.post("/api/exams/:examId/scores", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -25016,10 +27332,10 @@ function registerGradingRoutes(ctx) {
|
|
|
25016
27332
|
transaction(scores);
|
|
25017
27333
|
res.json({ success: true });
|
|
25018
27334
|
} catch (e) {
|
|
25019
|
-
res
|
|
27335
|
+
sendSafeError(res, e);
|
|
25020
27336
|
}
|
|
25021
27337
|
});
|
|
25022
|
-
app.get("/api/classes/:classId/semester-grades", (req, res) => {
|
|
27338
|
+
app.get("/api/classes/:classId/semester-grades", requireAuth(), (req, res) => {
|
|
25023
27339
|
try {
|
|
25024
27340
|
const classId = req.params.classId;
|
|
25025
27341
|
const semesterName = req.query.semesterName || "2026\u5E74\u6625\u5B63\u5B66\uFFFD?";
|
|
@@ -25161,7 +27477,7 @@ function registerGradingRoutes(ctx) {
|
|
|
25161
27477
|
});
|
|
25162
27478
|
res.json({ success: true, weights, students: result });
|
|
25163
27479
|
} catch (e) {
|
|
25164
|
-
res
|
|
27480
|
+
sendSafeError(res, e);
|
|
25165
27481
|
}
|
|
25166
27482
|
});
|
|
25167
27483
|
app.post("/api/grade-sync", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
@@ -25174,16 +27490,16 @@ function registerGradingRoutes(ctx) {
|
|
|
25174
27490
|
await gradeService.saveSemesterGrade(lessonId, studentId, Math.round(Number(grade)));
|
|
25175
27491
|
res.json({ success: true });
|
|
25176
27492
|
} catch (e) {
|
|
25177
|
-
res
|
|
27493
|
+
sendSafeError(res, e);
|
|
25178
27494
|
}
|
|
25179
27495
|
});
|
|
25180
|
-
app.get("/api/classes/:classId/points-dimensions", async (req, res) => {
|
|
27496
|
+
app.get("/api/classes/:classId/points-dimensions", requireAuth(), async (req, res) => {
|
|
25181
27497
|
try {
|
|
25182
27498
|
const dimensionRegistry = await kernelContainer.serviceRegistry.resolve(IPointsDimensionRegistryToken);
|
|
25183
27499
|
const dimensions = dimensionRegistry.listDimensions();
|
|
25184
27500
|
res.json({ success: true, dimensions });
|
|
25185
27501
|
} catch (e) {
|
|
25186
|
-
res
|
|
27502
|
+
sendSafeError(res, e);
|
|
25187
27503
|
}
|
|
25188
27504
|
});
|
|
25189
27505
|
app.post("/api/students/:studentId/points", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
@@ -25211,10 +27527,10 @@ function registerGradingRoutes(ctx) {
|
|
|
25211
27527
|
});
|
|
25212
27528
|
res.json({ success: true, logItem });
|
|
25213
27529
|
} catch (e) {
|
|
25214
|
-
res
|
|
27530
|
+
sendSafeError(res, e);
|
|
25215
27531
|
}
|
|
25216
27532
|
});
|
|
25217
|
-
app.get("/api/students/:studentId/points-logs", async (req, res) => {
|
|
27533
|
+
app.get("/api/students/:studentId/points-logs", requireAuth(), async (req, res) => {
|
|
25218
27534
|
try {
|
|
25219
27535
|
const { studentId } = req.params;
|
|
25220
27536
|
const { classId } = req.query;
|
|
@@ -25223,7 +27539,7 @@ function registerGradingRoutes(ctx) {
|
|
|
25223
27539
|
const summary = await ledgerService.getStudentDimensionSummary(studentId, classId || "");
|
|
25224
27540
|
res.json({ success: true, logs, summary });
|
|
25225
27541
|
} catch (e) {
|
|
25226
|
-
res
|
|
27542
|
+
sendSafeError(res, e);
|
|
25227
27543
|
}
|
|
25228
27544
|
});
|
|
25229
27545
|
app.post("/api/classes/:classId/semester-reports/archive", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -25276,7 +27592,7 @@ function registerGradingRoutes(ctx) {
|
|
|
25276
27592
|
transaction(reports);
|
|
25277
27593
|
res.json({ success: true });
|
|
25278
27594
|
} catch (e) {
|
|
25279
|
-
res
|
|
27595
|
+
sendSafeError(res, e);
|
|
25280
27596
|
}
|
|
25281
27597
|
});
|
|
25282
27598
|
app.post(
|
|
@@ -25377,16 +27693,16 @@ ${examsText}
|
|
|
25377
27693
|
res.json({ success: true, aiEvaluation: text });
|
|
25378
27694
|
} catch (e) {
|
|
25379
27695
|
console.error("AI Semester Evaluation error:", e);
|
|
25380
|
-
res
|
|
27696
|
+
sendSafeError(res, e);
|
|
25381
27697
|
}
|
|
25382
27698
|
}
|
|
25383
27699
|
);
|
|
25384
27700
|
}
|
|
25385
27701
|
|
|
25386
27702
|
// server/routes/plugins.ts
|
|
25387
|
-
var
|
|
25388
|
-
var
|
|
25389
|
-
var
|
|
27703
|
+
var import_express3 = __toESM(require("express"), 1);
|
|
27704
|
+
var import_path16 = __toESM(require("path"), 1);
|
|
27705
|
+
var import_fs14 = __toESM(require("fs"), 1);
|
|
25390
27706
|
var import_semver3 = __toESM(require("semver"), 1);
|
|
25391
27707
|
|
|
25392
27708
|
// server/services/version-fetcher.ts
|
|
@@ -25407,6 +27723,14 @@ function cached(key) {
|
|
|
25407
27723
|
function setCache(key, result) {
|
|
25408
27724
|
cache.set(key, { result, timestamp: Date.now() });
|
|
25409
27725
|
}
|
|
27726
|
+
var REPO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27727
|
+
function normalizeSource(source) {
|
|
27728
|
+
if (!source) return null;
|
|
27729
|
+
if (source.type !== "github-release" && source.type !== "gitee-release") return null;
|
|
27730
|
+
if (typeof source.repo !== "string" || source.repo.length > 140) return null;
|
|
27731
|
+
if (!REPO_PATTERN.test(source.repo)) return null;
|
|
27732
|
+
return { type: source.type, repo: source.repo };
|
|
27733
|
+
}
|
|
25410
27734
|
function repoUrl(source) {
|
|
25411
27735
|
if (source.type === "github-release") {
|
|
25412
27736
|
return `https://github.com/${source.repo}.git`;
|
|
@@ -25483,9 +27807,6 @@ function latestFromReleases(releases, installed, source) {
|
|
|
25483
27807
|
return { version: null, isPrerelease: false, downloadUrl: null, changelog: null };
|
|
25484
27808
|
}
|
|
25485
27809
|
async function checkVersion(source, installedVersion) {
|
|
25486
|
-
const key = cacheKey(source);
|
|
25487
|
-
const hit = cached(key);
|
|
25488
|
-
if (hit) return { ...hit, installedVersion };
|
|
25489
27810
|
const base = {
|
|
25490
27811
|
hasUpdate: false,
|
|
25491
27812
|
installedVersion,
|
|
@@ -25494,9 +27815,16 @@ async function checkVersion(source, installedVersion) {
|
|
|
25494
27815
|
downloadUrl: null,
|
|
25495
27816
|
changelog: null
|
|
25496
27817
|
};
|
|
27818
|
+
const safeSource = normalizeSource(source);
|
|
27819
|
+
if (!safeSource) {
|
|
27820
|
+
return { ...base, error: "\u65E0\u6548\u7684\u66F4\u65B0\u6E90\uFF1A\u4EC5\u652F\u6301 owner/name \u5F62\u5F0F\u7684 GitHub / Gitee \u4ED3\u5E93" };
|
|
27821
|
+
}
|
|
27822
|
+
const key = cacheKey(safeSource);
|
|
27823
|
+
const hit = cached(key);
|
|
27824
|
+
if (hit) return { ...hit, installedVersion };
|
|
25497
27825
|
try {
|
|
25498
|
-
const url = repoUrl(
|
|
25499
|
-
const stdout = (0, import_child_process2.
|
|
27826
|
+
const url = repoUrl(safeSource);
|
|
27827
|
+
const stdout = (0, import_child_process2.execFileSync)("git", ["ls-remote", "--tags", url], {
|
|
25500
27828
|
timeout: 8e3,
|
|
25501
27829
|
encoding: "utf-8",
|
|
25502
27830
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -25517,7 +27845,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
25517
27845
|
} catch {
|
|
25518
27846
|
}
|
|
25519
27847
|
try {
|
|
25520
|
-
const apiUrl = apiReleasesUrl(
|
|
27848
|
+
const apiUrl = apiReleasesUrl(safeSource);
|
|
25521
27849
|
const resp = await fetch(apiUrl, {
|
|
25522
27850
|
headers: { "User-Agent": "OpenLearnV2-PluginUpdater/1.0" },
|
|
25523
27851
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -25526,7 +27854,11 @@ async function checkVersion(source, installedVersion) {
|
|
|
25526
27854
|
return { ...base, error: `\u66F4\u65B0\u6E90\u54CD\u5E94\u5F02\u5E38: HTTP ${resp.status}` };
|
|
25527
27855
|
}
|
|
25528
27856
|
const releases = await resp.json();
|
|
25529
|
-
const { version, isPrerelease, downloadUrl, changelog } = latestFromReleases(
|
|
27857
|
+
const { version, isPrerelease, downloadUrl, changelog } = latestFromReleases(
|
|
27858
|
+
releases,
|
|
27859
|
+
installedVersion,
|
|
27860
|
+
safeSource
|
|
27861
|
+
);
|
|
25530
27862
|
const result = {
|
|
25531
27863
|
...base,
|
|
25532
27864
|
hasUpdate: version !== null,
|
|
@@ -25898,16 +28230,16 @@ data: ${JSON.stringify({ error: msg })}
|
|
|
25898
28230
|
|
|
25899
28231
|
// server/routes/plugins.ts
|
|
25900
28232
|
function findLocalPluginSource(manifestId) {
|
|
25901
|
-
const root =
|
|
25902
|
-
if (!
|
|
28233
|
+
const root = import_path16.default.resolve(process.cwd(), "v2_plugins");
|
|
28234
|
+
if (!import_fs14.default.existsSync(root)) return null;
|
|
25903
28235
|
let best = null;
|
|
25904
28236
|
try {
|
|
25905
|
-
for (const entry of
|
|
28237
|
+
for (const entry of import_fs14.default.readdirSync(root, { withFileTypes: true })) {
|
|
25906
28238
|
if (!entry.isDirectory()) continue;
|
|
25907
|
-
const manifestPath =
|
|
25908
|
-
if (!
|
|
28239
|
+
const manifestPath = import_path16.default.join(root, entry.name, "manifest.json");
|
|
28240
|
+
if (!import_fs14.default.existsSync(manifestPath)) continue;
|
|
25909
28241
|
try {
|
|
25910
|
-
const m = JSON.parse(
|
|
28242
|
+
const m = JSON.parse(import_fs14.default.readFileSync(manifestPath, "utf-8"));
|
|
25911
28243
|
if (m?.id !== manifestId || !m?.version) continue;
|
|
25912
28244
|
if (!best || import_semver3.default.gt(m.version, best.version)) best = { dir: entry.name, version: m.version };
|
|
25913
28245
|
} catch {
|
|
@@ -25943,11 +28275,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25943
28275
|
const { app, buildOpenAITools: buildOpenAITools2, executeAgentToolCall: executeAgentToolCall2, buildOpenAIChatUrl: buildOpenAIChatUrl2, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2 } = ctx;
|
|
25944
28276
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
25945
28277
|
try {
|
|
25946
|
-
const docPath =
|
|
25947
|
-
if (!
|
|
28278
|
+
const docPath = import_path16.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
28279
|
+
if (!import_fs14.default.existsSync(docPath)) {
|
|
25948
28280
|
return res.status(404).json({ success: false, error: "Document not found" });
|
|
25949
28281
|
}
|
|
25950
|
-
const content =
|
|
28282
|
+
const content = import_fs14.default.readFileSync(docPath, "utf-8");
|
|
25951
28283
|
res.json({ success: true, content });
|
|
25952
28284
|
} catch (e) {
|
|
25953
28285
|
sendSafeError(res, e);
|
|
@@ -25958,11 +28290,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25958
28290
|
const limit = parseInt(req.query.limit) || 200;
|
|
25959
28291
|
const component = req.query.component;
|
|
25960
28292
|
const levelFilter = req.query.level;
|
|
25961
|
-
const logFile2 =
|
|
25962
|
-
if (!
|
|
28293
|
+
const logFile2 = import_path16.default.resolve(process.cwd(), "logs", "openlearn.log");
|
|
28294
|
+
if (!import_fs14.default.existsSync(logFile2)) {
|
|
25963
28295
|
return res.json({ success: true, logs: [] });
|
|
25964
28296
|
}
|
|
25965
|
-
const content =
|
|
28297
|
+
const content = import_fs14.default.readFileSync(logFile2, "utf-8");
|
|
25966
28298
|
const lines = content.split("\n").filter(Boolean);
|
|
25967
28299
|
let parsedLogs = lines.map((line) => {
|
|
25968
28300
|
try {
|
|
@@ -26023,7 +28355,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
26023
28355
|
sendSafeError(res, e);
|
|
26024
28356
|
}
|
|
26025
28357
|
});
|
|
26026
|
-
app.get("/api/plugins/market", async (req, res) => {
|
|
28358
|
+
app.get("/api/plugins/market", requireAuth(), async (req, res) => {
|
|
26027
28359
|
try {
|
|
26028
28360
|
const plugins = kernelContainer.db.prepare("SELECT id, manifest FROM plugins WHERE manifest LIKE '%updateSource%'").all();
|
|
26029
28361
|
const results = [];
|
|
@@ -26049,7 +28381,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
26049
28381
|
sendSafeError(res, e);
|
|
26050
28382
|
}
|
|
26051
28383
|
});
|
|
26052
|
-
app.post("/api/plugins/:id(*)/check-update", async (req, res) => {
|
|
28384
|
+
app.post("/api/plugins/:id(*)/check-update", requireAuth(), async (req, res) => {
|
|
26053
28385
|
try {
|
|
26054
28386
|
const rawId = decodeURIComponent(req.params.id);
|
|
26055
28387
|
const pluginId = kernelContainer.pluginHost.resolvePluginUuid(rawId);
|
|
@@ -26252,7 +28584,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
26252
28584
|
app.post(
|
|
26253
28585
|
"/api/plugins/upload-zip-raw",
|
|
26254
28586
|
requireAuth("administrator"),
|
|
26255
|
-
|
|
28587
|
+
import_express3.default.raw({ type: "application/octet-stream", limit: "400mb" }),
|
|
26256
28588
|
async (req, res) => {
|
|
26257
28589
|
try {
|
|
26258
28590
|
const zipBuffer = req.body;
|
|
@@ -26299,7 +28631,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
26299
28631
|
app.post(
|
|
26300
28632
|
"/api/plugins/:id(*)/update-zip-raw",
|
|
26301
28633
|
requireAuth("administrator"),
|
|
26302
|
-
|
|
28634
|
+
import_express3.default.raw({ type: "application/octet-stream", limit: "400mb" }),
|
|
26303
28635
|
async (req, res) => {
|
|
26304
28636
|
try {
|
|
26305
28637
|
const targetPluginId = decodeURIComponent(req.params.id);
|
|
@@ -26330,7 +28662,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
26330
28662
|
}
|
|
26331
28663
|
}
|
|
26332
28664
|
);
|
|
26333
|
-
app.post("/api/plugins/execute-command", async (req, res) => {
|
|
28665
|
+
app.post("/api/plugins/execute-command", requireAuth(), async (req, res) => {
|
|
26334
28666
|
try {
|
|
26335
28667
|
const { type, payload } = req.body;
|
|
26336
28668
|
if (!type) {
|
|
@@ -26524,7 +28856,7 @@ async function startServer() {
|
|
|
26524
28856
|
config: { port: Number(process.env.PORT) || 9e3 }
|
|
26525
28857
|
});
|
|
26526
28858
|
try {
|
|
26527
|
-
const migrationsDir =
|
|
28859
|
+
const migrationsDir = import_path17.default.join(process.cwd(), "migrations");
|
|
26528
28860
|
const migrations = loadMigrationsFromDirectory(migrationsDir);
|
|
26529
28861
|
if (migrations.length > 0) {
|
|
26530
28862
|
runMigrations(kernelContainer.db, migrations);
|
|
@@ -26538,7 +28870,7 @@ async function startServer() {
|
|
|
26538
28870
|
registerOfficialActivities(activityRegistry, kernelContainer.actionRegistry);
|
|
26539
28871
|
await kernelContainer.serviceRegistry.register(IActivityRegistryToken, activityRegistry);
|
|
26540
28872
|
console.log(`[ActivityEcosystem] Registered ${activityRegistry.listProviders().length} official activity providers.`);
|
|
26541
|
-
const app = (0,
|
|
28873
|
+
const app = (0, import_express4.default)();
|
|
26542
28874
|
kernelContainer.pluginHost.setExpressApp(app);
|
|
26543
28875
|
const PORT = parseInt(process.env.PORT || "9000", 10);
|
|
26544
28876
|
app.set("trust proxy", 1);
|
|
@@ -26586,10 +28918,25 @@ async function startServer() {
|
|
|
26586
28918
|
standardHeaders: true,
|
|
26587
28919
|
legacyHeaders: false
|
|
26588
28920
|
});
|
|
26589
|
-
app.use(
|
|
26590
|
-
app.use(
|
|
26591
|
-
app.use(
|
|
26592
|
-
|
|
28921
|
+
app.use(import_express4.default.json({ limit: "10mb" }));
|
|
28922
|
+
app.use(import_express4.default.urlencoded({ limit: "10mb", extended: true }));
|
|
28923
|
+
app.use(
|
|
28924
|
+
"/uploads",
|
|
28925
|
+
(req, res, next) => {
|
|
28926
|
+
const token = req.headers.cookie?.match?.(/edu_os_token=([^;]+)/)?.[1];
|
|
28927
|
+
if (!token) {
|
|
28928
|
+
}
|
|
28929
|
+
next();
|
|
28930
|
+
},
|
|
28931
|
+
import_express4.default.static(import_path17.default.join(process.cwd(), "uploads"), {
|
|
28932
|
+
// 禁用目录索引与隐藏文件
|
|
28933
|
+
index: false,
|
|
28934
|
+
dotfiles: "ignore",
|
|
28935
|
+
// 缓存控制:静态资源可缓存 1h,接口不受影响
|
|
28936
|
+
maxAge: "1h"
|
|
28937
|
+
})
|
|
28938
|
+
);
|
|
28939
|
+
app.use("/plugins", import_express4.default.static(import_path17.default.join(process.cwd(), "plugins"), { index: false, dotfiles: "ignore" }));
|
|
26593
28940
|
const configuredOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
26594
28941
|
const isOriginAllowed = (origin, hostHeader) => {
|
|
26595
28942
|
if (!origin) return true;
|
|
@@ -26714,6 +29061,7 @@ async function startServer() {
|
|
|
26714
29061
|
registerAdminRoutes(ctx);
|
|
26715
29062
|
registerRosterRoutes(ctx);
|
|
26716
29063
|
registerAssignmentsRoutes(ctx);
|
|
29064
|
+
registerAssignmentHubRoutes(ctx);
|
|
26717
29065
|
registerSchedulesRoutes(ctx);
|
|
26718
29066
|
registerGradingRoutes(ctx);
|
|
26719
29067
|
registerPluginsRoutes(ctx);
|
|
@@ -26762,10 +29110,10 @@ async function startServer() {
|
|
|
26762
29110
|
});
|
|
26763
29111
|
app.use(vite.middlewares);
|
|
26764
29112
|
} else {
|
|
26765
|
-
const distPath = __dirname ||
|
|
26766
|
-
app.use(
|
|
29113
|
+
const distPath = __dirname || import_path17.default.join(process.cwd(), "dist");
|
|
29114
|
+
app.use(import_express4.default.static(distPath));
|
|
26767
29115
|
app.get("*", (req, res) => {
|
|
26768
|
-
res.sendFile(
|
|
29116
|
+
res.sendFile(import_path17.default.join(distPath, "index.html"));
|
|
26769
29117
|
});
|
|
26770
29118
|
}
|
|
26771
29119
|
const HOST = process.env.HOST || "0.0.0.0";
|