openlearn-next 0.7.8 → 0.7.10
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 +4 -2
- package/cli-data.mjs +1 -1
- package/dist/assets/{ActivityWorkspaceWidget-BXS1IamH.js → ActivityWorkspaceWidget-BU88GGUG.js} +1 -1
- package/dist/assets/{AdminDirectoryView-479SfYyD.js → AdminDirectoryView-Cdsk9Mdr.js} +1 -1
- package/dist/assets/{AssessmentCenter-CJ-pchOQ.js → AssessmentCenter-BoSWbJMt.js} +1 -1
- package/dist/assets/{AssignmentFilePreview-wjnrKjsp.js → AssignmentFilePreview-D_POvLmv.js} +1 -1
- package/dist/assets/{BatchPickerModal-JvoD-1pF.js → BatchPickerModal-DiHd_6Tg.js} +1 -1
- package/dist/assets/{ClassesView-CB8q2kW0.js → ClassesView-CWKqzWH0.js} +1 -1
- package/dist/assets/{ClassroomCountdownWidget-BqzBpAvi.js → ClassroomCountdownWidget-B5-KUVU8.js} +1 -1
- package/dist/assets/{CloudDriveModal-DHxOnL4m.js → CloudDriveModal-CjcvdDiw.js} +1 -1
- package/dist/assets/{ComputerLabView-BEDe7WiU.js → ComputerLabView-CNTcmWAh.js} +1 -1
- package/dist/assets/CourseManagement-ha_iSnLU.js +1 -0
- package/dist/assets/{CourseWizardModal-D3UPFtT_.js → CourseWizardModal-oVT9mYSG.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-CqJqcZGs.js → CoursewareHubPanel-BuKp0mmW.js} +1 -1
- package/dist/assets/{Dashboard-C3BaEHIG.js → Dashboard-Vb9coh9_.js} +1 -1
- package/dist/assets/{ExportWeightModal-iF0pVevG.js → ExportWeightModal-BEesz6rd.js} +1 -1
- package/dist/assets/{HelpTour-6ChRqWnB.js → HelpTour-D3QcGDUz.js} +1 -1
- package/dist/assets/{HelpView-D3iYhzBz.js → HelpView--Um9DFYx.js} +1 -1
- package/dist/assets/ImportLessonsModal-DXGopj4j.js +1 -0
- package/dist/assets/{ImportModal-DAHGdI9h.js → ImportModal-h5ShIMzO.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-DX9xnk6j.js → InteractiveCoursewareViewer-DzgF9_LU.js} +1 -1
- package/dist/assets/{InteractiveWhiteboard-41mi9keH.js → InteractiveWhiteboard-DJqgP_tU.js} +7 -7
- package/dist/assets/{LazyCourseware-DKyUHXaE.js → LazyCourseware-6yb_QiYS.js} +2 -2
- package/dist/assets/{LazyWhiteboard-fXS2P44c.js → LazyWhiteboard-CsF9GB6J.js} +2 -2
- package/dist/assets/LessonEditorView-CZTZ7Q0B.js +1 -0
- package/dist/assets/{LiveClassroomView-m0hKdcVK.js → LiveClassroomView-D2ugupT9.js} +1 -1
- package/dist/assets/{NotificationDetailModal-C2tg_IoJ.js → NotificationDetailModal-mUeIQP4R.js} +1 -1
- package/dist/assets/{PluginView-BPRUpCjV.js → PluginView-CT3-zS2n.js} +1 -1
- package/dist/assets/{ProcessLogsModal-CULHzXp7.js → ProcessLogsModal-DmsOrZnV.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-CYxngpW9.js → QuizGeneratorModal-ViKxNX5Q.js} +1 -1
- package/dist/assets/{StudentAssignmentEvalPanel-Dfe4IOs5.js → StudentAssignmentEvalPanel-BRxHMABY.js} +1 -1
- package/dist/assets/{StudentAssignmentView-IfRo3yJk.js → StudentAssignmentView-g-ewJIS3.js} +1 -1
- package/dist/assets/{StudentLessonView-Dw2uxBhq.js → StudentLessonView-Dbvr3Xlo.js} +1 -1
- package/dist/assets/{StudentView-DVYUSrVU.js → StudentView-20vfEk4v.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-BhbpfIBt.js → SystemResourceLibraryModal-CDKeaKf8.js} +1 -1
- package/dist/assets/{TeacherView-CZjIejet.js → TeacherView-KIKuhXGF.js} +2 -2
- package/dist/assets/{TimetableView-CupOIfEy.js → TimetableView-7V2AwWml.js} +1 -1
- package/dist/assets/{index-BDFyco9S.css → index-C4cHQZyx.css} +1 -1
- package/dist/assets/index-XduPa2-x.js +131 -0
- package/dist/assets/{pluginParsers-C9W6GBVx.js → pluginParsers-BO6U1E-h.js} +1 -1
- package/dist/assets/{student-workspace-CsbA8Cs4.js → student-workspace-DstiW0pM.js} +1 -1
- package/dist/assets/{vendor-icons-DaYNfRAc.js → vendor-icons-DLOPlM4i.js} +1 -1
- package/dist/env-validation.cjs +14 -6
- package/dist/index.html +3 -3
- package/dist/server.cjs +1489 -867
- package/package.json +2 -2
- package/dist/assets/CourseManagement-BjxQC6sq.js +0 -1
- package/dist/assets/ImportLessonsModal-BOwj7ezj.js +0 -1
- package/dist/assets/LessonEditorView-Dx-hOKtt.js +0 -1
- package/dist/assets/index-BiHQqzdm.js +0 -131
package/dist/server.cjs
CHANGED
|
@@ -507,6 +507,74 @@ var init_plugin_metrics = __esm({
|
|
|
507
507
|
}
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
+
// packages/core/capability/catalog.ts
|
|
511
|
+
function validateManifestCapabilities(capabilities) {
|
|
512
|
+
const unknown = [];
|
|
513
|
+
for (const cap of capabilities) {
|
|
514
|
+
if (typeof cap !== "string" || !KNOWN_CAPABILITIES.has(cap)) {
|
|
515
|
+
unknown.push(typeof cap === "string" ? cap : String(cap));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return { valid: unknown.length === 0, unknown };
|
|
519
|
+
}
|
|
520
|
+
var CAP_API_WRITE, CAP_API_ADMIN, CAP_GRADES_WRITE, CAP_AGENT_TOOL, CAP_AI_INVOKE, CAP_TASK_REGISTER, CAP_STUDENT_DATA_READ, CAP_STUDENT_DATA_WRITE, CAP, CAPABILITIES, KNOWN_CAPABILITIES;
|
|
521
|
+
var init_catalog = __esm({
|
|
522
|
+
"packages/core/capability/catalog.ts"() {
|
|
523
|
+
"use strict";
|
|
524
|
+
CAP_API_WRITE = "api:write";
|
|
525
|
+
CAP_API_ADMIN = "api:admin";
|
|
526
|
+
CAP_GRADES_WRITE = "grades:write";
|
|
527
|
+
CAP_AGENT_TOOL = "agent:tool";
|
|
528
|
+
CAP_AI_INVOKE = "ai:invoke";
|
|
529
|
+
CAP_TASK_REGISTER = "task:register";
|
|
530
|
+
CAP_STUDENT_DATA_READ = "student-data:read";
|
|
531
|
+
CAP_STUDENT_DATA_WRITE = "student-data:write";
|
|
532
|
+
CAP = {
|
|
533
|
+
// ── 资源型(ActionDescriptor.capabilityRequired)──────────────────────
|
|
534
|
+
ASSIGNMENT_MANAGE: "assignment:manage",
|
|
535
|
+
ASSIGNMENT_READ: "assignment:read",
|
|
536
|
+
ASSIGNMENT_REVIEW: "assignment:review",
|
|
537
|
+
ASSIGNMENT_SUBMIT: "assignment:submit",
|
|
538
|
+
ASSIGNMENT_WRITE: "assignment:write",
|
|
539
|
+
LESSON_DELETE: "lesson:delete",
|
|
540
|
+
LESSON_READ: "lesson:read",
|
|
541
|
+
LESSON_WRITE: "lesson:write",
|
|
542
|
+
MANAGEMENT_READ: "management:read",
|
|
543
|
+
MANAGEMENT_WRITE: "management:write",
|
|
544
|
+
PLUGIN_READ: "plugin:read",
|
|
545
|
+
PLUGIN_WRITE: "plugin:write",
|
|
546
|
+
PROCESS_READ: "process:read",
|
|
547
|
+
PROCESS_WRITE: "process:write",
|
|
548
|
+
STUDENT_WRITE: "student:write",
|
|
549
|
+
VFS_READ: "vfs:read",
|
|
550
|
+
VFS_WRITE: "vfs:write",
|
|
551
|
+
WHITEBOARD_READ: "whiteboard:read",
|
|
552
|
+
WHITEBOARD_WRITE: "whiteboard:write",
|
|
553
|
+
// ── 历史上只出现在 capabilitiesProposed、从未作为 capabilityRequired ──
|
|
554
|
+
// 保留是为了不让内置 management 插件在升级后突然被标成「未知串」。
|
|
555
|
+
CLASS_READ: "class:read",
|
|
556
|
+
CLASS_WRITE: "class:write",
|
|
557
|
+
STUDENT_READ: "student:read",
|
|
558
|
+
// ── 方法级(method-policy / 服务代理的 Security Barrier 3)───────────
|
|
559
|
+
API_WRITE: CAP_API_WRITE,
|
|
560
|
+
API_ADMIN: CAP_API_ADMIN,
|
|
561
|
+
GRADES_WRITE: CAP_GRADES_WRITE,
|
|
562
|
+
AGENT_TOOL: CAP_AGENT_TOOL,
|
|
563
|
+
AI_INVOKE: CAP_AI_INVOKE,
|
|
564
|
+
TASK_REGISTER: CAP_TASK_REGISTER,
|
|
565
|
+
STUDENT_DATA_READ: CAP_STUDENT_DATA_READ,
|
|
566
|
+
STUDENT_DATA_WRITE: CAP_STUDENT_DATA_WRITE,
|
|
567
|
+
// ── 平台内部 ─────────────────────────────────────────────────────────
|
|
568
|
+
// 片段命令(segment gate)用,插件片段可声明;不在 AI 工具箱里。
|
|
569
|
+
SEGMENT_GATE_EVAL: "segment:gate_eval"
|
|
570
|
+
};
|
|
571
|
+
CAPABILITIES = Object.freeze(
|
|
572
|
+
Array.from(new Set(Object.values(CAP)))
|
|
573
|
+
);
|
|
574
|
+
KNOWN_CAPABILITIES = new Set(CAPABILITIES);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
510
578
|
// packages/core/esm-loader/manifest-schema.ts
|
|
511
579
|
var import_zod, classroomToolSchema, helpDocSchema, quickActivitySchema, timelineSegmentSchema, paletteItemSchema, cockpitWidgetSchema, stageCardSchema, classroomTopbarActionSchema, classroomAttributionAwardSchema, studentCompetencyDimensionSchema, studentProfileWidgetSchema, canvasWidgetSchema, barometerMetricSchema, peerReviewRubricSchema, peerReviewBadgeSchema, preclassPasscodeActionSchema, preclassPasscodeAddonSchema, commandPaletteItemSchema, quickInsertItemSchema, aiPromptTemplateSchema, aiSkillProviderSchema, classTabSchema, classBatchActionSchema, studentRowPanelSchema, editorHeaderActionSchema, postClassWidgetSchema, coursewareViewerToolbarSchema, coursewareViewerOverlaySchema, portalTelemetrySchema, portalCourseBadgeSchema, portalTeachingModeSchema, portalInsightSchema, portalPreflightSchema, portalLaunchActionSchema, countdownWidgetSchema, countdownActionSchema, studentCountdownSchema, studentQuickActionsItemSchema, studentQuickActionsActionSchema, studentQuickActionsFabSchema, notificationTabsSchema, pacingDashboardSchema, diagnosticFeedSchema, collabCanvasSchema, topbarPillSchema, attributionActionSchema, leaderboardActionSchema, studentProfileActionSchema, studentProfileTimelineItemSchema, classroomHeaderActionSchema, classroomAgendaActionSchema, whiteboardDockPluginSchema, classroomAuditEventSchema, peerReviewActionSchema, peerReviewShowcaseWidgetSchema, seatingToolbarSchema, seatingLegendSchema, seatingSeatBadgeSchema, seatingSeatActionsSchema, seatingSummarySchema, whiteboardAutosaveStatusSchema, whiteboardAutosaveActionSchema, teacherPanelSchema, studentFullscreenSchema, globalSettingSchema, navUserMenuSchema, frontendSlotEntrySchema, frontendSlotsShape, pluginLocalesSchema, contributesSchema, deploySchema, apiRouteSchema, apiSchema, requiresItemSchema, manifestSchema, requiresItemV3Schema, manifestSchemaV3;
|
|
512
580
|
var init_manifest_schema = __esm({
|
|
@@ -1800,7 +1868,7 @@ var init_token = __esm({
|
|
|
1800
1868
|
});
|
|
1801
1869
|
|
|
1802
1870
|
// packages/core/di/interfaces.ts
|
|
1803
|
-
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, IClassroomLifecycleServiceToken, IInteractionRuntimeServiceToken, IClassroomCountdownServiceToken, IAssessmentGraderRegistryToken, IStageGuardServiceToken, PLUGIN_RESOLVABLE_TOKENS, PLUGIN_RESOLVABLE_TOKEN_NAMES, INetworkProxyServiceToken;
|
|
1871
|
+
var ICommandBusServiceToken4, IEventBusServiceToken3, IActionRegistryServiceToken4, ICapabilityServiceToken, IProcessServiceToken2, IStorageServiceToken, IAIServiceToken, IDatabaseToken4, IPluginHostToken, IPluginLifecycleManagerToken, IPluginDistributionManagerToken, IPluginRuntimeCompositionToken, IUnifiedExtensionRegistryToken, IPluginCapabilityGatewayToken, ICapabilityRegistryToken, IAuthSessionBridgeToken, ISemesterGradeServiceToken, IPointsDimensionRegistryToken, IPointsLedgerServiceToken, IClassRosterServiceToken, ILessonEngineServiceToken, IClassroomRuntimeServiceToken, IPresenceEngineServiceToken, ITeachingCollaborationServiceToken, ILearningAnalyticsServiceToken, IAICapabilityServiceToken, ICapabilityRuntimeServiceToken, ICapabilityGovernanceServiceToken, IPlatformServiceRegistryToken, ICoursewareRuntimeScriptRegistryToken, IClassroomLifecycleServiceToken, IInteractionRuntimeServiceToken, IClassroomCountdownServiceToken, IAssessmentGraderRegistryToken, IStageGuardServiceToken, PLUGIN_RESOLVABLE_TOKENS, PLUGIN_RESOLVABLE_TOKEN_NAMES, INetworkProxyServiceToken;
|
|
1804
1872
|
var init_interfaces = __esm({
|
|
1805
1873
|
"packages/core/di/interfaces.ts"() {
|
|
1806
1874
|
"use strict";
|
|
@@ -1839,6 +1907,7 @@ var init_interfaces = __esm({
|
|
|
1839
1907
|
"@openlearn/core:IPointsDimensionRegistry"
|
|
1840
1908
|
);
|
|
1841
1909
|
IPointsLedgerServiceToken = new Token("@openlearn/core:IPointsLedgerService");
|
|
1910
|
+
IClassRosterServiceToken = new Token("@openlearn/core:IClassRosterService");
|
|
1842
1911
|
ILessonEngineServiceToken = new Token("@openlearn/core:ILessonEngineService");
|
|
1843
1912
|
IClassroomRuntimeServiceToken = new Token(
|
|
1844
1913
|
"@openlearn/core:IClassroomRuntimeService"
|
|
@@ -1889,6 +1958,7 @@ var init_interfaces = __esm({
|
|
|
1889
1958
|
ISemesterGradeServiceToken,
|
|
1890
1959
|
IPointsDimensionRegistryToken,
|
|
1891
1960
|
IPointsLedgerServiceToken,
|
|
1961
|
+
IClassRosterServiceToken,
|
|
1892
1962
|
ILessonEngineServiceToken,
|
|
1893
1963
|
IClassroomRuntimeServiceToken,
|
|
1894
1964
|
IPresenceEngineServiceToken,
|
|
@@ -3929,9 +3999,9 @@ var init_http_router = __esm({
|
|
|
3929
3999
|
/**
|
|
3930
4000
|
* 注册指定动词与路径的 Handler
|
|
3931
4001
|
*/
|
|
3932
|
-
route(method,
|
|
4002
|
+
route(method, path48, handler, options = {}) {
|
|
3933
4003
|
const upperMethod = method.toUpperCase();
|
|
3934
|
-
const normalized =
|
|
4004
|
+
const normalized = path48.startsWith("/") ? path48 : "/" + path48;
|
|
3935
4005
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3936
4006
|
this.warnIfUnscoped(upperMethod, normalized, options);
|
|
3937
4007
|
this.routes.push({
|
|
@@ -3944,20 +4014,20 @@ var init_http_router = __esm({
|
|
|
3944
4014
|
scope: options.scope
|
|
3945
4015
|
});
|
|
3946
4016
|
}
|
|
3947
|
-
get(
|
|
3948
|
-
this.route("GET",
|
|
4017
|
+
get(path48, handler, options) {
|
|
4018
|
+
this.route("GET", path48, handler, options);
|
|
3949
4019
|
}
|
|
3950
|
-
post(
|
|
3951
|
-
this.route("POST",
|
|
4020
|
+
post(path48, handler, options) {
|
|
4021
|
+
this.route("POST", path48, handler, options);
|
|
3952
4022
|
}
|
|
3953
|
-
put(
|
|
3954
|
-
this.route("PUT",
|
|
4023
|
+
put(path48, handler, options) {
|
|
4024
|
+
this.route("PUT", path48, handler, options);
|
|
3955
4025
|
}
|
|
3956
|
-
patch(
|
|
3957
|
-
this.route("PATCH",
|
|
4026
|
+
patch(path48, handler, options) {
|
|
4027
|
+
this.route("PATCH", path48, handler, options);
|
|
3958
4028
|
}
|
|
3959
|
-
delete(
|
|
3960
|
-
this.route("DELETE",
|
|
4029
|
+
delete(path48, handler, options) {
|
|
4030
|
+
this.route("DELETE", path48, handler, options);
|
|
3961
4031
|
}
|
|
3962
4032
|
warnIfUnscoped(method, pattern, options) {
|
|
3963
4033
|
if (options.scope) return;
|
|
@@ -3972,19 +4042,19 @@ var init_http_router = __esm({
|
|
|
3972
4042
|
}
|
|
3973
4043
|
stream(methodOrPath, pathOrHandler, maybeHandler) {
|
|
3974
4044
|
if (typeof pathOrHandler === "function") {
|
|
3975
|
-
const
|
|
4045
|
+
const path48 = methodOrPath;
|
|
3976
4046
|
const handler = pathOrHandler;
|
|
3977
|
-
this.registerStream("GET",
|
|
3978
|
-
this.registerStream("POST",
|
|
4047
|
+
this.registerStream("GET", path48, handler);
|
|
4048
|
+
this.registerStream("POST", path48, handler);
|
|
3979
4049
|
} else {
|
|
3980
4050
|
const method = methodOrPath.toUpperCase();
|
|
3981
|
-
const
|
|
4051
|
+
const path48 = pathOrHandler;
|
|
3982
4052
|
const handler = maybeHandler;
|
|
3983
|
-
this.registerStream(method,
|
|
4053
|
+
this.registerStream(method, path48, handler);
|
|
3984
4054
|
}
|
|
3985
4055
|
}
|
|
3986
|
-
registerStream(method,
|
|
3987
|
-
const normalized =
|
|
4056
|
+
registerStream(method, path48, handler) {
|
|
4057
|
+
const normalized = path48.startsWith("/") ? path48 : "/" + path48;
|
|
3988
4058
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3989
4059
|
this.routes.push({
|
|
3990
4060
|
method: method.toUpperCase(),
|
|
@@ -3998,9 +4068,9 @@ var init_http_router = __esm({
|
|
|
3998
4068
|
/**
|
|
3999
4069
|
* 检查指定动词与路径是否为流式路由
|
|
4000
4070
|
*/
|
|
4001
|
-
isStream(method,
|
|
4071
|
+
isStream(method, path48) {
|
|
4002
4072
|
const upperMethod = method.toUpperCase();
|
|
4003
|
-
const normalized =
|
|
4073
|
+
const normalized = path48.startsWith("/") ? path48 : "/" + path48;
|
|
4004
4074
|
for (const entry of this.routes) {
|
|
4005
4075
|
if (entry.method !== upperMethod) continue;
|
|
4006
4076
|
if (entry.regex.test(normalized)) {
|
|
@@ -4012,9 +4082,9 @@ var init_http_router = __esm({
|
|
|
4012
4082
|
/**
|
|
4013
4083
|
* 匹配路由并提取路由参数
|
|
4014
4084
|
*/
|
|
4015
|
-
match(method,
|
|
4085
|
+
match(method, path48) {
|
|
4016
4086
|
const upperMethod = method.toUpperCase();
|
|
4017
|
-
const normalized =
|
|
4087
|
+
const normalized = path48.startsWith("/") ? path48 : "/" + path48;
|
|
4018
4088
|
for (const entry of this.routes) {
|
|
4019
4089
|
if (entry.method !== upperMethod) continue;
|
|
4020
4090
|
const match = normalized.match(entry.regex);
|
|
@@ -4157,13 +4227,13 @@ function checkMethodPolicy(input) {
|
|
|
4157
4227
|
if (tokens.frontendApi && token === tokens.frontendApi) {
|
|
4158
4228
|
if (isReadOnlyMethod(method)) return null;
|
|
4159
4229
|
if (method === "request" && !Array.isArray(args)) return null;
|
|
4160
|
-
const
|
|
4161
|
-
const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) =>
|
|
4162
|
-
const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) =>
|
|
4163
|
-
const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(
|
|
4230
|
+
const path48 = extractPathArg(args);
|
|
4231
|
+
const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path48 === p || path48.startsWith(`${p}/`));
|
|
4232
|
+
const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path48.endsWith(s));
|
|
4233
|
+
const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path48);
|
|
4164
4234
|
if (isHighRisk || isIrreversible || isEntityDelete) {
|
|
4165
4235
|
if (hasCapability(caps, CAP_API_ADMIN)) return null;
|
|
4166
|
-
const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${
|
|
4236
|
+
const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${path48}'` : isEntityDelete ? `\u5B9E\u4F53\u7EA7\u5220\u9664 '${path48}'` : `\u5199\u5165\u9AD8\u5371\u8DEF\u5F84 '${path48 || "<\u672A\u63D0\u4F9B\u8DEF\u5F84>"}'`;
|
|
4167
4237
|
return `${why} \u9700\u8981 manifest \u58F0\u660E '${CAP_API_ADMIN}' \u80FD\u529B`;
|
|
4168
4238
|
}
|
|
4169
4239
|
return hasCapability(caps, CAP_API_WRITE) ? null : `\u5199\u64CD\u4F5C\uFF08${method}\uFF09\u9700\u8981 manifest \u58F0\u660E '${CAP_API_WRITE}' \u80FD\u529B\uFF08\u666E\u901A\u53EA\u8BFB\u8C03\u7528\u8BF7\u7528 get\uFF09`;
|
|
@@ -4209,18 +4279,11 @@ function checkMethodPolicy(input) {
|
|
|
4209
4279
|
if (tokens.uiService && token === tokens.uiService) return null;
|
|
4210
4280
|
return null;
|
|
4211
4281
|
}
|
|
4212
|
-
var
|
|
4282
|
+
var HIGH_RISK_PATH_PREFIXES, IRREVERSIBLE_PATH_SUFFIXES, ENTITY_DELETE_RE;
|
|
4213
4283
|
var init_method_policy = __esm({
|
|
4214
4284
|
"packages/core/plugin-host/method-policy.ts"() {
|
|
4215
4285
|
"use strict";
|
|
4216
|
-
|
|
4217
|
-
CAP_API_ADMIN = "api:admin";
|
|
4218
|
-
CAP_GRADES_WRITE = "grades:write";
|
|
4219
|
-
CAP_AGENT_TOOL = "agent:tool";
|
|
4220
|
-
CAP_AI_INVOKE = "ai:invoke";
|
|
4221
|
-
CAP_TASK_REGISTER = "task:register";
|
|
4222
|
-
CAP_STUDENT_DATA_READ = "student-data:read";
|
|
4223
|
-
CAP_STUDENT_DATA_WRITE = "student-data:write";
|
|
4286
|
+
init_catalog();
|
|
4224
4287
|
HIGH_RISK_PATH_PREFIXES = Object.freeze([
|
|
4225
4288
|
"/api/plugins",
|
|
4226
4289
|
// 插件增删改、一键更新(可执行任意第三方代码)
|
|
@@ -4244,51 +4307,238 @@ var init_method_policy = __esm({
|
|
|
4244
4307
|
}
|
|
4245
4308
|
});
|
|
4246
4309
|
|
|
4247
|
-
// packages/core/
|
|
4248
|
-
function
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4310
|
+
// packages/core/plugin-host/sql-guard.ts
|
|
4311
|
+
function sanitizeSql(sql) {
|
|
4312
|
+
let inSingleQuote = false;
|
|
4313
|
+
let inDoubleQuote = false;
|
|
4314
|
+
let inBacktick = false;
|
|
4315
|
+
let inBracket = false;
|
|
4316
|
+
let result = "";
|
|
4317
|
+
const len = sql.length;
|
|
4318
|
+
for (let i = 0; i < len; i++) {
|
|
4319
|
+
const ch = sql[i];
|
|
4320
|
+
const next = i + 1 < len ? sql[i + 1] : "";
|
|
4321
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "-" && next === "-") {
|
|
4322
|
+
while (i < len && sql[i] !== "\n") i++;
|
|
4323
|
+
result += " ";
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "/" && next === "*") {
|
|
4327
|
+
i += 2;
|
|
4328
|
+
while (i < len && !(sql[i] === "*" && i + 1 < len && sql[i + 1] === "/")) {
|
|
4329
|
+
i++;
|
|
4330
|
+
}
|
|
4331
|
+
i++;
|
|
4332
|
+
result += " ";
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
if (ch === "'" && !inDoubleQuote && !inBacktick && !inBracket) {
|
|
4336
|
+
if (inSingleQuote) {
|
|
4337
|
+
if (next === "'") {
|
|
4338
|
+
i++;
|
|
4339
|
+
} else {
|
|
4340
|
+
inSingleQuote = false;
|
|
4341
|
+
}
|
|
4342
|
+
} else {
|
|
4343
|
+
inSingleQuote = true;
|
|
4344
|
+
}
|
|
4345
|
+
result += " ";
|
|
4346
|
+
continue;
|
|
4347
|
+
}
|
|
4348
|
+
if (inSingleQuote) {
|
|
4349
|
+
result += " ";
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
4352
|
+
if (ch === "[" && !inDoubleQuote && !inBacktick) {
|
|
4353
|
+
inBracket = true;
|
|
4354
|
+
result += " ";
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
if (ch === "]" && inBracket) {
|
|
4358
|
+
inBracket = false;
|
|
4359
|
+
result += " ";
|
|
4360
|
+
continue;
|
|
4361
|
+
}
|
|
4362
|
+
if (ch === "`" && !inDoubleQuote) {
|
|
4363
|
+
inBacktick = !inBacktick;
|
|
4364
|
+
result += " ";
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
if (ch === '"' && !inBacktick) {
|
|
4368
|
+
inDoubleQuote = !inDoubleQuote;
|
|
4369
|
+
result += " ";
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
result += ch;
|
|
4252
4373
|
}
|
|
4253
|
-
|
|
4254
|
-
const metadata = raw.metadata && typeof raw.metadata === "object" ? { ...raw.metadata } : {};
|
|
4255
|
-
delete metadata.approved;
|
|
4256
|
-
return { ...raw, actorId: actor, metadata };
|
|
4374
|
+
return result;
|
|
4257
4375
|
}
|
|
4258
|
-
function
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
4376
|
+
function extractFlatTables(flatSql, tables) {
|
|
4377
|
+
const ddlRegex = /\b(?:CREATE|DROP|ALTER)\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?([a-zA-Z_][\w]*)/gi;
|
|
4378
|
+
for (const m of flatSql.matchAll(ddlRegex)) {
|
|
4379
|
+
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
4380
|
+
tables.add(m[1].toLowerCase());
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
const renameToRegex = /\bRENAME\s+TO\s+([a-zA-Z_][\w]*)/gi;
|
|
4384
|
+
for (const m of flatSql.matchAll(renameToRegex)) {
|
|
4385
|
+
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
4386
|
+
tables.add(m[1].toLowerCase());
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
const simpleClauseRegex = /\b(?:INTO|UPDATE(?:\s+OR\s+(?:ROLLBACK|ABORT|REPLACE|FAIL|IGNORE))?|JOIN)\s+([a-zA-Z_][\w]*)/gi;
|
|
4390
|
+
for (const m of flatSql.matchAll(simpleClauseRegex)) {
|
|
4391
|
+
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
4392
|
+
tables.add(m[1].toLowerCase());
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
const fromRegex = /\bFROM\s+([^;]+?)(?=\bWHERE\b|\bGROUP\b|\bHAVING\b|\bORDER\b|\bLIMIT\b|\bUNION\b|\bEXCEPT\b|\bINTERSECT\b|\bWINDOW\b|\bVALUES\b|\bSET\b|\bINTO\b|\bJOIN\b|\bLEFT\b|\bRIGHT\b|\bINNER\b|\bOUTER\b|\bCROSS\b|\bNATURAL\b|\bON\b|\bUSING\b|\bSELECT\b|;|$)/gi;
|
|
4396
|
+
for (const match of flatSql.matchAll(fromRegex)) {
|
|
4397
|
+
const clause = match[1];
|
|
4398
|
+
if (!clause) continue;
|
|
4399
|
+
const parts = clause.split(",");
|
|
4400
|
+
for (const part of parts) {
|
|
4401
|
+
const trimmed2 = part.trim();
|
|
4402
|
+
if (!trimmed2) continue;
|
|
4403
|
+
const tokenMatch = /^[a-zA-Z_][\w]*/.exec(trimmed2);
|
|
4404
|
+
if (tokenMatch) {
|
|
4405
|
+
const tableName = tokenMatch[0].toLowerCase();
|
|
4406
|
+
if (tableName !== SUBQUERY_PLACEHOLDER) {
|
|
4407
|
+
tables.add(tableName);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4262
4411
|
}
|
|
4263
|
-
return id.startsWith("plugin:") ? id : `plugin:${id}`;
|
|
4264
4412
|
}
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4413
|
+
function extractReferencedTables(cleanSql) {
|
|
4414
|
+
const tables = /* @__PURE__ */ new Set();
|
|
4415
|
+
let working = cleanSql;
|
|
4416
|
+
const parenRegex = /\(([^()]*)\)/g;
|
|
4417
|
+
let depth = 0;
|
|
4418
|
+
while (parenRegex.test(working) && depth++ < 100) {
|
|
4419
|
+
working = working.replace(parenRegex, (_full, inner) => {
|
|
4420
|
+
extractFlatTables(inner, tables);
|
|
4421
|
+
const trimmedInner = inner.trim();
|
|
4422
|
+
if (/^(?:SELECT|VALUES|WITH)\b/i.test(trimmedInner)) {
|
|
4423
|
+
return ` ${SUBQUERY_PLACEHOLDER} `;
|
|
4424
|
+
}
|
|
4425
|
+
return ` ${inner} `;
|
|
4426
|
+
});
|
|
4268
4427
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
function resolvePluginCommandType(type, pluginId) {
|
|
4273
|
-
const prefix = pluginId + ".";
|
|
4274
|
-
if (type.startsWith(prefix)) return type;
|
|
4275
|
-
if (UUID_V7_PREFIX.test(type)) return type;
|
|
4276
|
-
return prefix + type;
|
|
4428
|
+
working = working.replace(/[()]/g, " ");
|
|
4429
|
+
extractFlatTables(working, tables);
|
|
4430
|
+
return tables;
|
|
4277
4431
|
}
|
|
4278
|
-
function
|
|
4279
|
-
const
|
|
4280
|
-
if (
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4432
|
+
function assertPluginSqlAllowed(options) {
|
|
4433
|
+
const { sql, ownPrefix, allowedPrefixes = [], researchAllowed = false } = options;
|
|
4434
|
+
if (typeof sql !== "string" || sql.trim() === "") return;
|
|
4435
|
+
for (const opRegex of FORBIDDEN_OPERATIONS) {
|
|
4436
|
+
if (opRegex.test(sql)) {
|
|
4437
|
+
throw new Error(`Dangerous statement is forbidden: "${sql.trim()}"`);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
const clean = sanitizeSql(sql);
|
|
4441
|
+
const tables = extractReferencedTables(clean);
|
|
4442
|
+
if (/\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\b/i.test(clean) && tables.size === 0) {
|
|
4443
|
+
throw new Error(`Worker plugin SQL rejected: unable to resolve target table in "${sql.trim()}"`);
|
|
4444
|
+
}
|
|
4445
|
+
const isDdl = /^\s*(DROP|CREATE|ALTER|TRUNCATE)\b/i.test(clean);
|
|
4446
|
+
const words = clean.toLowerCase().match(/\b[a-zA-Z_][\w]*\b/g) || [];
|
|
4447
|
+
for (const word of words) {
|
|
4448
|
+
if (FORBIDDEN_CORE_TABLES_SET.has(word)) {
|
|
4449
|
+
if (tables.has(word)) {
|
|
4450
|
+
if (isDdl) {
|
|
4451
|
+
throw new Error(
|
|
4452
|
+
`Worker plugin is not permitted to perform DDL on table "${word}" (core security table is strictly forbidden)`
|
|
4453
|
+
);
|
|
4454
|
+
}
|
|
4455
|
+
throw new Error(
|
|
4456
|
+
`Worker plugin is not permitted to access table "${word}" (core security table is strictly forbidden)`
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
const ownPrefixLower = ownPrefix ? ownPrefix.toLowerCase() : "";
|
|
4462
|
+
const validPrefixes = [ownPrefixLower, ...allowedPrefixes.map((p) => p.toLowerCase())].filter(Boolean);
|
|
4463
|
+
for (const table of tables) {
|
|
4464
|
+
if (table === "plugin_migrations") continue;
|
|
4465
|
+
if (researchAllowed && table.startsWith("plugin_research_")) continue;
|
|
4466
|
+
const isOwn = validPrefixes.some((p) => table.startsWith(p));
|
|
4467
|
+
if (isOwn) continue;
|
|
4468
|
+
if (table === "sqlite_master" || table === "sqlite_schema" || table === "sqlite_temp_master") {
|
|
4469
|
+
const strippedComments = sql.replace(/--.*$/gm, " ").replace(/\/\*[\s\S]*?\*\//g, " ").trim();
|
|
4470
|
+
const isAllowedMetaQuery = /^\s*SELECT\s+name\s+FROM\s+(?:sqlite_master|sqlite_schema)\s+WHERE\s+type\s*=\s*['"]table['"]\s+AND\s+name\s+LIKE\s+['"][A-Za-z0-9_%]+['"]\s*$/i.test(
|
|
4471
|
+
strippedComments
|
|
4472
|
+
) && ownPrefixLower && strippedComments.toLowerCase().includes(ownPrefixLower);
|
|
4473
|
+
if (isAllowedMetaQuery) continue;
|
|
4474
|
+
throw new Error(`Direct access to sqlite metadata table "${table}" is forbidden`);
|
|
4475
|
+
}
|
|
4476
|
+
if (isDdl) {
|
|
4477
|
+
throw new Error(
|
|
4478
|
+
`Worker plugin is not permitted to perform DDL on table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
throw new Error(
|
|
4482
|
+
`Worker plugin is not permitted to access table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
|
|
4483
|
+
);
|
|
4284
4484
|
}
|
|
4285
|
-
return type;
|
|
4286
4485
|
}
|
|
4287
|
-
var
|
|
4288
|
-
var
|
|
4289
|
-
"packages/core/plugin-host/
|
|
4486
|
+
var FORBIDDEN_CORE_TABLES, FORBIDDEN_CORE_TABLES_SET, FORBIDDEN_OPERATIONS, SUBQUERY_PLACEHOLDER;
|
|
4487
|
+
var init_sql_guard = __esm({
|
|
4488
|
+
"packages/core/plugin-host/sql-guard.ts"() {
|
|
4290
4489
|
"use strict";
|
|
4291
|
-
|
|
4490
|
+
FORBIDDEN_CORE_TABLES = Object.freeze([
|
|
4491
|
+
"users",
|
|
4492
|
+
"client_sessions",
|
|
4493
|
+
"plugins",
|
|
4494
|
+
"plugin_storage",
|
|
4495
|
+
"ai_providers",
|
|
4496
|
+
"processes",
|
|
4497
|
+
"pending_commands",
|
|
4498
|
+
"classes",
|
|
4499
|
+
"students",
|
|
4500
|
+
"class_students",
|
|
4501
|
+
"lessons",
|
|
4502
|
+
"assignments",
|
|
4503
|
+
"assignment_submissions",
|
|
4504
|
+
"attendance",
|
|
4505
|
+
"schedules",
|
|
4506
|
+
"class_grade_weights",
|
|
4507
|
+
"exams",
|
|
4508
|
+
"exam_scores",
|
|
4509
|
+
"student_semester_reports",
|
|
4510
|
+
"student_rollcalls",
|
|
4511
|
+
"student_seats",
|
|
4512
|
+
"student_read_notifications",
|
|
4513
|
+
"student_lesson_progress",
|
|
4514
|
+
"courseware",
|
|
4515
|
+
"courseware_attempt",
|
|
4516
|
+
"system_resources",
|
|
4517
|
+
"events",
|
|
4518
|
+
"_migrations",
|
|
4519
|
+
"site_settings",
|
|
4520
|
+
"agent_conversations",
|
|
4521
|
+
"classroom_sessions",
|
|
4522
|
+
"lesson_quiz_submissions",
|
|
4523
|
+
"courseware_score_config",
|
|
4524
|
+
"demo_data_registry",
|
|
4525
|
+
"whiteboard_elements",
|
|
4526
|
+
"student_point_logs",
|
|
4527
|
+
"lianyun_research_activities",
|
|
4528
|
+
"lianyun_research_groups",
|
|
4529
|
+
"lianyun_research_submissions",
|
|
4530
|
+
"lianyun_research_reviews"
|
|
4531
|
+
]);
|
|
4532
|
+
FORBIDDEN_CORE_TABLES_SET = new Set(FORBIDDEN_CORE_TABLES.map((t) => t.toLowerCase()));
|
|
4533
|
+
FORBIDDEN_OPERATIONS = [
|
|
4534
|
+
/\bATTACH(?:\s+DATABASE)?\b/i,
|
|
4535
|
+
/\bDETACH(?:\s+DATABASE)?\b/i,
|
|
4536
|
+
/\bPRAGMA\b/i,
|
|
4537
|
+
/\bVACUUM\b/i,
|
|
4538
|
+
/\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
|
|
4539
|
+
/\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
|
|
4540
|
+
];
|
|
4541
|
+
SUBQUERY_PLACEHOLDER = "__ol_subquery__";
|
|
4292
4542
|
}
|
|
4293
4543
|
});
|
|
4294
4544
|
|
|
@@ -4318,6 +4568,108 @@ var init_plugin_tables = __esm({
|
|
|
4318
4568
|
}
|
|
4319
4569
|
});
|
|
4320
4570
|
|
|
4571
|
+
// packages/core/plugin-host/inline-db-guard.ts
|
|
4572
|
+
function isSqlGuardAllModesEnabled(env = process.env) {
|
|
4573
|
+
const raw = env.OPENLEARN_SQL_GUARD_ALL_MODES?.trim().toLowerCase();
|
|
4574
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
4575
|
+
}
|
|
4576
|
+
function createGuardedPluginDatabase(db2, pluginId, manifestId) {
|
|
4577
|
+
const ownPrefix = pluginTablePrefix(pluginId, manifestId);
|
|
4578
|
+
const allowedPrefixes = listAllowedPluginTablePrefixes(pluginId, manifestId);
|
|
4579
|
+
const assertSql = (sql) => {
|
|
4580
|
+
try {
|
|
4581
|
+
assertPluginSqlAllowed({ sql: typeof sql === "string" ? sql : String(sql ?? ""), ownPrefix, allowedPrefixes });
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
const message = err?.message ? String(err.message) : String(err);
|
|
4584
|
+
const wrapped = new Error(`${SQL_GUARD_ERROR_PREFIX}\uFF1A${message}\uFF08\u63D2\u4EF6 "${pluginId}"\uFF09`);
|
|
4585
|
+
wrapped.code = "PLUGIN_SQL_REJECTED";
|
|
4586
|
+
throw wrapped;
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
return new Proxy(db2, {
|
|
4590
|
+
get(target, prop, receiver) {
|
|
4591
|
+
const value = Reflect.get(target, prop, receiver);
|
|
4592
|
+
if (typeof value !== "function") return value;
|
|
4593
|
+
if (prop === "prepare") {
|
|
4594
|
+
return (sql, ...rest) => {
|
|
4595
|
+
assertSql(sql);
|
|
4596
|
+
return value.call(target, sql, ...rest);
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
if (prop === "exec") {
|
|
4600
|
+
return (sql, ...rest) => {
|
|
4601
|
+
assertSql(sql);
|
|
4602
|
+
return value.call(target, sql, ...rest);
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
if (prop === "pragma") {
|
|
4606
|
+
return (source, ...rest) => {
|
|
4607
|
+
assertSql(`PRAGMA ${String(source ?? "")}`);
|
|
4608
|
+
return value.call(target, source, ...rest);
|
|
4609
|
+
};
|
|
4610
|
+
}
|
|
4611
|
+
return value.bind(target);
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
var SQL_GUARD_ERROR_PREFIX;
|
|
4616
|
+
var init_inline_db_guard = __esm({
|
|
4617
|
+
"packages/core/plugin-host/inline-db-guard.ts"() {
|
|
4618
|
+
"use strict";
|
|
4619
|
+
init_sql_guard();
|
|
4620
|
+
init_plugin_tables();
|
|
4621
|
+
SQL_GUARD_ERROR_PREFIX = "[PluginHost] SQL \u88AB\u62D2\u7EDD";
|
|
4622
|
+
}
|
|
4623
|
+
});
|
|
4624
|
+
|
|
4625
|
+
// packages/core/command-bus/plugin-command.ts
|
|
4626
|
+
function bindPluginCommand(command, pluginActorId) {
|
|
4627
|
+
const actor = typeof pluginActorId === "string" ? pluginActorId.trim() : "";
|
|
4628
|
+
if (!actor) {
|
|
4629
|
+
throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
|
|
4630
|
+
}
|
|
4631
|
+
const raw = command && typeof command === "object" ? { ...command } : {};
|
|
4632
|
+
const metadata = raw.metadata && typeof raw.metadata === "object" ? { ...raw.metadata } : {};
|
|
4633
|
+
delete metadata.approved;
|
|
4634
|
+
return { ...raw, actorId: actor, metadata };
|
|
4635
|
+
}
|
|
4636
|
+
function pluginActorIdFor(manifestId, pluginId) {
|
|
4637
|
+
const id = (manifestId || pluginId || "").trim();
|
|
4638
|
+
if (!id) {
|
|
4639
|
+
throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
|
|
4640
|
+
}
|
|
4641
|
+
return id.startsWith("plugin:") ? id : `plugin:${id}`;
|
|
4642
|
+
}
|
|
4643
|
+
var init_plugin_command = __esm({
|
|
4644
|
+
"packages/core/command-bus/plugin-command.ts"() {
|
|
4645
|
+
"use strict";
|
|
4646
|
+
}
|
|
4647
|
+
});
|
|
4648
|
+
|
|
4649
|
+
// packages/core/plugin-host/plugin-namespace.ts
|
|
4650
|
+
function resolvePluginCommandType(type, pluginId) {
|
|
4651
|
+
const prefix = pluginId + ".";
|
|
4652
|
+
if (type.startsWith(prefix)) return type;
|
|
4653
|
+
if (UUID_V7_PREFIX.test(type)) return type;
|
|
4654
|
+
return prefix + type;
|
|
4655
|
+
}
|
|
4656
|
+
function stripPluginCommandPrefix(type, pluginId) {
|
|
4657
|
+
const prefix = pluginId + ".";
|
|
4658
|
+
if (type.startsWith(prefix)) return type.slice(prefix.length);
|
|
4659
|
+
if (UUID_V7_PREFIX.test(type)) {
|
|
4660
|
+
const idx = type.indexOf(".");
|
|
4661
|
+
return type.slice(idx + 1);
|
|
4662
|
+
}
|
|
4663
|
+
return type;
|
|
4664
|
+
}
|
|
4665
|
+
var UUID_V7_PREFIX;
|
|
4666
|
+
var init_plugin_namespace = __esm({
|
|
4667
|
+
"packages/core/plugin-host/plugin-namespace.ts"() {
|
|
4668
|
+
"use strict";
|
|
4669
|
+
UUID_V7_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\./i;
|
|
4670
|
+
}
|
|
4671
|
+
});
|
|
4672
|
+
|
|
4321
4673
|
// packages/core/plugin-host/context-builder.ts
|
|
4322
4674
|
var context_builder_exports = {};
|
|
4323
4675
|
__export(context_builder_exports, {
|
|
@@ -4720,15 +5072,51 @@ function wrapInteractionRuntimeService(service, tracker, pluginId) {
|
|
|
4720
5072
|
listActivityProviders: createSafeFunction(() => service.listActivityProviders())
|
|
4721
5073
|
};
|
|
4722
5074
|
}
|
|
4723
|
-
function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId) {
|
|
5075
|
+
function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId, manifestId) {
|
|
4724
5076
|
if (tokenName === IClassroomLifecycleServiceToken.name) {
|
|
4725
5077
|
return wrapClassroomLifecycleService(raw, tracker, pluginId);
|
|
4726
5078
|
}
|
|
4727
5079
|
if (tokenName === IInteractionRuntimeServiceToken.name) {
|
|
4728
5080
|
return wrapInteractionRuntimeService(raw, tracker, pluginId);
|
|
4729
5081
|
}
|
|
5082
|
+
if (tokenName === "@openlearn/core:IDatabase" && isSqlGuardAllModesEnabled()) {
|
|
5083
|
+
return createGuardedPluginDatabase(raw, pluginId, manifestId);
|
|
5084
|
+
}
|
|
4730
5085
|
return raw;
|
|
4731
5086
|
}
|
|
5087
|
+
function wrapRosterService(service, pluginId, capabilities) {
|
|
5088
|
+
const CAP_ROSTER_READ = "management:read";
|
|
5089
|
+
const safe = (fn) => createSafeFunction(fn);
|
|
5090
|
+
const assertReadable = (method, detail) => {
|
|
5091
|
+
const hasCap = hasCapability(capabilities ?? [], CAP_ROSTER_READ);
|
|
5092
|
+
recordSecurityAudit("plugin.roster_read", {
|
|
5093
|
+
pluginId,
|
|
5094
|
+
method,
|
|
5095
|
+
...detail,
|
|
5096
|
+
hasCapability: hasCap,
|
|
5097
|
+
...hasCap ? {} : { denied: true }
|
|
5098
|
+
});
|
|
5099
|
+
if (hasCap) return;
|
|
5100
|
+
console.error(
|
|
5101
|
+
`[Security] \u63D2\u4EF6 "${pluginId}" \u8C03\u7528 roster.${method} \u88AB\u62D2\uFF1A\u7F3A\u5C11\u80FD\u529B "${CAP_ROSTER_READ}"\u3002\u8BF7\u5728 manifest.capabilitiesProposed \u58F0\u660E\u5B83\uFF08\u6559\u5E08\u89D2\u8272\u9ED8\u8BA4\u5177\u5907\uFF09\u3002`
|
|
5102
|
+
);
|
|
5103
|
+
throw new PluginCapabilityDeniedError(pluginId, CAP_ROSTER_READ, "read", method);
|
|
5104
|
+
};
|
|
5105
|
+
return {
|
|
5106
|
+
listClasses: safe(async () => {
|
|
5107
|
+
assertReadable("listClasses", {});
|
|
5108
|
+
return service.listClasses();
|
|
5109
|
+
}),
|
|
5110
|
+
listClassStudents: safe(async (classId) => {
|
|
5111
|
+
assertReadable("listClassStudents", { classId });
|
|
5112
|
+
return service.listClassStudents(classId);
|
|
5113
|
+
}),
|
|
5114
|
+
getStudent: safe(async (studentId) => {
|
|
5115
|
+
assertReadable("getStudent", { studentId });
|
|
5116
|
+
return service.getStudent(studentId);
|
|
5117
|
+
})
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
4732
5120
|
function wrapPointsLedgerService(service, pluginId, capabilities) {
|
|
4733
5121
|
const auditAccess = (studentId, method, operation, requiredCap) => {
|
|
4734
5122
|
const hasCap = hasCapability(capabilities ?? [], requiredCap);
|
|
@@ -4741,6 +5129,9 @@ function wrapPointsLedgerService(service, pluginId, capabilities) {
|
|
|
4741
5129
|
...hasCap ? {} : { denied: true }
|
|
4742
5130
|
});
|
|
4743
5131
|
if (!hasCap) {
|
|
5132
|
+
console.error(
|
|
5133
|
+
`[Security] \u63D2\u4EF6 "${pluginId}" \u8C03\u7528 pointsLedger.${method} \u88AB\u62D2\uFF1A\u7F3A\u5C11\u80FD\u529B "${requiredCap}"\uFF08${operation} / student=${studentId}\uFF09\u3002\u8BF7\u5728 manifest.capabilitiesProposed \u58F0\u660E\u5B83\u3002`
|
|
5134
|
+
);
|
|
4744
5135
|
throw new PluginCapabilityDeniedError(pluginId, requiredCap, operation, method);
|
|
4745
5136
|
}
|
|
4746
5137
|
};
|
|
@@ -4857,6 +5248,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4857
5248
|
const aiService = await serviceRegistry.resolve(IAIServiceToken);
|
|
4858
5249
|
const pointsDimensionRegistry = await serviceRegistry.tryResolve(IPointsDimensionRegistryToken);
|
|
4859
5250
|
const pointsLedgerService = await serviceRegistry.tryResolve(IPointsLedgerServiceToken);
|
|
5251
|
+
const rosterService = await serviceRegistry.tryResolve(IClassRosterServiceToken);
|
|
4860
5252
|
const wrappedCommandBus = wrapCommandBus(commandBusService, tracker, pluginId, manifest.id, actionRegistryService);
|
|
4861
5253
|
const wrappedEventBus = wrapEventBus(eventBusService, tracker, pluginId);
|
|
4862
5254
|
const wrappedProcessManager = wrapProcessManager(
|
|
@@ -4873,6 +5265,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4873
5265
|
const wrappedAI = wrapAI(aiService, pluginId, tracker);
|
|
4874
5266
|
const wrappedPointsDimension = pointsDimensionRegistry ? wrapPointsDimensionRegistry(pointsDimensionRegistry, tracker, pluginId) : null;
|
|
4875
5267
|
const wrappedPointsLedger = pointsLedgerService ? wrapPointsLedgerService(pointsLedgerService, pluginId, manifest.capabilitiesProposed) : null;
|
|
5268
|
+
const wrappedRoster = rosterService ? wrapRosterService(rosterService, pluginId, manifest.capabilitiesProposed) : null;
|
|
4876
5269
|
Object.setPrototypeOf(wrappedCommandBus, null);
|
|
4877
5270
|
Object.setPrototypeOf(wrappedEventBus, null);
|
|
4878
5271
|
Object.setPrototypeOf(wrappedProcessManager, null);
|
|
@@ -4900,7 +5293,8 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4900
5293
|
storage: wrappedStorage,
|
|
4901
5294
|
ai: wrappedAI,
|
|
4902
5295
|
pointsDimension: wrappedPointsDimension,
|
|
4903
|
-
pointsLedger: wrappedPointsLedger
|
|
5296
|
+
pointsLedger: wrappedPointsLedger,
|
|
5297
|
+
roster: wrappedRoster
|
|
4904
5298
|
};
|
|
4905
5299
|
if (skipTokens && skipTokens.size > 0) {
|
|
4906
5300
|
const TOKEN_TO_SERVICE_KEY = {
|
|
@@ -4912,7 +5306,8 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4912
5306
|
"@openlearn/core:IStorageService": "storage",
|
|
4913
5307
|
"@openlearn/core:IAIService": "ai",
|
|
4914
5308
|
"@openlearn/core:IPointsDimensionRegistry": "pointsDimension",
|
|
4915
|
-
"@openlearn/core:IPointsLedgerService": "pointsLedger"
|
|
5309
|
+
"@openlearn/core:IPointsLedgerService": "pointsLedger",
|
|
5310
|
+
"@openlearn/core:IClassRosterService": "roster"
|
|
4916
5311
|
};
|
|
4917
5312
|
for (const tokenName of skipTokens) {
|
|
4918
5313
|
const serviceKey = TOKEN_TO_SERVICE_KEY[tokenName];
|
|
@@ -5023,9 +5418,9 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
5023
5418
|
`[PluginHost] Plugin "${manifest.id}" cannot resolve "${token}" by name. Use the Token object imported from @openlearn/plugin-sdk, or one of the SDK-exported Token names.`
|
|
5024
5419
|
);
|
|
5025
5420
|
}
|
|
5026
|
-
return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId));
|
|
5421
|
+
return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId, manifest.id));
|
|
5027
5422
|
}
|
|
5028
|
-
return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId));
|
|
5423
|
+
return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId, manifest.id));
|
|
5029
5424
|
},
|
|
5030
5425
|
// V3.2: 插件向 DI 容器提供服务(对应 manifest.provides)
|
|
5031
5426
|
provide: async (token, instance) => {
|
|
@@ -5125,6 +5520,7 @@ var init_context_builder = __esm({
|
|
|
5125
5520
|
init_audit_log();
|
|
5126
5521
|
init_method_policy();
|
|
5127
5522
|
init_errors2();
|
|
5523
|
+
init_inline_db_guard();
|
|
5128
5524
|
init_extension_points();
|
|
5129
5525
|
init_interfaces();
|
|
5130
5526
|
init_plugin_command();
|
|
@@ -5862,7 +6258,7 @@ function isPidAlive(pid) {
|
|
|
5862
6258
|
function writeBootstrapTempFile(code) {
|
|
5863
6259
|
const file = import_node_path11.default.join(
|
|
5864
6260
|
import_node_os.default.tmpdir(),
|
|
5865
|
-
`${BOOTSTRAP_PREFIX}${process.pid}-${
|
|
6261
|
+
`${BOOTSTRAP_PREFIX}${process.pid}-${import_node_crypto6.default.randomBytes(8).toString("hex")}${BOOTSTRAP_SUFFIX}`
|
|
5866
6262
|
);
|
|
5867
6263
|
import_node_fs9.default.writeFileSync(file, code, { encoding: "utf8", mode: 384 });
|
|
5868
6264
|
return file;
|
|
@@ -5925,12 +6321,12 @@ function resolveNetPreloadPath(options = {}) {
|
|
|
5925
6321
|
}
|
|
5926
6322
|
return found;
|
|
5927
6323
|
}
|
|
5928
|
-
var import_node_child_process,
|
|
6324
|
+
var import_node_child_process, import_node_crypto6, import_node_fs9, import_node_os, import_node_path11, import_node_url3, import_meta6, ENV_ALLOWLIST, ENV_DENYLIST, WORKER_DATA_ENV, BOOTSTRAP_PREFIX, BOOTSTRAP_SUFFIX, LEGACY_BOOTSTRAP_STALE_MS;
|
|
5929
6325
|
var init_child_spawn = __esm({
|
|
5930
6326
|
"packages/core/worker-runtime/child-spawn.ts"() {
|
|
5931
6327
|
"use strict";
|
|
5932
6328
|
import_node_child_process = require("node:child_process");
|
|
5933
|
-
|
|
6329
|
+
import_node_crypto6 = __toESM(require("node:crypto"), 1);
|
|
5934
6330
|
import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
5935
6331
|
import_node_os = __toESM(require("node:os"), 1);
|
|
5936
6332
|
import_node_path11 = __toESM(require("node:path"), 1);
|
|
@@ -7518,241 +7914,6 @@ var init_event_forwarder = __esm({
|
|
|
7518
7914
|
}
|
|
7519
7915
|
});
|
|
7520
7916
|
|
|
7521
|
-
// packages/core/plugin-host/sql-guard.ts
|
|
7522
|
-
function sanitizeSql(sql) {
|
|
7523
|
-
let inSingleQuote = false;
|
|
7524
|
-
let inDoubleQuote = false;
|
|
7525
|
-
let inBacktick = false;
|
|
7526
|
-
let inBracket = false;
|
|
7527
|
-
let result = "";
|
|
7528
|
-
const len = sql.length;
|
|
7529
|
-
for (let i = 0; i < len; i++) {
|
|
7530
|
-
const ch = sql[i];
|
|
7531
|
-
const next = i + 1 < len ? sql[i + 1] : "";
|
|
7532
|
-
if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "-" && next === "-") {
|
|
7533
|
-
while (i < len && sql[i] !== "\n") i++;
|
|
7534
|
-
result += " ";
|
|
7535
|
-
continue;
|
|
7536
|
-
}
|
|
7537
|
-
if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "/" && next === "*") {
|
|
7538
|
-
i += 2;
|
|
7539
|
-
while (i < len && !(sql[i] === "*" && i + 1 < len && sql[i + 1] === "/")) {
|
|
7540
|
-
i++;
|
|
7541
|
-
}
|
|
7542
|
-
i++;
|
|
7543
|
-
result += " ";
|
|
7544
|
-
continue;
|
|
7545
|
-
}
|
|
7546
|
-
if (ch === "'" && !inDoubleQuote && !inBacktick && !inBracket) {
|
|
7547
|
-
if (inSingleQuote) {
|
|
7548
|
-
if (next === "'") {
|
|
7549
|
-
i++;
|
|
7550
|
-
} else {
|
|
7551
|
-
inSingleQuote = false;
|
|
7552
|
-
}
|
|
7553
|
-
} else {
|
|
7554
|
-
inSingleQuote = true;
|
|
7555
|
-
}
|
|
7556
|
-
result += " ";
|
|
7557
|
-
continue;
|
|
7558
|
-
}
|
|
7559
|
-
if (inSingleQuote) {
|
|
7560
|
-
result += " ";
|
|
7561
|
-
continue;
|
|
7562
|
-
}
|
|
7563
|
-
if (ch === "[" && !inDoubleQuote && !inBacktick) {
|
|
7564
|
-
inBracket = true;
|
|
7565
|
-
result += " ";
|
|
7566
|
-
continue;
|
|
7567
|
-
}
|
|
7568
|
-
if (ch === "]" && inBracket) {
|
|
7569
|
-
inBracket = false;
|
|
7570
|
-
result += " ";
|
|
7571
|
-
continue;
|
|
7572
|
-
}
|
|
7573
|
-
if (ch === "`" && !inDoubleQuote) {
|
|
7574
|
-
inBacktick = !inBacktick;
|
|
7575
|
-
result += " ";
|
|
7576
|
-
continue;
|
|
7577
|
-
}
|
|
7578
|
-
if (ch === '"' && !inBacktick) {
|
|
7579
|
-
inDoubleQuote = !inDoubleQuote;
|
|
7580
|
-
result += " ";
|
|
7581
|
-
continue;
|
|
7582
|
-
}
|
|
7583
|
-
result += ch;
|
|
7584
|
-
}
|
|
7585
|
-
return result;
|
|
7586
|
-
}
|
|
7587
|
-
function extractFlatTables(flatSql, tables) {
|
|
7588
|
-
const ddlRegex = /\b(?:CREATE|DROP|ALTER)\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?([a-zA-Z_][\w]*)/gi;
|
|
7589
|
-
for (const m of flatSql.matchAll(ddlRegex)) {
|
|
7590
|
-
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
7591
|
-
tables.add(m[1].toLowerCase());
|
|
7592
|
-
}
|
|
7593
|
-
}
|
|
7594
|
-
const renameToRegex = /\bRENAME\s+TO\s+([a-zA-Z_][\w]*)/gi;
|
|
7595
|
-
for (const m of flatSql.matchAll(renameToRegex)) {
|
|
7596
|
-
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
7597
|
-
tables.add(m[1].toLowerCase());
|
|
7598
|
-
}
|
|
7599
|
-
}
|
|
7600
|
-
const simpleClauseRegex = /\b(?:INTO|UPDATE(?:\s+OR\s+(?:ROLLBACK|ABORT|REPLACE|FAIL|IGNORE))?|JOIN)\s+([a-zA-Z_][\w]*)/gi;
|
|
7601
|
-
for (const m of flatSql.matchAll(simpleClauseRegex)) {
|
|
7602
|
-
if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
|
|
7603
|
-
tables.add(m[1].toLowerCase());
|
|
7604
|
-
}
|
|
7605
|
-
}
|
|
7606
|
-
const fromRegex = /\bFROM\s+([^;]+?)(?=\bWHERE\b|\bGROUP\b|\bHAVING\b|\bORDER\b|\bLIMIT\b|\bUNION\b|\bEXCEPT\b|\bINTERSECT\b|\bWINDOW\b|\bVALUES\b|\bSET\b|\bINTO\b|\bJOIN\b|\bLEFT\b|\bRIGHT\b|\bINNER\b|\bOUTER\b|\bCROSS\b|\bNATURAL\b|\bON\b|\bUSING\b|\bSELECT\b|;|$)/gi;
|
|
7607
|
-
for (const match of flatSql.matchAll(fromRegex)) {
|
|
7608
|
-
const clause = match[1];
|
|
7609
|
-
if (!clause) continue;
|
|
7610
|
-
const parts = clause.split(",");
|
|
7611
|
-
for (const part of parts) {
|
|
7612
|
-
const trimmed2 = part.trim();
|
|
7613
|
-
if (!trimmed2) continue;
|
|
7614
|
-
const tokenMatch = /^[a-zA-Z_][\w]*/.exec(trimmed2);
|
|
7615
|
-
if (tokenMatch) {
|
|
7616
|
-
const tableName = tokenMatch[0].toLowerCase();
|
|
7617
|
-
if (tableName !== SUBQUERY_PLACEHOLDER) {
|
|
7618
|
-
tables.add(tableName);
|
|
7619
|
-
}
|
|
7620
|
-
}
|
|
7621
|
-
}
|
|
7622
|
-
}
|
|
7623
|
-
}
|
|
7624
|
-
function extractReferencedTables(cleanSql) {
|
|
7625
|
-
const tables = /* @__PURE__ */ new Set();
|
|
7626
|
-
let working = cleanSql;
|
|
7627
|
-
const parenRegex = /\(([^()]*)\)/g;
|
|
7628
|
-
let depth = 0;
|
|
7629
|
-
while (parenRegex.test(working) && depth++ < 100) {
|
|
7630
|
-
working = working.replace(parenRegex, (_full, inner) => {
|
|
7631
|
-
extractFlatTables(inner, tables);
|
|
7632
|
-
const trimmedInner = inner.trim();
|
|
7633
|
-
if (/^(?:SELECT|VALUES|WITH)\b/i.test(trimmedInner)) {
|
|
7634
|
-
return ` ${SUBQUERY_PLACEHOLDER} `;
|
|
7635
|
-
}
|
|
7636
|
-
return ` ${inner} `;
|
|
7637
|
-
});
|
|
7638
|
-
}
|
|
7639
|
-
working = working.replace(/[()]/g, " ");
|
|
7640
|
-
extractFlatTables(working, tables);
|
|
7641
|
-
return tables;
|
|
7642
|
-
}
|
|
7643
|
-
function assertPluginSqlAllowed(options) {
|
|
7644
|
-
const { sql, ownPrefix, allowedPrefixes = [], researchAllowed = false } = options;
|
|
7645
|
-
if (typeof sql !== "string" || sql.trim() === "") return;
|
|
7646
|
-
for (const opRegex of FORBIDDEN_OPERATIONS) {
|
|
7647
|
-
if (opRegex.test(sql)) {
|
|
7648
|
-
throw new Error(`Dangerous statement is forbidden: "${sql.trim()}"`);
|
|
7649
|
-
}
|
|
7650
|
-
}
|
|
7651
|
-
const clean = sanitizeSql(sql);
|
|
7652
|
-
const tables = extractReferencedTables(clean);
|
|
7653
|
-
if (/\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\b/i.test(clean) && tables.size === 0) {
|
|
7654
|
-
throw new Error(`Worker plugin SQL rejected: unable to resolve target table in "${sql.trim()}"`);
|
|
7655
|
-
}
|
|
7656
|
-
const isDdl = /^\s*(DROP|CREATE|ALTER|TRUNCATE)\b/i.test(clean);
|
|
7657
|
-
const words = clean.toLowerCase().match(/\b[a-zA-Z_][\w]*\b/g) || [];
|
|
7658
|
-
for (const word of words) {
|
|
7659
|
-
if (FORBIDDEN_CORE_TABLES_SET.has(word)) {
|
|
7660
|
-
if (tables.has(word)) {
|
|
7661
|
-
if (isDdl) {
|
|
7662
|
-
throw new Error(
|
|
7663
|
-
`Worker plugin is not permitted to perform DDL on table "${word}" (core security table is strictly forbidden)`
|
|
7664
|
-
);
|
|
7665
|
-
}
|
|
7666
|
-
throw new Error(
|
|
7667
|
-
`Worker plugin is not permitted to access table "${word}" (core security table is strictly forbidden)`
|
|
7668
|
-
);
|
|
7669
|
-
}
|
|
7670
|
-
}
|
|
7671
|
-
}
|
|
7672
|
-
const ownPrefixLower = ownPrefix ? ownPrefix.toLowerCase() : "";
|
|
7673
|
-
const validPrefixes = [ownPrefixLower, ...allowedPrefixes.map((p) => p.toLowerCase())].filter(Boolean);
|
|
7674
|
-
for (const table of tables) {
|
|
7675
|
-
if (table === "plugin_migrations") continue;
|
|
7676
|
-
if (researchAllowed && table.startsWith("plugin_research_")) continue;
|
|
7677
|
-
const isOwn = validPrefixes.some((p) => table.startsWith(p));
|
|
7678
|
-
if (isOwn) continue;
|
|
7679
|
-
if (table === "sqlite_master" || table === "sqlite_schema" || table === "sqlite_temp_master") {
|
|
7680
|
-
const strippedComments = sql.replace(/--.*$/gm, " ").replace(/\/\*[\s\S]*?\*\//g, " ").trim();
|
|
7681
|
-
const isAllowedMetaQuery = /^\s*SELECT\s+name\s+FROM\s+(?:sqlite_master|sqlite_schema)\s+WHERE\s+type\s*=\s*['"]table['"]\s+AND\s+name\s+LIKE\s+['"][A-Za-z0-9_%]+['"]\s*$/i.test(
|
|
7682
|
-
strippedComments
|
|
7683
|
-
) && ownPrefixLower && strippedComments.toLowerCase().includes(ownPrefixLower);
|
|
7684
|
-
if (isAllowedMetaQuery) continue;
|
|
7685
|
-
throw new Error(`Direct access to sqlite metadata table "${table}" is forbidden`);
|
|
7686
|
-
}
|
|
7687
|
-
if (isDdl) {
|
|
7688
|
-
throw new Error(
|
|
7689
|
-
`Worker plugin is not permitted to perform DDL on table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
|
|
7690
|
-
);
|
|
7691
|
-
}
|
|
7692
|
-
throw new Error(
|
|
7693
|
-
`Worker plugin is not permitted to access table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
|
|
7694
|
-
);
|
|
7695
|
-
}
|
|
7696
|
-
}
|
|
7697
|
-
var FORBIDDEN_CORE_TABLES, FORBIDDEN_CORE_TABLES_SET, FORBIDDEN_OPERATIONS, SUBQUERY_PLACEHOLDER;
|
|
7698
|
-
var init_sql_guard = __esm({
|
|
7699
|
-
"packages/core/plugin-host/sql-guard.ts"() {
|
|
7700
|
-
"use strict";
|
|
7701
|
-
FORBIDDEN_CORE_TABLES = Object.freeze([
|
|
7702
|
-
"users",
|
|
7703
|
-
"client_sessions",
|
|
7704
|
-
"plugins",
|
|
7705
|
-
"plugin_storage",
|
|
7706
|
-
"ai_providers",
|
|
7707
|
-
"processes",
|
|
7708
|
-
"pending_commands",
|
|
7709
|
-
"classes",
|
|
7710
|
-
"students",
|
|
7711
|
-
"class_students",
|
|
7712
|
-
"lessons",
|
|
7713
|
-
"assignments",
|
|
7714
|
-
"assignment_submissions",
|
|
7715
|
-
"attendance",
|
|
7716
|
-
"schedules",
|
|
7717
|
-
"class_grade_weights",
|
|
7718
|
-
"exams",
|
|
7719
|
-
"exam_scores",
|
|
7720
|
-
"student_semester_reports",
|
|
7721
|
-
"student_rollcalls",
|
|
7722
|
-
"student_seats",
|
|
7723
|
-
"student_read_notifications",
|
|
7724
|
-
"student_lesson_progress",
|
|
7725
|
-
"courseware",
|
|
7726
|
-
"courseware_attempt",
|
|
7727
|
-
"system_resources",
|
|
7728
|
-
"events",
|
|
7729
|
-
"_migrations",
|
|
7730
|
-
"site_settings",
|
|
7731
|
-
"agent_conversations",
|
|
7732
|
-
"classroom_sessions",
|
|
7733
|
-
"lesson_quiz_submissions",
|
|
7734
|
-
"courseware_score_config",
|
|
7735
|
-
"demo_data_registry",
|
|
7736
|
-
"whiteboard_elements",
|
|
7737
|
-
"student_point_logs",
|
|
7738
|
-
"lianyun_research_activities",
|
|
7739
|
-
"lianyun_research_groups",
|
|
7740
|
-
"lianyun_research_submissions",
|
|
7741
|
-
"lianyun_research_reviews"
|
|
7742
|
-
]);
|
|
7743
|
-
FORBIDDEN_CORE_TABLES_SET = new Set(FORBIDDEN_CORE_TABLES.map((t) => t.toLowerCase()));
|
|
7744
|
-
FORBIDDEN_OPERATIONS = [
|
|
7745
|
-
/\bATTACH(?:\s+DATABASE)?\b/i,
|
|
7746
|
-
/\bDETACH(?:\s+DATABASE)?\b/i,
|
|
7747
|
-
/\bPRAGMA\b/i,
|
|
7748
|
-
/\bVACUUM\b/i,
|
|
7749
|
-
/\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
|
|
7750
|
-
/\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
|
|
7751
|
-
];
|
|
7752
|
-
SUBQUERY_PLACEHOLDER = "__ol_subquery__";
|
|
7753
|
-
}
|
|
7754
|
-
});
|
|
7755
|
-
|
|
7756
7917
|
// packages/core/worker-runtime/service-host.ts
|
|
7757
7918
|
function sanitizeClonableValue(val, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
7758
7919
|
if (depth > 12) return null;
|
|
@@ -7787,12 +7948,12 @@ function researchSqlPluginIds(env = process.env) {
|
|
|
7787
7948
|
}
|
|
7788
7949
|
return /* @__PURE__ */ new Set(["lianyun-course"]);
|
|
7789
7950
|
}
|
|
7790
|
-
var
|
|
7951
|
+
var import_node_crypto7, import_promises2, import_node_net2, CORE_WORKER_TOKENS, STACK_CAP, ServiceHost;
|
|
7791
7952
|
var init_service_host = __esm({
|
|
7792
7953
|
"packages/core/worker-runtime/service-host.ts"() {
|
|
7793
7954
|
"use strict";
|
|
7794
7955
|
init_event_forwarder();
|
|
7795
|
-
|
|
7956
|
+
import_node_crypto7 = __toESM(require("node:crypto"), 1);
|
|
7796
7957
|
import_promises2 = __toESM(require("node:dns/promises"), 1);
|
|
7797
7958
|
import_node_net2 = __toESM(require("node:net"), 1);
|
|
7798
7959
|
init_errors3();
|
|
@@ -8432,7 +8593,7 @@ var init_service_host = __esm({
|
|
|
8432
8593
|
const [event] = msg.args;
|
|
8433
8594
|
const pluginId = this.pluginId || this.pluginActorId.replace(/^plugin:/, "");
|
|
8434
8595
|
const enrichedEvent = {
|
|
8435
|
-
id: event.id || `evt_${Date.now()}_${
|
|
8596
|
+
id: event.id || `evt_${Date.now()}_${import_node_crypto7.default.randomUUID()}`,
|
|
8436
8597
|
timestamp: event.timestamp || Date.now(),
|
|
8437
8598
|
...event,
|
|
8438
8599
|
source: event.source ? `plugin:${pluginId}.${event.source}` : `plugin:${pluginId}`
|
|
@@ -10309,6 +10470,15 @@ var CommandBus = class {
|
|
|
10309
10470
|
hasHandler(commandType) {
|
|
10310
10471
|
return this.handlers.has(commandType) || this.legacyHandlers.has(commandType);
|
|
10311
10472
|
}
|
|
10473
|
+
/**
|
|
10474
|
+
* 只读枚举已注册的命令类型(modern + legacy)。
|
|
10475
|
+
*
|
|
10476
|
+
* 供插件激活后的「handler 有、ActionDescriptor 没有」自检使用(Issue #369):
|
|
10477
|
+
* 自检要能看清「这次激活新增了哪些命令」,而不是去读私有 Map。
|
|
10478
|
+
*/
|
|
10479
|
+
listRegisteredCommandTypes() {
|
|
10480
|
+
return Array.from(/* @__PURE__ */ new Set([...this.handlers.keys(), ...this.legacyHandlers.keys()]));
|
|
10481
|
+
}
|
|
10312
10482
|
async execute(command) {
|
|
10313
10483
|
const actorId = typeof command.actorId === "string" ? command.actorId.trim() : "";
|
|
10314
10484
|
if (!actorId) {
|
|
@@ -10372,6 +10542,14 @@ var ActionRegistry = class {
|
|
|
10372
10542
|
if (this.actions.has(descriptor.id)) {
|
|
10373
10543
|
throw new Error(`Action ${descriptor.id} is already registered.`);
|
|
10374
10544
|
}
|
|
10545
|
+
const owner = this.getActionByCommandType(descriptor.commandType);
|
|
10546
|
+
if (owner) {
|
|
10547
|
+
const message = `Command type "${descriptor.commandType}" is already registered by action ${owner.id}; ${descriptor.id} cannot claim it again.`;
|
|
10548
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
|
10549
|
+
throw new Error(message);
|
|
10550
|
+
}
|
|
10551
|
+
console.warn(`[ActionRegistry] ${message}`);
|
|
10552
|
+
}
|
|
10375
10553
|
this.actions.set(descriptor.id, descriptor);
|
|
10376
10554
|
}
|
|
10377
10555
|
unregister(id) {
|
|
@@ -10403,6 +10581,20 @@ var ActionRegistry = class {
|
|
|
10403
10581
|
getActionByCommandType(commandType) {
|
|
10404
10582
|
return Array.from(this.actions.values()).find((a) => a.commandType === commandType);
|
|
10405
10583
|
}
|
|
10584
|
+
/** 已登记描述符覆盖的 commandType(去重、只读)。 */
|
|
10585
|
+
listRegisteredCommandTypes() {
|
|
10586
|
+
return Array.from(new Set(Array.from(this.actions.values()).map((a) => a.commandType))).sort();
|
|
10587
|
+
}
|
|
10588
|
+
/**
|
|
10589
|
+
* 给定命令类型里**没有** ActionDescriptor 的那些。
|
|
10590
|
+
*
|
|
10591
|
+
* 命令总线是 default-deny:这种命令会在用户点击时被 403 `COMMAND_NOT_REGISTERED` 拒绝。
|
|
10592
|
+
* 插件激活后用它把「注册了 handler 却忘了登记描述符」提前打成一条 WARN(Issue #369)。
|
|
10593
|
+
*/
|
|
10594
|
+
listMissingDescriptors(commandTypes) {
|
|
10595
|
+
const registered = new Set(Array.from(this.actions.values()).map((a) => a.commandType));
|
|
10596
|
+
return Array.from(new Set(commandTypes)).filter((type) => !registered.has(type)).sort();
|
|
10597
|
+
}
|
|
10406
10598
|
};
|
|
10407
10599
|
|
|
10408
10600
|
// packages/core/capability/event/capability-event-bus.ts
|
|
@@ -10670,6 +10862,7 @@ var InvocationEngine = class {
|
|
|
10670
10862
|
};
|
|
10671
10863
|
|
|
10672
10864
|
// packages/core/capability/guard/capability-guard.ts
|
|
10865
|
+
init_catalog();
|
|
10673
10866
|
var CapabilityGuard = class _CapabilityGuard {
|
|
10674
10867
|
constructor() {
|
|
10675
10868
|
this.actorCapabilities = /* @__PURE__ */ new Map();
|
|
@@ -10685,20 +10878,20 @@ var CapabilityGuard = class _CapabilityGuard {
|
|
|
10685
10878
|
]);
|
|
10686
10879
|
this.actorCapabilities.set("anonymous", []);
|
|
10687
10880
|
this.actorCapabilities.set("agent-system-0", [
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10881
|
+
CAP.LESSON_WRITE,
|
|
10882
|
+
CAP.LESSON_DELETE,
|
|
10883
|
+
CAP.WHITEBOARD_WRITE,
|
|
10691
10884
|
"quiz:write",
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10885
|
+
CAP.PLUGIN_READ,
|
|
10886
|
+
CAP.VFS_READ,
|
|
10887
|
+
CAP.VFS_WRITE,
|
|
10888
|
+
CAP.MANAGEMENT_WRITE,
|
|
10889
|
+
CAP.MANAGEMENT_READ,
|
|
10890
|
+
CAP.PROCESS_WRITE,
|
|
10891
|
+
CAP.PROCESS_READ
|
|
10699
10892
|
]);
|
|
10700
10893
|
this.actorCapabilities.set("teacher-demo", ["lesson:*", "whiteboard:*", "management:*", "quiz:*", "vfs:*"]);
|
|
10701
|
-
this.actorCapabilities.set("student-demo", [
|
|
10894
|
+
this.actorCapabilities.set("student-demo", [CAP.STUDENT_WRITE, CAP.LESSON_READ, CAP.WHITEBOARD_READ]);
|
|
10702
10895
|
}
|
|
10703
10896
|
grant(actorId, cap) {
|
|
10704
10897
|
const caps = this.actorCapabilities.get(actorId) || [];
|
|
@@ -10732,11 +10925,11 @@ var CapabilityGuard = class _CapabilityGuard {
|
|
|
10732
10925
|
"vfs:*",
|
|
10733
10926
|
"process:*",
|
|
10734
10927
|
"plugin:*",
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10928
|
+
CAP.STUDENT_WRITE,
|
|
10929
|
+
CAP.ASSIGNMENT_READ,
|
|
10930
|
+
CAP.ASSIGNMENT_SUBMIT,
|
|
10931
|
+
CAP.ASSIGNMENT_REVIEW,
|
|
10932
|
+
CAP.ASSIGNMENT_MANAGE
|
|
10740
10933
|
];
|
|
10741
10934
|
const [reqRes2, reqAct2] = requiredCap.split(":");
|
|
10742
10935
|
if (teacherCaps.some((c) => {
|
|
@@ -10748,12 +10941,12 @@ var CapabilityGuard = class _CapabilityGuard {
|
|
|
10748
10941
|
}
|
|
10749
10942
|
if (role === "student") {
|
|
10750
10943
|
const studentCaps = [
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10944
|
+
CAP.STUDENT_WRITE,
|
|
10945
|
+
CAP.LESSON_READ,
|
|
10946
|
+
CAP.WHITEBOARD_READ,
|
|
10947
|
+
CAP.ASSIGNMENT_READ,
|
|
10948
|
+
CAP.ASSIGNMENT_SUBMIT,
|
|
10949
|
+
CAP.ASSIGNMENT_REVIEW
|
|
10757
10950
|
];
|
|
10758
10951
|
const [reqRes2, reqAct2] = requiredCap.split(":");
|
|
10759
10952
|
if (studentCaps.some((c) => {
|
|
@@ -12092,18 +12285,25 @@ function generateUnambiguousPassword(length = 20) {
|
|
|
12092
12285
|
}
|
|
12093
12286
|
return out;
|
|
12094
12287
|
}
|
|
12288
|
+
function resolveSeedPasswordMode(env = process.env) {
|
|
12289
|
+
return env.OPENLEARN_SEED_PASSWORD_MODE?.trim().toLowerCase() === "random" ? "random" : "default";
|
|
12290
|
+
}
|
|
12095
12291
|
function resolveSeedCredential(username, options) {
|
|
12096
12292
|
if (options?.allowWellKnown) {
|
|
12097
|
-
return { password: username, mustChange: false, generated: false };
|
|
12293
|
+
return { password: username, mustChange: false, generated: false, source: "playwright" };
|
|
12098
12294
|
}
|
|
12099
12295
|
const envValue = options?.envValue?.trim();
|
|
12100
12296
|
if (envValue) {
|
|
12101
|
-
return { password: envValue, mustChange: envValue === username, generated: false };
|
|
12297
|
+
return { password: envValue, mustChange: envValue === username, generated: false, source: "env" };
|
|
12102
12298
|
}
|
|
12103
12299
|
if (options?.vitestFallback) {
|
|
12104
|
-
return { password: `Vitest-${username}-NotDefault9`, mustChange: false, generated: false };
|
|
12300
|
+
return { password: `Vitest-${username}-NotDefault9`, mustChange: false, generated: false, source: "vitest" };
|
|
12301
|
+
}
|
|
12302
|
+
const mode = options?.mode ?? resolveSeedPasswordMode();
|
|
12303
|
+
if (mode === "random") {
|
|
12304
|
+
return { password: generateUnambiguousPassword(20), mustChange: true, generated: true, source: "random" };
|
|
12105
12305
|
}
|
|
12106
|
-
return { password:
|
|
12306
|
+
return { password: username, mustChange: true, generated: false, source: "default" };
|
|
12107
12307
|
}
|
|
12108
12308
|
function resolveBootstrapCredentialDir() {
|
|
12109
12309
|
if (process.env.OPENLEARN_DATA_DIR?.trim()) {
|
|
@@ -12127,6 +12327,11 @@ function persistGeneratedSeedCredentials(lines) {
|
|
|
12127
12327
|
console.warn(`[SECURITY] \u65E0\u6CD5\u5199\u5165 ${filePath}\uFF0C\u8BF7\u4ECE\u4E0A\u65B9\u65E5\u5FD7\u6284\u5F55\u521D\u59CB\u53E3\u4EE4\u3002`, err);
|
|
12128
12328
|
}
|
|
12129
12329
|
}
|
|
12330
|
+
function logDefaultSeedCredentials(usernames) {
|
|
12331
|
+
console.warn(
|
|
12332
|
+
"[SECURITY] \u9996\u6B21\u542F\u52A8\u5DF2\u521B\u5EFA\u79CD\u5B50\u8D26\u53F7\uFF0C\u521D\u59CB\u53E3\u4EE4 = \u7528\u6237\u540D\uFF08\u9996\u6B21\u767B\u5F55\u540E\u5FC5\u987B\u4FEE\u6539\uFF09\uFF1A\n" + usernames.map((name) => ` ${name} / ${name}`).join("\n") + "\n \u6539\u5BC6\u524D\uFF0C\u9664\u300C\u6539\u5BC6 / \u767B\u51FA\u300D\u4EE5\u5916\u7684\u6240\u6709\u63A5\u53E3\u90FD\u4F1A\u88AB\u62D2\u7EDD\uFF08403 FORBIDDEN_DEFAULT_PASSWORD\uFF09\u3002\n \u5982\u9700\u968F\u673A\u521D\u59CB\u53E3\u4EE4\uFF0C\u8BBE\u7F6E OPENLEARN_SEED_PASSWORD_MODE=random \u540E\u91CD\u5EFA\u6570\u636E\u5E93\u3002"
|
|
12333
|
+
);
|
|
12334
|
+
}
|
|
12130
12335
|
function verifyPassword(pwd, storedHash) {
|
|
12131
12336
|
if (storedHash.startsWith("$2")) {
|
|
12132
12337
|
return { valid: import_bcryptjs.default.compareSync(pwd, storedHash), needsUpgrade: false };
|
|
@@ -12240,16 +12445,19 @@ function initializeDatabase(instance) {
|
|
|
12240
12445
|
const countObj = instance.prepare("SELECT COUNT(*) as cnt FROM users").get();
|
|
12241
12446
|
const allowWellKnown = isPlaywrightTestFlag(process.env.PLAYWRIGHT_TEST);
|
|
12242
12447
|
const vitestFallback = Boolean(process.env.VITEST) && !allowWellKnown;
|
|
12448
|
+
const seedMode = resolveSeedPasswordMode();
|
|
12243
12449
|
if (countObj && countObj.cnt === 0) {
|
|
12244
12450
|
const adminCred = resolveSeedCredential("admin", {
|
|
12245
12451
|
envValue: process.env.OPENLEARN_ADMIN_PASSWORD,
|
|
12246
12452
|
allowWellKnown,
|
|
12247
|
-
vitestFallback
|
|
12453
|
+
vitestFallback,
|
|
12454
|
+
mode: seedMode
|
|
12248
12455
|
});
|
|
12249
12456
|
const teacherCred = resolveSeedCredential("teacher", {
|
|
12250
12457
|
envValue: process.env.OPENLEARN_TEACHER_PASSWORD,
|
|
12251
12458
|
allowWellKnown,
|
|
12252
|
-
vitestFallback
|
|
12459
|
+
vitestFallback,
|
|
12460
|
+
mode: seedMode
|
|
12253
12461
|
});
|
|
12254
12462
|
const insertStmt = instance.prepare(
|
|
12255
12463
|
"INSERT INTO users (id, username, password_hash, role, name, created_at, must_change_password) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
@@ -12276,8 +12484,12 @@ function initializeDatabase(instance) {
|
|
|
12276
12484
|
if (adminCred.generated) generated.push(`admin ${adminCred.password}`);
|
|
12277
12485
|
if (teacherCred.generated) generated.push(`teacher ${teacherCred.password}`);
|
|
12278
12486
|
if (generated.length > 0) persistGeneratedSeedCredentials(generated);
|
|
12279
|
-
|
|
12280
|
-
|
|
12487
|
+
const defaulted = [];
|
|
12488
|
+
if (adminCred.source === "default") defaulted.push("admin");
|
|
12489
|
+
if (teacherCred.source === "default") defaulted.push("teacher");
|
|
12490
|
+
if (defaulted.length > 0) logDefaultSeedCredentials(defaulted);
|
|
12491
|
+
} else {
|
|
12492
|
+
const seeded = allowWellKnown ? [] : instance.prepare("SELECT id, username, password_hash FROM users WHERE username IN (?, ?)").all("admin", "teacher");
|
|
12281
12493
|
for (const user of seeded) {
|
|
12282
12494
|
if (!verifyPassword(user.username, user.password_hash).valid) continue;
|
|
12283
12495
|
instance.prepare("UPDATE users SET must_change_password = 1 WHERE id = ?").run(user.id);
|
|
@@ -12745,247 +12957,6 @@ var ProcessPlugin = {
|
|
|
12745
12957
|
// packages/plugins/management.ts
|
|
12746
12958
|
var import_uuid4 = require("uuid");
|
|
12747
12959
|
var import_plugin_sdk3 = require("@openlearn/plugin-sdk");
|
|
12748
|
-
|
|
12749
|
-
// packages/core/db/assignment-grade-writer.ts
|
|
12750
|
-
var import_node_crypto3 = __toESM(require("node:crypto"), 1);
|
|
12751
|
-
var AUTOMATED_SOURCES = /* @__PURE__ */ new Set(["auto", "ai", "system"]);
|
|
12752
|
-
function gradeActorRole(actorId) {
|
|
12753
|
-
const match = /^user:[^:]+:(administrator|admin|teacher|student)$/.exec(String(actorId ?? ""));
|
|
12754
|
-
return match ? match[1] : null;
|
|
12755
|
-
}
|
|
12756
|
-
function isPrivilegedGradeActor(actorId) {
|
|
12757
|
-
const role = gradeActorRole(actorId);
|
|
12758
|
-
return role === "teacher" || role === "administrator" || role === "admin";
|
|
12759
|
-
}
|
|
12760
|
-
function parseScore(raw) {
|
|
12761
|
-
if (raw === null || raw === void 0) return { ok: true, value: null };
|
|
12762
|
-
if (typeof raw === "string" && raw.trim() === "") return { ok: false };
|
|
12763
|
-
const num = typeof raw === "number" ? raw : Number(raw);
|
|
12764
|
-
if (!Number.isFinite(num)) return { ok: false };
|
|
12765
|
-
const rounded = Math.round(num);
|
|
12766
|
-
if (rounded < 0 || rounded > 100) return { ok: false };
|
|
12767
|
-
return { ok: true, value: rounded };
|
|
12768
|
-
}
|
|
12769
|
-
var UPSERT_SUBMISSION = `
|
|
12770
|
-
INSERT INTO assignment_submissions
|
|
12771
|
-
(assignment_id, student_id, content, score, feedback, submitted_at, graded_at, status, source, academic_year, graded_by)
|
|
12772
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
12773
|
-
ON CONFLICT(assignment_id, student_id) DO UPDATE SET
|
|
12774
|
-
content = COALESCE(excluded.content, assignment_submissions.content),
|
|
12775
|
-
score = excluded.score,
|
|
12776
|
-
feedback = COALESCE(excluded.feedback, assignment_submissions.feedback),
|
|
12777
|
-
submitted_at = COALESCE(assignment_submissions.submitted_at, excluded.submitted_at),
|
|
12778
|
-
graded_at = COALESCE(excluded.graded_at, assignment_submissions.graded_at),
|
|
12779
|
-
status = excluded.status,
|
|
12780
|
-
source = excluded.source,
|
|
12781
|
-
academic_year = COALESCE(excluded.academic_year, assignment_submissions.academic_year),
|
|
12782
|
-
graded_by = COALESCE(excluded.graded_by, assignment_submissions.graded_by)
|
|
12783
|
-
`;
|
|
12784
|
-
var INSERT_HISTORY = `
|
|
12785
|
-
INSERT INTO assignment_grade_history
|
|
12786
|
-
(id, assignment_id, student_id, old_score, new_score, old_status, new_status,
|
|
12787
|
-
old_source, new_source, actor_id, actor_role, reason, created_at)
|
|
12788
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
12789
|
-
`;
|
|
12790
|
-
function applyGradeChange(db2, input) {
|
|
12791
|
-
const assignmentId = String(input.assignmentId ?? "").trim();
|
|
12792
|
-
const studentId = String(input.studentId ?? "").trim();
|
|
12793
|
-
if (!assignmentId || !studentId) {
|
|
12794
|
-
return { ok: false, reason: "invalid-target", assignmentId, studentId };
|
|
12795
|
-
}
|
|
12796
|
-
const parsed = parseScore(input.score);
|
|
12797
|
-
if (!parsed.ok) {
|
|
12798
|
-
return { ok: false, reason: "invalid-score", assignmentId, studentId };
|
|
12799
|
-
}
|
|
12800
|
-
const newScore = parsed.value;
|
|
12801
|
-
const newStatus = input.status ?? (newScore === null ? "absent" : "graded");
|
|
12802
|
-
if (newScore === null && newStatus !== "absent") {
|
|
12803
|
-
return { ok: false, reason: "invalid-score", assignmentId, studentId };
|
|
12804
|
-
}
|
|
12805
|
-
const now = input.gradedAt ?? Date.now();
|
|
12806
|
-
const submittedAt = input.submittedAt ?? now;
|
|
12807
|
-
const createIfMissing = input.createIfMissing !== false;
|
|
12808
|
-
const automatedIncoming = AUTOMATED_SOURCES.has(input.source);
|
|
12809
|
-
const protectedIncoming = input.source !== "manual";
|
|
12810
|
-
const actorId = input.actorId ?? (automatedIncoming ? "system" : null);
|
|
12811
|
-
const actorRole = input.actorRole ?? gradeActorRole(actorId) ?? (actorId === "system" ? "system" : null);
|
|
12812
|
-
let result = { ok: false, reason: "invalid-target", assignmentId, studentId };
|
|
12813
|
-
const write = () => {
|
|
12814
|
-
const existing = db2.prepare(
|
|
12815
|
-
`SELECT score, feedback, status, source, academic_year
|
|
12816
|
-
FROM assignment_submissions
|
|
12817
|
-
WHERE assignment_id = ? AND student_id = ?
|
|
12818
|
-
LIMIT 1`
|
|
12819
|
-
).get(assignmentId, studentId);
|
|
12820
|
-
if (!existing && !createIfMissing) {
|
|
12821
|
-
result = { ok: false, reason: "submission-not-found", assignmentId, studentId };
|
|
12822
|
-
return;
|
|
12823
|
-
}
|
|
12824
|
-
if (existing && !input.overrideProtected && protectedIncoming) {
|
|
12825
|
-
if (existing.status === "absent" && newStatus !== "absent") {
|
|
12826
|
-
result = { ok: false, reason: "absent-protected", assignmentId, studentId, oldScore: existing.score };
|
|
12827
|
-
return;
|
|
12828
|
-
}
|
|
12829
|
-
const existingHasGrade = existing.status === "graded" || existing.status === "absent" || existing.score !== null;
|
|
12830
|
-
const existingProtected = existing.source === null || existing.source === "manual" || existing.source === "plugin";
|
|
12831
|
-
if (existingHasGrade && existingProtected && existing.source !== input.source) {
|
|
12832
|
-
result = { ok: false, reason: "manual-protected", assignmentId, studentId, oldScore: existing.score };
|
|
12833
|
-
return;
|
|
12834
|
-
}
|
|
12835
|
-
}
|
|
12836
|
-
const assignment = db2.prepare("SELECT academic_year FROM assignments WHERE id = ?").get(assignmentId);
|
|
12837
|
-
if (!existing && !assignment) {
|
|
12838
|
-
result = { ok: false, reason: "assignment-not-found", assignmentId, studentId };
|
|
12839
|
-
return;
|
|
12840
|
-
}
|
|
12841
|
-
const academicYear = input.academicYear ?? existing?.academic_year ?? assignment?.academic_year ?? null;
|
|
12842
|
-
const gradedBy = actorId ?? null;
|
|
12843
|
-
db2.prepare(UPSERT_SUBMISSION).run(
|
|
12844
|
-
assignmentId,
|
|
12845
|
-
studentId,
|
|
12846
|
-
input.content ?? null,
|
|
12847
|
-
newScore,
|
|
12848
|
-
input.feedback ?? null,
|
|
12849
|
-
submittedAt,
|
|
12850
|
-
now,
|
|
12851
|
-
newStatus,
|
|
12852
|
-
input.source,
|
|
12853
|
-
academicYear,
|
|
12854
|
-
gradedBy
|
|
12855
|
-
);
|
|
12856
|
-
const oldScore = existing?.score ?? null;
|
|
12857
|
-
const oldStatus = existing?.status ?? null;
|
|
12858
|
-
const oldSource = existing?.source ?? null;
|
|
12859
|
-
const scoreChanged = newScore === null ? oldScore !== null : oldScore === null || Number(oldScore) !== newScore;
|
|
12860
|
-
const changed = !existing || scoreChanged || oldStatus !== newStatus || oldSource !== input.source;
|
|
12861
|
-
let historyId;
|
|
12862
|
-
if (changed) {
|
|
12863
|
-
historyId = `agh-${import_node_crypto3.default.randomUUID()}`;
|
|
12864
|
-
db2.prepare(INSERT_HISTORY).run(
|
|
12865
|
-
historyId,
|
|
12866
|
-
assignmentId,
|
|
12867
|
-
studentId,
|
|
12868
|
-
oldScore,
|
|
12869
|
-
newScore,
|
|
12870
|
-
oldStatus,
|
|
12871
|
-
newStatus,
|
|
12872
|
-
oldSource,
|
|
12873
|
-
input.source,
|
|
12874
|
-
actorId,
|
|
12875
|
-
actorRole,
|
|
12876
|
-
input.reason ?? null,
|
|
12877
|
-
now
|
|
12878
|
-
);
|
|
12879
|
-
}
|
|
12880
|
-
result = {
|
|
12881
|
-
ok: true,
|
|
12882
|
-
assignmentId,
|
|
12883
|
-
studentId,
|
|
12884
|
-
historyId,
|
|
12885
|
-
changed,
|
|
12886
|
-
oldScore,
|
|
12887
|
-
newScore
|
|
12888
|
-
};
|
|
12889
|
-
};
|
|
12890
|
-
const tx = typeof db2.transaction === "function" ? db2.transaction(write) : null;
|
|
12891
|
-
if (typeof tx === "function") tx();
|
|
12892
|
-
else write();
|
|
12893
|
-
return result;
|
|
12894
|
-
}
|
|
12895
|
-
|
|
12896
|
-
// packages/core/db/assignment-resubmit.ts
|
|
12897
|
-
var import_node_crypto4 = __toESM(require("node:crypto"), 1);
|
|
12898
|
-
var RESUBMIT_CONTENT_REPLACED_REASON = "resubmit-content-replaced";
|
|
12899
|
-
function keepStatus(current) {
|
|
12900
|
-
return current === "graded" || current === "absent" ? current : "submitted";
|
|
12901
|
-
}
|
|
12902
|
-
function writeContentReplacementHistory(db2, input) {
|
|
12903
|
-
const historyId = `agh-${import_node_crypto4.default.randomUUID()}`;
|
|
12904
|
-
try {
|
|
12905
|
-
db2.prepare(
|
|
12906
|
-
`INSERT INTO assignment_grade_history
|
|
12907
|
-
(id, assignment_id, student_id, old_score, new_score, old_status, new_status,
|
|
12908
|
-
old_source, new_source, actor_id, actor_role, reason, created_at, old_content)
|
|
12909
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
12910
|
-
).run(
|
|
12911
|
-
historyId,
|
|
12912
|
-
input.assignmentId,
|
|
12913
|
-
input.studentId,
|
|
12914
|
-
input.score,
|
|
12915
|
-
input.score,
|
|
12916
|
-
input.status,
|
|
12917
|
-
input.status,
|
|
12918
|
-
input.source,
|
|
12919
|
-
input.source,
|
|
12920
|
-
input.actorId ?? null,
|
|
12921
|
-
input.actorRole ?? null,
|
|
12922
|
-
RESUBMIT_CONTENT_REPLACED_REASON,
|
|
12923
|
-
input.now,
|
|
12924
|
-
input.oldContent
|
|
12925
|
-
);
|
|
12926
|
-
return historyId;
|
|
12927
|
-
} catch (err) {
|
|
12928
|
-
console.warn("[assignment.resubmit] \u5185\u5BB9\u66FF\u6362\u7559\u75D5\u5931\u8D25:", err.message);
|
|
12929
|
-
return void 0;
|
|
12930
|
-
}
|
|
12931
|
-
}
|
|
12932
|
-
function applyResubmission(db2, input) {
|
|
12933
|
-
const assignmentId = String(input.assignmentId ?? "").trim();
|
|
12934
|
-
const studentId = String(input.studentId ?? "").trim();
|
|
12935
|
-
if (!assignmentId || !studentId) {
|
|
12936
|
-
return { ok: false, contentReplaced: false };
|
|
12937
|
-
}
|
|
12938
|
-
const now = input.now ?? Date.now();
|
|
12939
|
-
const content = input.content ?? "";
|
|
12940
|
-
let result = { ok: false, contentReplaced: false };
|
|
12941
|
-
const write = () => {
|
|
12942
|
-
const existing = db2.prepare(
|
|
12943
|
-
`SELECT content, score, feedback, status, source, academic_year
|
|
12944
|
-
FROM assignment_submissions
|
|
12945
|
-
WHERE assignment_id = ? AND student_id = ?
|
|
12946
|
-
LIMIT 1`
|
|
12947
|
-
).get(assignmentId, studentId);
|
|
12948
|
-
if (!existing) {
|
|
12949
|
-
db2.prepare(
|
|
12950
|
-
`INSERT INTO assignment_submissions
|
|
12951
|
-
(assignment_id, student_id, content, score, feedback, submitted_at, status)
|
|
12952
|
-
VALUES (?, ?, ?, NULL, NULL, ?, 'submitted')`
|
|
12953
|
-
).run(assignmentId, studentId, content, now);
|
|
12954
|
-
result = { ok: true, contentReplaced: false, status: "submitted" };
|
|
12955
|
-
return;
|
|
12956
|
-
}
|
|
12957
|
-
const nextStatus = keepStatus(existing.status);
|
|
12958
|
-
const contentChanged = (existing.content ?? "") !== content;
|
|
12959
|
-
const gradedRow = existing.status === "graded" || existing.status === "absent";
|
|
12960
|
-
const contentReplaced = gradedRow && contentChanged;
|
|
12961
|
-
let historyId;
|
|
12962
|
-
if (contentReplaced) {
|
|
12963
|
-
historyId = writeContentReplacementHistory(db2, {
|
|
12964
|
-
assignmentId,
|
|
12965
|
-
studentId,
|
|
12966
|
-
oldContent: existing.content,
|
|
12967
|
-
score: existing.score,
|
|
12968
|
-
status: existing.status,
|
|
12969
|
-
source: existing.source,
|
|
12970
|
-
actorId: input.actorId ?? null,
|
|
12971
|
-
actorRole: input.actorRole ?? null,
|
|
12972
|
-
now
|
|
12973
|
-
});
|
|
12974
|
-
}
|
|
12975
|
-
db2.prepare(
|
|
12976
|
-
`UPDATE assignment_submissions
|
|
12977
|
-
SET content = ?, submitted_at = ?, status = ?
|
|
12978
|
-
WHERE assignment_id = ? AND student_id = ?`
|
|
12979
|
-
).run(content, now, nextStatus, assignmentId, studentId);
|
|
12980
|
-
result = { ok: true, contentReplaced, historyId, status: nextStatus };
|
|
12981
|
-
};
|
|
12982
|
-
const tx = typeof db2.transaction === "function" ? db2.transaction(write) : null;
|
|
12983
|
-
if (typeof tx === "function") tx();
|
|
12984
|
-
else write();
|
|
12985
|
-
return result;
|
|
12986
|
-
}
|
|
12987
|
-
|
|
12988
|
-
// packages/plugins/management.ts
|
|
12989
12960
|
function parseUserActor(actorId) {
|
|
12990
12961
|
const match = /^user:([^:]+):(administrator|admin|teacher|student)$/.exec(String(actorId || ""));
|
|
12991
12962
|
if (!match) return null;
|
|
@@ -13518,137 +13489,6 @@ ${sampleRow}`;
|
|
|
13518
13489
|
return { success: true, studentId: payload.studentId, category: payload.category, private_notes: serialized };
|
|
13519
13490
|
}
|
|
13520
13491
|
});
|
|
13521
|
-
const assignmentCreateCmd = "assignment.create";
|
|
13522
|
-
await actionRegistry.register({
|
|
13523
|
-
id: "core-assignment-create",
|
|
13524
|
-
commandType: assignmentCreateCmd,
|
|
13525
|
-
description: "\u4E3A\u6307\u5B9A\u73ED\u7EA7\u521B\u5EFA\u8BFE\u540E\u4F5C\u4E1A",
|
|
13526
|
-
capabilityRequired: "management:write",
|
|
13527
|
-
inputSchema: {
|
|
13528
|
-
type: "OBJECT",
|
|
13529
|
-
properties: {
|
|
13530
|
-
classId: { type: "STRING", description: "\u76EE\u6807\u73ED\u7EA7 ID" },
|
|
13531
|
-
title: { type: "STRING", description: "\u4F5C\u4E1A\u6807\u9898" },
|
|
13532
|
-
description: { type: "STRING", description: "\u4F5C\u4E1A\u7684\u7B80\u77ED\u6458\u8981\u6216\u6982\u8FF0" },
|
|
13533
|
-
content: { type: "STRING", description: "\u8BE6\u7EC6\u7684\u4F5C\u4E1A\u8BF4\u660E\u6216\u8981\u6C42" }
|
|
13534
|
-
},
|
|
13535
|
-
required: ["classId", "title"]
|
|
13536
|
-
}
|
|
13537
|
-
});
|
|
13538
|
-
await commandBus.registerHandler(assignmentCreateCmd, {
|
|
13539
|
-
async execute(command) {
|
|
13540
|
-
const payload = command.payload;
|
|
13541
|
-
const assignmentId = (0, import_uuid4.v7)();
|
|
13542
|
-
db2.prepare(
|
|
13543
|
-
"INSERT INTO assignments (id, class_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
13544
|
-
).run(
|
|
13545
|
-
assignmentId,
|
|
13546
|
-
payload.classId,
|
|
13547
|
-
payload.title,
|
|
13548
|
-
payload.description || "",
|
|
13549
|
-
payload.content || "",
|
|
13550
|
-
Date.now()
|
|
13551
|
-
);
|
|
13552
|
-
return { assignmentId, classId: payload.classId, title: payload.title };
|
|
13553
|
-
}
|
|
13554
|
-
});
|
|
13555
|
-
const assignmentSubmitCmd = "assignment.submit";
|
|
13556
|
-
await actionRegistry.register({
|
|
13557
|
-
id: "core-assignment-submit",
|
|
13558
|
-
commandType: assignmentSubmitCmd,
|
|
13559
|
-
description: "\u4EE3\u8868\u5B66\u751F\u63D0\u4EA4\u4F5C\u4E1A\u5185\u5BB9\uFF08\u6587\u672C\u3001Markdown \u6216\u4EE3\u7801\uFF09",
|
|
13560
|
-
capabilityRequired: "management:write",
|
|
13561
|
-
inputSchema: {
|
|
13562
|
-
type: "OBJECT",
|
|
13563
|
-
properties: {
|
|
13564
|
-
assignmentId: { type: "STRING", description: "\u4F5C\u4E1A ID" },
|
|
13565
|
-
studentId: { type: "STRING", description: "\u5B66\u751F ID" },
|
|
13566
|
-
content: { type: "STRING", description: "\u63D0\u4EA4\u5185\u5BB9\uFF08\u6587\u672C\u3001Markdown \u6216\u4EE3\u7801\uFF09" }
|
|
13567
|
-
},
|
|
13568
|
-
required: ["assignmentId", "studentId", "content"]
|
|
13569
|
-
}
|
|
13570
|
-
});
|
|
13571
|
-
await commandBus.registerHandler(assignmentSubmitCmd, {
|
|
13572
|
-
async execute(command) {
|
|
13573
|
-
const payload = command.payload;
|
|
13574
|
-
const result = applyResubmission(db2, {
|
|
13575
|
-
assignmentId: payload.assignmentId,
|
|
13576
|
-
studentId: payload.studentId,
|
|
13577
|
-
content: payload.content,
|
|
13578
|
-
actorId: `user:${payload.studentId}:student`,
|
|
13579
|
-
actorRole: "student"
|
|
13580
|
-
});
|
|
13581
|
-
return {
|
|
13582
|
-
success: true,
|
|
13583
|
-
assignmentId: payload.assignmentId,
|
|
13584
|
-
studentId: payload.studentId,
|
|
13585
|
-
/** 本次重交是否替换了已批改行的作答内容(内容替换已记入审计历史)。 */
|
|
13586
|
-
contentReplaced: result.contentReplaced
|
|
13587
|
-
};
|
|
13588
|
-
}
|
|
13589
|
-
});
|
|
13590
|
-
const assignmentGradeCmd = "assignment.grade";
|
|
13591
|
-
await actionRegistry.register({
|
|
13592
|
-
id: "core-assignment-grade",
|
|
13593
|
-
commandType: assignmentGradeCmd,
|
|
13594
|
-
description: "\u4E3A\u5B66\u751F\u4F5C\u4E1A\u6253\u5206\u5E76\u7ED9\u51FA\u53CD\u9988\u610F\u89C1",
|
|
13595
|
-
capabilityRequired: "management:write",
|
|
13596
|
-
inputSchema: {
|
|
13597
|
-
type: "OBJECT",
|
|
13598
|
-
properties: {
|
|
13599
|
-
assignmentId: { type: "STRING", description: "ID of the assignment" },
|
|
13600
|
-
studentId: { type: "STRING", description: "ID of the student" },
|
|
13601
|
-
score: { type: "INTEGER", description: "\u6EE1\u5206 100 \u7684\u5F97\u5206" },
|
|
13602
|
-
feedback: { type: "STRING", description: "\u6709\u5EFA\u8BBE\u6027\u7684\u53CD\u9988\u8BC4\u8BED" }
|
|
13603
|
-
},
|
|
13604
|
-
required: ["assignmentId", "studentId", "score"]
|
|
13605
|
-
}
|
|
13606
|
-
});
|
|
13607
|
-
await commandBus.registerHandler(assignmentGradeCmd, {
|
|
13608
|
-
async execute(command) {
|
|
13609
|
-
const payload = command.payload;
|
|
13610
|
-
const actorId = command.actorId || null;
|
|
13611
|
-
const result = applyGradeChange(db2, {
|
|
13612
|
-
assignmentId: payload.assignmentId,
|
|
13613
|
-
studentId: payload.studentId,
|
|
13614
|
-
score: payload.score,
|
|
13615
|
-
feedback: payload.feedback || "",
|
|
13616
|
-
status: "graded",
|
|
13617
|
-
source: "manual",
|
|
13618
|
-
actorId,
|
|
13619
|
-
reason: "management-grade",
|
|
13620
|
-
createIfMissing: true,
|
|
13621
|
-
overrideProtected: isPrivilegedGradeActor(actorId)
|
|
13622
|
-
});
|
|
13623
|
-
if (!result.ok) {
|
|
13624
|
-
const err = new Error(
|
|
13625
|
-
result.reason === "invalid-score" ? "\u6210\u7EE9\u5FC5\u987B\u662F 0~100 \u7684\u6570\u503C" : result.reason === "assignment-not-found" ? "\u4F5C\u4E1A\u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u5F55\u5165\u6210\u7EE9" : "\u6210\u7EE9\u5199\u5165\u88AB\u62D2\u7EDD"
|
|
13626
|
-
);
|
|
13627
|
-
err.reason = result.reason;
|
|
13628
|
-
throw err;
|
|
13629
|
-
}
|
|
13630
|
-
await eventBus.publish({
|
|
13631
|
-
id: (0, import_uuid4.v7)(),
|
|
13632
|
-
type: "assignment.graded",
|
|
13633
|
-
source: "management.student",
|
|
13634
|
-
payload: {
|
|
13635
|
-
assignmentId: payload.assignmentId,
|
|
13636
|
-
studentId: payload.studentId,
|
|
13637
|
-
score: result.newScore,
|
|
13638
|
-
feedback: payload.feedback || ""
|
|
13639
|
-
},
|
|
13640
|
-
timestamp: Date.now(),
|
|
13641
|
-
correlationId: command.id
|
|
13642
|
-
});
|
|
13643
|
-
return {
|
|
13644
|
-
success: true,
|
|
13645
|
-
assignmentId: payload.assignmentId,
|
|
13646
|
-
studentId: payload.studentId,
|
|
13647
|
-
score: result.newScore,
|
|
13648
|
-
historyId: result.historyId
|
|
13649
|
-
};
|
|
13650
|
-
}
|
|
13651
|
-
});
|
|
13652
13492
|
const scheduleCreateCmd = "schedule.create";
|
|
13653
13493
|
await actionRegistry.register({
|
|
13654
13494
|
id: "core-schedule-create",
|
|
@@ -14480,10 +14320,10 @@ function parseScoreFields(fields) {
|
|
|
14480
14320
|
if (typeof fields !== "string" || !fields.trim()) return [];
|
|
14481
14321
|
return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
|
|
14482
14322
|
}
|
|
14483
|
-
function getNested(raw,
|
|
14323
|
+
function getNested(raw, path48) {
|
|
14484
14324
|
if (raw === null || raw === void 0) return void 0;
|
|
14485
|
-
if (!
|
|
14486
|
-
const parts = String(
|
|
14325
|
+
if (!path48) return void 0;
|
|
14326
|
+
const parts = String(path48).split(".").map((p) => p.trim()).filter(Boolean);
|
|
14487
14327
|
let cursor = raw;
|
|
14488
14328
|
for (let i = 0; i < parts.length; i += 1) {
|
|
14489
14329
|
if (cursor === null || cursor === void 0) return void 0;
|
|
@@ -15284,6 +15124,45 @@ var BuiltinPlugin = {
|
|
|
15284
15124
|
"page_meta"
|
|
15285
15125
|
// 分页与大纲元数据
|
|
15286
15126
|
];
|
|
15127
|
+
const normalizePluginElementData = (dataObj) => {
|
|
15128
|
+
const pluginId = typeof dataObj.pluginId === "string" ? dataObj.pluginId.trim() : "";
|
|
15129
|
+
if (!pluginId) {
|
|
15130
|
+
console.warn("[Whiteboard] plugin \u5143\u7D20\u672A\u58F0\u660E pluginId\uFF0C\u5C06\u53EA\u80FD\u6309 widgetId \u67E5\u627E\u7EC4\u4EF6\uFF1B\u5EFA\u8BAE\u8865\u4E0A manifest id\u3002");
|
|
15131
|
+
}
|
|
15132
|
+
const teacher = typeof dataObj.teacherWidgetId === "string" ? dataObj.teacherWidgetId.trim() : "";
|
|
15133
|
+
const student = typeof dataObj.studentWidgetId === "string" ? dataObj.studentWidgetId.trim() : "";
|
|
15134
|
+
if (!teacher && !student) {
|
|
15135
|
+
throw new Error(
|
|
15136
|
+
"plugin \u5143\u7D20\u5FC5\u987B\u81F3\u5C11\u58F0\u660E teacherWidgetId \u6216 studentWidgetId \u4E4B\u4E00\uFF0C\u5426\u5219\u6559\u5E08\u7AEF / \u5B66\u751F\u7AEF\u90FD\u4F1A\u505C\u5728\u52A0\u8F7D\u6001\u3002"
|
|
15137
|
+
);
|
|
15138
|
+
}
|
|
15139
|
+
if (!teacher) {
|
|
15140
|
+
dataObj.teacherWidgetId = student;
|
|
15141
|
+
console.warn(`[Whiteboard] plugin \u5143\u7D20\u53EA\u58F0\u660E\u4E86 studentWidgetId\uFF0C\u5DF2\u56DE\u9000 teacherWidgetId="${student}"\u3002`);
|
|
15142
|
+
} else if (!student) {
|
|
15143
|
+
dataObj.studentWidgetId = teacher;
|
|
15144
|
+
console.warn(`[Whiteboard] plugin \u5143\u7D20\u53EA\u58F0\u660E\u4E86 teacherWidgetId\uFF0C\u5DF2\u56DE\u9000 studentWidgetId="${teacher}"\u3002`);
|
|
15145
|
+
}
|
|
15146
|
+
};
|
|
15147
|
+
const buildElementData = (type, rawData, segmentId, page) => {
|
|
15148
|
+
let dataObj = null;
|
|
15149
|
+
try {
|
|
15150
|
+
const parsed = JSON.parse(rawData);
|
|
15151
|
+
dataObj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
15152
|
+
} catch {
|
|
15153
|
+
if (type === "plugin") {
|
|
15154
|
+
throw new Error(
|
|
15155
|
+
'plugin \u5143\u7D20\u7684 data \u5FC5\u987B\u662F\u5408\u6CD5 JSON \u5B57\u7B26\u4E32\uFF08\u4F8B\u5982 {"pluginId":"ext-x","teacherWidgetId":"w"}\uFF09\u3002'
|
|
15156
|
+
);
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
if (!dataObj) return rawData;
|
|
15160
|
+
if (segmentId && !dataObj.segmentId) dataObj.segmentId = segmentId;
|
|
15161
|
+
if (page !== void 0 && dataObj.page === void 0) dataObj.page = page;
|
|
15162
|
+
else if (dataObj.page === void 0) dataObj.page = 0;
|
|
15163
|
+
if (type === "plugin") normalizePluginElementData(dataObj);
|
|
15164
|
+
return JSON.stringify(dataObj);
|
|
15165
|
+
};
|
|
15287
15166
|
const isAllowedElementType = (type) => {
|
|
15288
15167
|
if (!type || typeof type !== "string") return false;
|
|
15289
15168
|
if (KNOWN_ELEMENT_TYPES2.includes(type)) return true;
|
|
@@ -15332,20 +15211,7 @@ var BuiltinPlugin = {
|
|
|
15332
15211
|
throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${payload.type}"\u3002${hint}`);
|
|
15333
15212
|
}
|
|
15334
15213
|
const elementId = (0, import_uuid7.v7)();
|
|
15335
|
-
|
|
15336
|
-
try {
|
|
15337
|
-
const dataObj = JSON.parse(dataStr);
|
|
15338
|
-
if (payload.segmentId && !dataObj.segmentId) {
|
|
15339
|
-
dataObj.segmentId = payload.segmentId;
|
|
15340
|
-
}
|
|
15341
|
-
if (payload.page !== void 0 && dataObj.page === void 0) {
|
|
15342
|
-
dataObj.page = payload.page;
|
|
15343
|
-
} else if (dataObj.page === void 0) {
|
|
15344
|
-
dataObj.page = 0;
|
|
15345
|
-
}
|
|
15346
|
-
dataStr = JSON.stringify(dataObj);
|
|
15347
|
-
} catch {
|
|
15348
|
-
}
|
|
15214
|
+
const dataStr = buildElementData(payload.type, payload.data, payload.segmentId, payload.page);
|
|
15349
15215
|
const stmt = db2.prepare(
|
|
15350
15216
|
"INSERT INTO whiteboard_elements (id, lesson_id, type, data, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
15351
15217
|
);
|
|
@@ -15574,15 +15440,7 @@ var BuiltinPlugin = {
|
|
|
15574
15440
|
throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${el.type}"\u3002\u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}`);
|
|
15575
15441
|
}
|
|
15576
15442
|
const elementId = (0, import_uuid7.v7)();
|
|
15577
|
-
|
|
15578
|
-
try {
|
|
15579
|
-
const dataObj = JSON.parse(dataStr);
|
|
15580
|
-
if (el.segmentId && !dataObj.segmentId) dataObj.segmentId = el.segmentId;
|
|
15581
|
-
if (el.page !== void 0 && dataObj.page === void 0) dataObj.page = el.page;
|
|
15582
|
-
else if (dataObj.page === void 0) dataObj.page = 0;
|
|
15583
|
-
dataStr = JSON.stringify(dataObj);
|
|
15584
|
-
} catch {
|
|
15585
|
-
}
|
|
15443
|
+
const dataStr = buildElementData(el.type, el.data, el.segmentId, el.page);
|
|
15586
15444
|
insertStmt.run(elementId, payload.lessonId, el.type, dataStr, Date.now());
|
|
15587
15445
|
elementIds.push(elementId);
|
|
15588
15446
|
}
|
|
@@ -16683,6 +16541,155 @@ var BuiltinPlugin = {
|
|
|
16683
16541
|
// packages/plugins/ai-planner.ts
|
|
16684
16542
|
var import_uuid8 = require("uuid");
|
|
16685
16543
|
var import_plugin_sdk5 = require("@openlearn/plugin-sdk");
|
|
16544
|
+
|
|
16545
|
+
// packages/core/db/assignment-grade-writer.ts
|
|
16546
|
+
var import_node_crypto3 = __toESM(require("node:crypto"), 1);
|
|
16547
|
+
var AUTOMATED_SOURCES = /* @__PURE__ */ new Set(["auto", "ai", "system"]);
|
|
16548
|
+
function gradeActorRole(actorId) {
|
|
16549
|
+
const match = /^user:[^:]+:(administrator|admin|teacher|student)$/.exec(String(actorId ?? ""));
|
|
16550
|
+
return match ? match[1] : null;
|
|
16551
|
+
}
|
|
16552
|
+
function isPrivilegedGradeActor(actorId) {
|
|
16553
|
+
const role = gradeActorRole(actorId);
|
|
16554
|
+
return role === "teacher" || role === "administrator" || role === "admin";
|
|
16555
|
+
}
|
|
16556
|
+
function parseScore(raw) {
|
|
16557
|
+
if (raw === null || raw === void 0) return { ok: true, value: null };
|
|
16558
|
+
if (typeof raw === "string" && raw.trim() === "") return { ok: false };
|
|
16559
|
+
const num = typeof raw === "number" ? raw : Number(raw);
|
|
16560
|
+
if (!Number.isFinite(num)) return { ok: false };
|
|
16561
|
+
const rounded = Math.round(num);
|
|
16562
|
+
if (rounded < 0 || rounded > 100) return { ok: false };
|
|
16563
|
+
return { ok: true, value: rounded };
|
|
16564
|
+
}
|
|
16565
|
+
var UPSERT_SUBMISSION = `
|
|
16566
|
+
INSERT INTO assignment_submissions
|
|
16567
|
+
(assignment_id, student_id, content, score, feedback, submitted_at, graded_at, status, source, academic_year, graded_by)
|
|
16568
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16569
|
+
ON CONFLICT(assignment_id, student_id) DO UPDATE SET
|
|
16570
|
+
content = COALESCE(excluded.content, assignment_submissions.content),
|
|
16571
|
+
score = excluded.score,
|
|
16572
|
+
feedback = COALESCE(excluded.feedback, assignment_submissions.feedback),
|
|
16573
|
+
submitted_at = COALESCE(assignment_submissions.submitted_at, excluded.submitted_at),
|
|
16574
|
+
graded_at = COALESCE(excluded.graded_at, assignment_submissions.graded_at),
|
|
16575
|
+
status = excluded.status,
|
|
16576
|
+
source = excluded.source,
|
|
16577
|
+
academic_year = COALESCE(excluded.academic_year, assignment_submissions.academic_year),
|
|
16578
|
+
graded_by = COALESCE(excluded.graded_by, assignment_submissions.graded_by)
|
|
16579
|
+
`;
|
|
16580
|
+
var INSERT_HISTORY = `
|
|
16581
|
+
INSERT INTO assignment_grade_history
|
|
16582
|
+
(id, assignment_id, student_id, old_score, new_score, old_status, new_status,
|
|
16583
|
+
old_source, new_source, actor_id, actor_role, reason, created_at)
|
|
16584
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16585
|
+
`;
|
|
16586
|
+
function applyGradeChange(db2, input) {
|
|
16587
|
+
const assignmentId = String(input.assignmentId ?? "").trim();
|
|
16588
|
+
const studentId = String(input.studentId ?? "").trim();
|
|
16589
|
+
if (!assignmentId || !studentId) {
|
|
16590
|
+
return { ok: false, reason: "invalid-target", assignmentId, studentId };
|
|
16591
|
+
}
|
|
16592
|
+
const parsed = parseScore(input.score);
|
|
16593
|
+
if (!parsed.ok) {
|
|
16594
|
+
return { ok: false, reason: "invalid-score", assignmentId, studentId };
|
|
16595
|
+
}
|
|
16596
|
+
const newScore = parsed.value;
|
|
16597
|
+
const newStatus = input.status ?? (newScore === null ? "absent" : "graded");
|
|
16598
|
+
if (newScore === null && newStatus !== "absent") {
|
|
16599
|
+
return { ok: false, reason: "invalid-score", assignmentId, studentId };
|
|
16600
|
+
}
|
|
16601
|
+
const now = input.gradedAt ?? Date.now();
|
|
16602
|
+
const submittedAt = input.submittedAt ?? now;
|
|
16603
|
+
const createIfMissing = input.createIfMissing !== false;
|
|
16604
|
+
const automatedIncoming = AUTOMATED_SOURCES.has(input.source);
|
|
16605
|
+
const protectedIncoming = input.source !== "manual";
|
|
16606
|
+
const actorId = input.actorId ?? (automatedIncoming ? "system" : null);
|
|
16607
|
+
const actorRole = input.actorRole ?? gradeActorRole(actorId) ?? (actorId === "system" ? "system" : null);
|
|
16608
|
+
let result = { ok: false, reason: "invalid-target", assignmentId, studentId };
|
|
16609
|
+
const write = () => {
|
|
16610
|
+
const existing = db2.prepare(
|
|
16611
|
+
`SELECT score, feedback, status, source, academic_year
|
|
16612
|
+
FROM assignment_submissions
|
|
16613
|
+
WHERE assignment_id = ? AND student_id = ?
|
|
16614
|
+
LIMIT 1`
|
|
16615
|
+
).get(assignmentId, studentId);
|
|
16616
|
+
if (!existing && !createIfMissing) {
|
|
16617
|
+
result = { ok: false, reason: "submission-not-found", assignmentId, studentId };
|
|
16618
|
+
return;
|
|
16619
|
+
}
|
|
16620
|
+
if (existing && !input.overrideProtected && protectedIncoming) {
|
|
16621
|
+
if (existing.status === "absent" && newStatus !== "absent") {
|
|
16622
|
+
result = { ok: false, reason: "absent-protected", assignmentId, studentId, oldScore: existing.score };
|
|
16623
|
+
return;
|
|
16624
|
+
}
|
|
16625
|
+
const existingHasGrade = existing.status === "graded" || existing.status === "absent" || existing.score !== null;
|
|
16626
|
+
const existingProtected = existing.source === null || existing.source === "manual" || existing.source === "plugin";
|
|
16627
|
+
if (existingHasGrade && existingProtected && existing.source !== input.source) {
|
|
16628
|
+
result = { ok: false, reason: "manual-protected", assignmentId, studentId, oldScore: existing.score };
|
|
16629
|
+
return;
|
|
16630
|
+
}
|
|
16631
|
+
}
|
|
16632
|
+
const assignment = db2.prepare("SELECT academic_year FROM assignments WHERE id = ?").get(assignmentId);
|
|
16633
|
+
if (!existing && !assignment) {
|
|
16634
|
+
result = { ok: false, reason: "assignment-not-found", assignmentId, studentId };
|
|
16635
|
+
return;
|
|
16636
|
+
}
|
|
16637
|
+
const academicYear = input.academicYear ?? existing?.academic_year ?? assignment?.academic_year ?? null;
|
|
16638
|
+
const gradedBy = actorId ?? null;
|
|
16639
|
+
db2.prepare(UPSERT_SUBMISSION).run(
|
|
16640
|
+
assignmentId,
|
|
16641
|
+
studentId,
|
|
16642
|
+
input.content ?? null,
|
|
16643
|
+
newScore,
|
|
16644
|
+
input.feedback ?? null,
|
|
16645
|
+
submittedAt,
|
|
16646
|
+
now,
|
|
16647
|
+
newStatus,
|
|
16648
|
+
input.source,
|
|
16649
|
+
academicYear,
|
|
16650
|
+
gradedBy
|
|
16651
|
+
);
|
|
16652
|
+
const oldScore = existing?.score ?? null;
|
|
16653
|
+
const oldStatus = existing?.status ?? null;
|
|
16654
|
+
const oldSource = existing?.source ?? null;
|
|
16655
|
+
const scoreChanged = newScore === null ? oldScore !== null : oldScore === null || Number(oldScore) !== newScore;
|
|
16656
|
+
const changed = !existing || scoreChanged || oldStatus !== newStatus || oldSource !== input.source;
|
|
16657
|
+
let historyId;
|
|
16658
|
+
if (changed) {
|
|
16659
|
+
historyId = `agh-${import_node_crypto3.default.randomUUID()}`;
|
|
16660
|
+
db2.prepare(INSERT_HISTORY).run(
|
|
16661
|
+
historyId,
|
|
16662
|
+
assignmentId,
|
|
16663
|
+
studentId,
|
|
16664
|
+
oldScore,
|
|
16665
|
+
newScore,
|
|
16666
|
+
oldStatus,
|
|
16667
|
+
newStatus,
|
|
16668
|
+
oldSource,
|
|
16669
|
+
input.source,
|
|
16670
|
+
actorId,
|
|
16671
|
+
actorRole,
|
|
16672
|
+
input.reason ?? null,
|
|
16673
|
+
now
|
|
16674
|
+
);
|
|
16675
|
+
}
|
|
16676
|
+
result = {
|
|
16677
|
+
ok: true,
|
|
16678
|
+
assignmentId,
|
|
16679
|
+
studentId,
|
|
16680
|
+
historyId,
|
|
16681
|
+
changed,
|
|
16682
|
+
oldScore,
|
|
16683
|
+
newScore
|
|
16684
|
+
};
|
|
16685
|
+
};
|
|
16686
|
+
const tx = typeof db2.transaction === "function" ? db2.transaction(write) : null;
|
|
16687
|
+
if (typeof tx === "function") tx();
|
|
16688
|
+
else write();
|
|
16689
|
+
return result;
|
|
16690
|
+
}
|
|
16691
|
+
|
|
16692
|
+
// packages/plugins/ai-planner.ts
|
|
16686
16693
|
var AiPlannerPlugin = {
|
|
16687
16694
|
manifest: {
|
|
16688
16695
|
id: "@openlearn/plugin-ai-planner",
|
|
@@ -17319,11 +17326,6 @@ var AssignmentEvalPlugin = {
|
|
|
17319
17326
|
for (const id of OWNED_ACTION_IDS) {
|
|
17320
17327
|
actionRegistry.unregister(id);
|
|
17321
17328
|
}
|
|
17322
|
-
commandBus.unregisterHandler("assignment.submit");
|
|
17323
|
-
commandBus.unregisterHandler("assignment.grade");
|
|
17324
|
-
actionRegistry.unregister("core-assignment-create");
|
|
17325
|
-
actionRegistry.unregister("core-assignment-submit");
|
|
17326
|
-
actionRegistry.unregister("core-assignment-grade");
|
|
17327
17329
|
const loadAssignment = (assignmentId) => db2.prepare("SELECT * FROM plugin_assignments WHERE id = ?").get(assignmentId);
|
|
17328
17330
|
const resolveAssignment = (assignmentId, lessonId) => {
|
|
17329
17331
|
if (assignmentId) {
|
|
@@ -18262,6 +18264,40 @@ var SemesterGradeService = class {
|
|
|
18262
18264
|
}
|
|
18263
18265
|
};
|
|
18264
18266
|
|
|
18267
|
+
// packages/core/di/roster-service.ts
|
|
18268
|
+
var ClassRosterService = class {
|
|
18269
|
+
constructor(db2) {
|
|
18270
|
+
this.db = db2;
|
|
18271
|
+
}
|
|
18272
|
+
async listClasses() {
|
|
18273
|
+
const rows = this.db.prepare(
|
|
18274
|
+
`SELECT c.id AS id, c.name AS name, COUNT(cs.student_id) AS student_count
|
|
18275
|
+
FROM classes c
|
|
18276
|
+
LEFT JOIN class_students cs ON cs.class_id = c.id
|
|
18277
|
+
GROUP BY c.id, c.name
|
|
18278
|
+
ORDER BY c.created_at DESC, c.name ASC`
|
|
18279
|
+
).all();
|
|
18280
|
+
return rows.map((row) => ({ id: row.id, name: row.name, studentCount: Number(row.student_count) }));
|
|
18281
|
+
}
|
|
18282
|
+
async listClassStudents(classId) {
|
|
18283
|
+
const rows = this.db.prepare(
|
|
18284
|
+
`SELECT s.id AS id, s.name AS name, s.student_number AS student_number
|
|
18285
|
+
FROM class_students cs
|
|
18286
|
+
JOIN students s ON s.id = cs.student_id
|
|
18287
|
+
WHERE cs.class_id = ?
|
|
18288
|
+
ORDER BY s.name ASC`
|
|
18289
|
+
).all(classId);
|
|
18290
|
+
return rows.map(toStudentRef);
|
|
18291
|
+
}
|
|
18292
|
+
async getStudent(studentId) {
|
|
18293
|
+
const row = this.db.prepare("SELECT id, name, student_number FROM students WHERE id = ?").get(studentId);
|
|
18294
|
+
return row ? toStudentRef(row) : null;
|
|
18295
|
+
}
|
|
18296
|
+
};
|
|
18297
|
+
function toStudentRef(row) {
|
|
18298
|
+
return { id: row.id, name: row.name, studentNumber: row.student_number ?? null };
|
|
18299
|
+
}
|
|
18300
|
+
|
|
18265
18301
|
// packages/core/di/points-dimension-registry.ts
|
|
18266
18302
|
var PointsDimensionRegistry = class {
|
|
18267
18303
|
constructor() {
|
|
@@ -18422,7 +18458,7 @@ var PointsLedgerService = class {
|
|
|
18422
18458
|
};
|
|
18423
18459
|
|
|
18424
18460
|
// packages/core/di/auth-session-bridge-service.ts
|
|
18425
|
-
var
|
|
18461
|
+
var import_node_crypto4 = __toESM(require("node:crypto"), 1);
|
|
18426
18462
|
var AuthSessionBridgeService = class {
|
|
18427
18463
|
constructor(db2) {
|
|
18428
18464
|
this.db = db2;
|
|
@@ -18434,7 +18470,7 @@ var AuthSessionBridgeService = class {
|
|
|
18434
18470
|
const now = Date.now();
|
|
18435
18471
|
const maxAgeSeconds = 7 * 24 * 60 * 60;
|
|
18436
18472
|
const expiresAt = now + maxAgeSeconds * 1e3;
|
|
18437
|
-
const sessionToken = "token_" +
|
|
18473
|
+
const sessionToken = "token_" + import_node_crypto4.default.randomBytes(16).toString("hex");
|
|
18438
18474
|
if (user.role === "student") {
|
|
18439
18475
|
const existingStudent = this.db.prepare("SELECT id FROM students WHERE id = ?").get(user.userId);
|
|
18440
18476
|
if (!existingStudent) {
|
|
@@ -19162,7 +19198,7 @@ init_errors2();
|
|
|
19162
19198
|
init_interfaces();
|
|
19163
19199
|
|
|
19164
19200
|
// packages/core/plugin-host/student-data-privacy.ts
|
|
19165
|
-
var
|
|
19201
|
+
var import_node_crypto5 = __toESM(require("node:crypto"), 1);
|
|
19166
19202
|
async function dispatchStudentExport(pluginInstances, studentId, timeoutMs = 1e4) {
|
|
19167
19203
|
const results = {};
|
|
19168
19204
|
for (const [pluginId, instance] of pluginInstances.entries()) {
|
|
@@ -19170,7 +19206,7 @@ async function dispatchStudentExport(pluginInstances, studentId, timeoutMs = 1e4
|
|
|
19170
19206
|
if (instance.workerRef?.transport) {
|
|
19171
19207
|
try {
|
|
19172
19208
|
const transport = instance.workerRef.transport;
|
|
19173
|
-
const invokeId =
|
|
19209
|
+
const invokeId = import_node_crypto5.default.randomUUID();
|
|
19174
19210
|
const responsePromise = new Promise((resolve2) => {
|
|
19175
19211
|
const timer = setTimeout(() => {
|
|
19176
19212
|
cleanup();
|
|
@@ -19236,7 +19272,7 @@ async function dispatchStudentDelete(pluginInstances, studentId, timeoutMs = 1e4
|
|
|
19236
19272
|
if (instance.workerRef?.transport) {
|
|
19237
19273
|
try {
|
|
19238
19274
|
const transport = instance.workerRef.transport;
|
|
19239
|
-
const invokeId =
|
|
19275
|
+
const invokeId = import_node_crypto5.default.randomUUID();
|
|
19240
19276
|
const responsePromise = new Promise((resolve2) => {
|
|
19241
19277
|
const timer = setTimeout(() => {
|
|
19242
19278
|
cleanup();
|
|
@@ -20225,7 +20261,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
20225
20261
|
/**
|
|
20226
20262
|
* 判断指定插件的某个请求路径是否为流式路由 (SSE)
|
|
20227
20263
|
*/
|
|
20228
|
-
isStreamRoute(pluginIdOrManifestId, method,
|
|
20264
|
+
isStreamRoute(pluginIdOrManifestId, method, path48) {
|
|
20229
20265
|
const upperMethod = method.toUpperCase();
|
|
20230
20266
|
const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
|
|
20231
20267
|
const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
|
|
@@ -20234,7 +20270,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
20234
20270
|
for (const r of manifest.api.routes) {
|
|
20235
20271
|
if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
|
|
20236
20272
|
const compiled = compileRoutePattern(r.path);
|
|
20237
|
-
if (compiled.regex.test(
|
|
20273
|
+
if (compiled.regex.test(path48)) {
|
|
20238
20274
|
return true;
|
|
20239
20275
|
}
|
|
20240
20276
|
}
|
|
@@ -20242,14 +20278,14 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
20242
20278
|
}
|
|
20243
20279
|
if (!instance) return false;
|
|
20244
20280
|
if (instance.context?.http) {
|
|
20245
|
-
return instance.context.http.isStream(upperMethod,
|
|
20281
|
+
return instance.context.http.isStream(upperMethod, path48);
|
|
20246
20282
|
}
|
|
20247
20283
|
if (instance.workerRef) {
|
|
20248
20284
|
const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
|
|
20249
20285
|
for (const r of routes) {
|
|
20250
20286
|
if (r.method === upperMethod && r.isStream) {
|
|
20251
20287
|
const compiled = compileRoutePattern(r.pattern);
|
|
20252
|
-
if (compiled.regex.test(
|
|
20288
|
+
if (compiled.regex.test(path48)) {
|
|
20253
20289
|
return true;
|
|
20254
20290
|
}
|
|
20255
20291
|
}
|
|
@@ -20473,6 +20509,7 @@ init_types();
|
|
|
20473
20509
|
init_errors2();
|
|
20474
20510
|
init_interfaces();
|
|
20475
20511
|
init_plugin_grants();
|
|
20512
|
+
init_catalog();
|
|
20476
20513
|
var PluginHostLifecycle = class extends PluginHostHttp {
|
|
20477
20514
|
/**
|
|
20478
20515
|
* 激活插件。
|
|
@@ -20530,12 +20567,47 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20530
20567
|
const currentState = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
|
|
20531
20568
|
this.validateTransition(pluginId, currentState, "activating" /* ACTIVATING */);
|
|
20532
20569
|
this.setPluginState(pluginId, "activating" /* ACTIVATING */);
|
|
20570
|
+
const commandTypesBefore = this.listHostCommandTypes();
|
|
20533
20571
|
const preloaded = this.preloadedPlugins.get(pluginId);
|
|
20534
20572
|
if (preloaded) {
|
|
20535
20573
|
await this.activatePreloaded(pluginId, preloaded);
|
|
20574
|
+
this.auditMissingActionDescriptors(pluginId, commandTypesBefore);
|
|
20536
20575
|
return;
|
|
20537
20576
|
}
|
|
20538
20577
|
await this.activateFromDatabase(pluginId, currentState);
|
|
20578
|
+
this.auditMissingActionDescriptors(pluginId, commandTypesBefore);
|
|
20579
|
+
}
|
|
20580
|
+
/** 宿主 CommandBus 当前注册的命令类型;解析失败返回空集(自检不能影响激活)。 */
|
|
20581
|
+
listHostCommandTypes() {
|
|
20582
|
+
try {
|
|
20583
|
+
const bus = this.serviceRegistry.resolve(ICommandBusServiceToken4);
|
|
20584
|
+
return new Set(bus.listRegisteredCommandTypes?.() ?? []);
|
|
20585
|
+
} catch {
|
|
20586
|
+
return /* @__PURE__ */ new Set();
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
20589
|
+
/**
|
|
20590
|
+
* 插件激活后的命令契约自检(Issue #369)。
|
|
20591
|
+
*
|
|
20592
|
+
* 命令总线是 default-deny:handler 有、ActionDescriptor 没有的命令会在用户点击时
|
|
20593
|
+
* 403 `COMMAND_NOT_REGISTERED`。静态扫描抓不到循环注册 / 对象表 / `regHandler()` 包装,
|
|
20594
|
+
* 所以改为激活后做一次运行时差集,把缺口提前打成立即可见的 WARN。
|
|
20595
|
+
*
|
|
20596
|
+
* 自检只服务可观测性:任何异常都不能影响激活结果,故整体 try/catch 且只打日志。
|
|
20597
|
+
*/
|
|
20598
|
+
auditMissingActionDescriptors(pluginId, before) {
|
|
20599
|
+
try {
|
|
20600
|
+
if (this.inflightActivate.size > 1) return;
|
|
20601
|
+
const added = Array.from(this.listHostCommandTypes()).filter((type) => !before.has(type));
|
|
20602
|
+
if (added.length === 0) return;
|
|
20603
|
+
const registry4 = this.serviceRegistry.resolve(IActionRegistryServiceToken4);
|
|
20604
|
+
const missing = registry4.listMissingDescriptors?.(added) ?? [];
|
|
20605
|
+
if (missing.length === 0) return;
|
|
20606
|
+
console.warn(
|
|
20607
|
+
`[PluginHost] \u63D2\u4EF6 "${pluginId}" \u6709 ${missing.length} \u4E2A\u547D\u4EE4 handler \u6CA1\u6709 ActionDescriptor\uFF1A${missing.join(", ")}\u3002\u8FD9\u4E9B\u547D\u4EE4\u4F1A\u88AB default-deny \u4EE5 403 COMMAND_NOT_REGISTERED \u62D2\u7EDD\uFF0C\u8BF7\u4E3A\u6BCF\u4E2A registerHandler \u540C\u65F6\u6CE8\u518C actionRegistry.register(...)\u3002`
|
|
20608
|
+
);
|
|
20609
|
+
} catch {
|
|
20610
|
+
}
|
|
20539
20611
|
}
|
|
20540
20612
|
/**
|
|
20541
20613
|
* preloaded 插件的激活(Phase 8,内置插件路径)。
|
|
@@ -20786,6 +20858,12 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20786
20858
|
async grantCapabilities(actorId, caps, opts) {
|
|
20787
20859
|
if (caps.length === 0) return;
|
|
20788
20860
|
try {
|
|
20861
|
+
const { unknown } = validateManifestCapabilities(caps);
|
|
20862
|
+
if (unknown.length > 0) {
|
|
20863
|
+
console.error(
|
|
20864
|
+
`[PluginHost] "${actorId}" \u7684 manifest \u58F0\u660E\u4E86\u672A\u77E5\u80FD\u529B\uFF1A${unknown.join(", ")}\u3002\u8FD9\u4E9B\u4E32\u4E0D\u4F1A\u751F\u6548\uFF08\u5B88\u536B\u53EA\u8BA4 ActionDescriptor.capabilityRequired\uFF09\u3002\u5408\u6CD5\u6E05\u5355\u89C1 packages/core/capability/catalog.ts \u4E0E docs/reference/plugin-capability-matrix.md\u3002`
|
|
20865
|
+
);
|
|
20866
|
+
}
|
|
20789
20867
|
const capService = await this.serviceRegistry.resolve(ICapabilityServiceToken);
|
|
20790
20868
|
for (const cap of caps) {
|
|
20791
20869
|
if (typeof cap !== "string" || isBlockedPluginCapability(cap)) {
|
|
@@ -21126,7 +21204,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
|
21126
21204
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
21127
21205
|
var import_node_url5 = require("node:url");
|
|
21128
21206
|
var import_meta7 = {};
|
|
21129
|
-
var FALLBACK_VERSION = "0.7.
|
|
21207
|
+
var FALLBACK_VERSION = "0.7.10";
|
|
21130
21208
|
function readPlatformVersion() {
|
|
21131
21209
|
const bases = [];
|
|
21132
21210
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
@@ -23249,10 +23327,10 @@ var UnifiedExtensionRegistry = class {
|
|
|
23249
23327
|
init_install_utils();
|
|
23250
23328
|
|
|
23251
23329
|
// packages/core/security/plugin-signature.ts
|
|
23252
|
-
var
|
|
23330
|
+
var import_node_crypto8 = __toESM(require("node:crypto"), 1);
|
|
23253
23331
|
var import_node_fs13 = __toESM(require("node:fs"), 1);
|
|
23254
23332
|
function computeSha256(input) {
|
|
23255
|
-
const hash =
|
|
23333
|
+
const hash = import_node_crypto8.default.createHash("sha256");
|
|
23256
23334
|
if (typeof input === "string") {
|
|
23257
23335
|
if (import_node_fs13.default.existsSync(input)) {
|
|
23258
23336
|
const fd = import_node_fs13.default.openSync(input, "r");
|
|
@@ -23306,10 +23384,10 @@ function verifySignature(integrity, signature, publicKeys, metadata, options) {
|
|
|
23306
23384
|
for (const pem of keys) {
|
|
23307
23385
|
if (!pem || !pem.trim()) continue;
|
|
23308
23386
|
try {
|
|
23309
|
-
const pubKey =
|
|
23387
|
+
const pubKey = import_node_crypto8.default.createPublicKey(pem.trim());
|
|
23310
23388
|
if (metadata && metadata.id) {
|
|
23311
23389
|
const boundBuf = buildAttestationPayload(integrity, metadata);
|
|
23312
|
-
if (
|
|
23390
|
+
if (import_node_crypto8.default.verify(null, boundBuf, pubKey, sig)) {
|
|
23313
23391
|
return { ok: true, matchedKey: pem };
|
|
23314
23392
|
}
|
|
23315
23393
|
if (!allowV1 || metadata.version !== void 0) {
|
|
@@ -23318,7 +23396,7 @@ function verifySignature(integrity, signature, publicKeys, metadata, options) {
|
|
|
23318
23396
|
}
|
|
23319
23397
|
if (allowV1) {
|
|
23320
23398
|
const rawBuf = Buffer.from(integrity.trim(), "utf8");
|
|
23321
|
-
if (
|
|
23399
|
+
if (import_node_crypto8.default.verify(null, rawBuf, pubKey, sig)) {
|
|
23322
23400
|
return { ok: true, matchedKey: pem };
|
|
23323
23401
|
}
|
|
23324
23402
|
}
|
|
@@ -23639,11 +23717,11 @@ var PluginDistributionManager = class {
|
|
|
23639
23717
|
return;
|
|
23640
23718
|
}
|
|
23641
23719
|
const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
|
|
23642
|
-
const
|
|
23643
|
-
if (!
|
|
23720
|
+
const fs41 = await import("node:fs");
|
|
23721
|
+
if (!fs41.existsSync(filePath)) {
|
|
23644
23722
|
throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
|
|
23645
23723
|
}
|
|
23646
|
-
const code =
|
|
23724
|
+
const code = fs41.readFileSync(filePath, "utf-8");
|
|
23647
23725
|
await this.pluginHost.reloadPlugin(pluginId, code);
|
|
23648
23726
|
}
|
|
23649
23727
|
async updateFromZip(zipPath, options = {}) {
|
|
@@ -23832,8 +23910,8 @@ var HotReloadController = class {
|
|
|
23832
23910
|
const timer = setTimeout(async () => {
|
|
23833
23911
|
this.debounceTimers.delete(pluginId);
|
|
23834
23912
|
try {
|
|
23835
|
-
const
|
|
23836
|
-
const newSourceCode = await
|
|
23913
|
+
const fs41 = await import("fs/promises");
|
|
23914
|
+
const newSourceCode = await fs41.readFile(filePath, "utf-8");
|
|
23837
23915
|
console.log(`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`);
|
|
23838
23916
|
await this.pluginHost.reloadPlugin(pluginId, newSourceCode);
|
|
23839
23917
|
} catch (err) {
|
|
@@ -30835,6 +30913,7 @@ var Kernel = class {
|
|
|
30835
30913
|
this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
|
|
30836
30914
|
this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
|
|
30837
30915
|
this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
|
|
30916
|
+
this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
|
|
30838
30917
|
this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
|
|
30839
30918
|
this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
|
|
30840
30919
|
getRuntimeKernel: async () => this.classroomRuntime
|
|
@@ -31307,7 +31386,7 @@ function clearLessonRuntimeState(lessonId) {
|
|
|
31307
31386
|
init_plugin_namespace();
|
|
31308
31387
|
|
|
31309
31388
|
// server/classroom-events.ts
|
|
31310
|
-
var
|
|
31389
|
+
var import_node_crypto9 = require("node:crypto");
|
|
31311
31390
|
var CLASSROOM_EVENTS = {
|
|
31312
31391
|
/** 全班锁屏 / 解锁 */
|
|
31313
31392
|
CLASSROOM_LOCK_CHANGED: "classroom.lock_changed",
|
|
@@ -31327,7 +31406,7 @@ var CLASSROOM_EVENTS = {
|
|
|
31327
31406
|
SEGMENT_GATE_OVERRIDE_REVOKED: "segment.gate_override_revoked"
|
|
31328
31407
|
};
|
|
31329
31408
|
function newEventId() {
|
|
31330
|
-
return `evt_${(0,
|
|
31409
|
+
return `evt_${(0, import_node_crypto9.randomBytes)(8).toString("hex")}`;
|
|
31331
31410
|
}
|
|
31332
31411
|
async function publishClassroomEvent(type, payload, options = {}) {
|
|
31333
31412
|
const event = {
|
|
@@ -33028,21 +33107,29 @@ function validateStartupEnv(env = process.env) {
|
|
|
33028
33107
|
if (isProduction) {
|
|
33029
33108
|
const adminPassword = trimmed(env, "OPENLEARN_ADMIN_PASSWORD");
|
|
33030
33109
|
if (adminPassword && adminPassword.toLowerCase() === "admin") {
|
|
33031
|
-
|
|
33110
|
+
warnings.push({
|
|
33032
33111
|
key: "OPENLEARN_ADMIN_PASSWORD",
|
|
33033
|
-
severity: "
|
|
33034
|
-
message:
|
|
33112
|
+
severity: "warning",
|
|
33113
|
+
message: "OPENLEARN_ADMIN_PASSWORD \u4E0E\u7528\u6237\u540D\u76F8\u540C\u3002\u8FD9\u4E0D\u6BD4\u7559\u7A7A\u66F4\u5F31\uFF08\u7559\u7A7A\u65F6\u521D\u59CB\u53E3\u4EE4\u540C\u6837\u662F admin\uFF09\uFF0C\u4F46\u8BF7\u786E\u8BA4\u9996\u6B21\u767B\u5F55\u540E\u7ACB\u5373\u6539\u5BC6\uFF1B\u751F\u4EA7\u90E8\u7F72\u5EFA\u8BAE\u76F4\u63A5\u8BBE\u7F6E\u4E3A\u5F3A\u53E3\u4EE4\u3002"
|
|
33035
33114
|
});
|
|
33036
33115
|
}
|
|
33037
33116
|
const teacherPassword = trimmed(env, "OPENLEARN_TEACHER_PASSWORD");
|
|
33038
33117
|
if (teacherPassword && teacherPassword.toLowerCase() === "teacher") {
|
|
33039
|
-
|
|
33118
|
+
warnings.push({
|
|
33040
33119
|
key: "OPENLEARN_TEACHER_PASSWORD",
|
|
33041
|
-
severity: "
|
|
33042
|
-
message:
|
|
33120
|
+
severity: "warning",
|
|
33121
|
+
message: "OPENLEARN_TEACHER_PASSWORD \u4E0E\u7528\u6237\u540D\u76F8\u540C\u3002\u8BF7\u786E\u8BA4\u9996\u6B21\u767B\u5F55\u540E\u7ACB\u5373\u6539\u5BC6\uFF0C\u751F\u4EA7\u90E8\u7F72\u5EFA\u8BAE\u8BBE\u7F6E\u4E3A\u5F3A\u53E3\u4EE4\u3002"
|
|
33043
33122
|
});
|
|
33044
33123
|
}
|
|
33045
33124
|
}
|
|
33125
|
+
const seedMode = trimmed(env, "OPENLEARN_SEED_PASSWORD_MODE");
|
|
33126
|
+
if (seedMode && !["default", "random"].includes(seedMode.toLowerCase())) {
|
|
33127
|
+
errors.push({
|
|
33128
|
+
key: "OPENLEARN_SEED_PASSWORD_MODE",
|
|
33129
|
+
severity: "error",
|
|
33130
|
+
message: `\u53EA\u652F\u6301 "default"\uFF08\u53E3\u4EE4 = \u7528\u6237\u540D\uFF0C\u9996\u6B21\u767B\u5F55\u5F3A\u5236\u6539\u5BC6\uFF09\u6216 "random"\uFF08\u968F\u673A\u53E3\u4EE4\u5199\u5165 bootstrap-credentials.txt\uFF09\uFF0C\u6536\u5230 "${seedMode}"\u3002`
|
|
33131
|
+
});
|
|
33132
|
+
}
|
|
33046
33133
|
const metricsToken = trimmed(env, "METRICS_TOKEN");
|
|
33047
33134
|
if (metricsToken && metricsToken.length < 16) {
|
|
33048
33135
|
warnings.push({
|
|
@@ -34163,9 +34250,9 @@ function isWhitelisted(route, whitelist) {
|
|
|
34163
34250
|
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
34164
34251
|
);
|
|
34165
34252
|
}
|
|
34166
|
-
function normalizePath2(
|
|
34167
|
-
if (!
|
|
34168
|
-
return
|
|
34253
|
+
function normalizePath2(path48) {
|
|
34254
|
+
if (!path48) return "/";
|
|
34255
|
+
return path48.endsWith("/") && path48.length > 1 ? path48.slice(0, -1) : path48;
|
|
34169
34256
|
}
|
|
34170
34257
|
function auditRouteScopes(app, whitelist = []) {
|
|
34171
34258
|
const declared = [];
|
|
@@ -34343,11 +34430,11 @@ function enforcePasswordChanged(req, res, next) {
|
|
|
34343
34430
|
if (isPasswordChangeGateSkipped()) {
|
|
34344
34431
|
return next();
|
|
34345
34432
|
}
|
|
34346
|
-
const
|
|
34347
|
-
if (!
|
|
34433
|
+
const path48 = req.path || "";
|
|
34434
|
+
if (!path48.startsWith("/api/")) {
|
|
34348
34435
|
return next();
|
|
34349
34436
|
}
|
|
34350
|
-
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(
|
|
34437
|
+
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path48)) {
|
|
34351
34438
|
return next();
|
|
34352
34439
|
}
|
|
34353
34440
|
const token = getCookieToken(req);
|
|
@@ -34422,8 +34509,8 @@ function csrfGuard(req, res, next) {
|
|
|
34422
34509
|
next();
|
|
34423
34510
|
return;
|
|
34424
34511
|
}
|
|
34425
|
-
const
|
|
34426
|
-
if (EXEMPT_PREFIXES.some((p) =>
|
|
34512
|
+
const path49 = req.path;
|
|
34513
|
+
if (EXEMPT_PREFIXES.some((p) => path49 === p || path49.startsWith(p))) {
|
|
34427
34514
|
next();
|
|
34428
34515
|
return;
|
|
34429
34516
|
}
|
|
@@ -34433,10 +34520,10 @@ function csrfGuard(req, res, next) {
|
|
|
34433
34520
|
const origin = req.headers.origin;
|
|
34434
34521
|
const referer = req.headers.referer;
|
|
34435
34522
|
const host = req.headers.host;
|
|
34436
|
-
const
|
|
34523
|
+
const path48 = req.path;
|
|
34437
34524
|
if (typeof origin === "string" && origin !== "") {
|
|
34438
34525
|
if (origin === "null") {
|
|
34439
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34526
|
+
if (!EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
34440
34527
|
res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
|
|
34441
34528
|
return;
|
|
34442
34529
|
}
|
|
@@ -34444,7 +34531,7 @@ function csrfGuard(req, res, next) {
|
|
|
34444
34531
|
try {
|
|
34445
34532
|
const originHost = new URL(origin).host;
|
|
34446
34533
|
if (host && originHost.toLowerCase() !== host.toLowerCase()) {
|
|
34447
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34534
|
+
if (!EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
34448
34535
|
res.status(403).json({
|
|
34449
34536
|
success: false,
|
|
34450
34537
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34462,7 +34549,7 @@ function csrfGuard(req, res, next) {
|
|
|
34462
34549
|
try {
|
|
34463
34550
|
const refererHost = new URL(referer).host;
|
|
34464
34551
|
if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
|
|
34465
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34552
|
+
if (!EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
34466
34553
|
res.status(403).json({
|
|
34467
34554
|
success: false,
|
|
34468
34555
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34480,7 +34567,7 @@ function csrfGuard(req, res, next) {
|
|
|
34480
34567
|
}
|
|
34481
34568
|
|
|
34482
34569
|
// packages/core/observability/error-reporter.ts
|
|
34483
|
-
var
|
|
34570
|
+
var import_node_crypto10 = require("node:crypto");
|
|
34484
34571
|
init_logger();
|
|
34485
34572
|
init_sanitize();
|
|
34486
34573
|
var testSink2 = null;
|
|
@@ -34515,8 +34602,8 @@ function parseSentryDsn(dsn) {
|
|
|
34515
34602
|
const projectId = url.pathname.split("/").filter(Boolean).pop();
|
|
34516
34603
|
if (!projectId || !/^\d+$/.test(projectId)) return null;
|
|
34517
34604
|
const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
|
|
34518
|
-
const
|
|
34519
|
-
const envelopeUrl = `${url.protocol}//${url.host}${
|
|
34605
|
+
const path48 = prefix ? `/${prefix}` : "";
|
|
34606
|
+
const envelopeUrl = `${url.protocol}//${url.host}${path48}/api/${projectId}/envelope/`;
|
|
34520
34607
|
return { envelopeUrl, publicKey };
|
|
34521
34608
|
} catch {
|
|
34522
34609
|
return null;
|
|
@@ -34526,7 +34613,7 @@ function buildSentryEnvelope(report, dsn) {
|
|
|
34526
34613
|
const safe = sanitizeErrorReport(report);
|
|
34527
34614
|
const parsed = parseSentryDsn(dsn);
|
|
34528
34615
|
if (!parsed) return null;
|
|
34529
|
-
const eventId = (0,
|
|
34616
|
+
const eventId = (0, import_node_crypto10.randomBytes)(16).toString("hex");
|
|
34530
34617
|
const header = JSON.stringify({ event_id: eventId, sent_at: (/* @__PURE__ */ new Date()).toISOString(), dsn });
|
|
34531
34618
|
const item = JSON.stringify({ type: "event", content_type: "application/json" });
|
|
34532
34619
|
const event = {
|
|
@@ -34631,7 +34718,7 @@ function sendSafeError(res, err, status = 500, fallbackMessage = "Internal serve
|
|
|
34631
34718
|
}
|
|
34632
34719
|
|
|
34633
34720
|
// server/middleware/request-observability.ts
|
|
34634
|
-
var
|
|
34721
|
+
var import_node_crypto11 = require("node:crypto");
|
|
34635
34722
|
init_logger();
|
|
34636
34723
|
init_request_context();
|
|
34637
34724
|
init_audit_log();
|
|
@@ -34775,7 +34862,7 @@ function resolveHttpAccessLogLevel(status, durationMs, mode = resolveHttpAccessL
|
|
|
34775
34862
|
}
|
|
34776
34863
|
function resolveRequestId(header) {
|
|
34777
34864
|
if (header && REQUEST_ID_RE.test(header)) return header;
|
|
34778
|
-
return (0,
|
|
34865
|
+
return (0, import_node_crypto11.randomUUID)();
|
|
34779
34866
|
}
|
|
34780
34867
|
function resolveRequestUserId(req) {
|
|
34781
34868
|
const attached = req.session;
|
|
@@ -34962,9 +35049,9 @@ var import_node_path18 = __toESM(require("node:path"), 1);
|
|
|
34962
35049
|
// packages/core/observability/health.ts
|
|
34963
35050
|
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
34964
35051
|
var import_node_path17 = __toESM(require("node:path"), 1);
|
|
34965
|
-
var
|
|
35052
|
+
var import_node_crypto12 = require("node:crypto");
|
|
34966
35053
|
function probeStorageWritable(dir) {
|
|
34967
|
-
const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0,
|
|
35054
|
+
const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto12.randomBytes)(4).toString("hex")}`);
|
|
34968
35055
|
try {
|
|
34969
35056
|
import_node_fs16.default.mkdirSync(dir, { recursive: true });
|
|
34970
35057
|
import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
|
|
@@ -35242,7 +35329,7 @@ var import_zod2 = require("zod");
|
|
|
35242
35329
|
// server/services/course-package-assets.ts
|
|
35243
35330
|
var import_node_fs18 = __toESM(require("node:fs"), 1);
|
|
35244
35331
|
var import_node_path19 = __toESM(require("node:path"), 1);
|
|
35245
|
-
var
|
|
35332
|
+
var import_node_crypto13 = __toESM(require("node:crypto"), 1);
|
|
35246
35333
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
35247
35334
|
var import_jspdf = require("jspdf");
|
|
35248
35335
|
|
|
@@ -35507,7 +35594,7 @@ async function materializePackageAssets(assets) {
|
|
|
35507
35594
|
if (isTextAssetType(asset.contentType) && buffer.includes(0)) {
|
|
35508
35595
|
throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
|
|
35509
35596
|
}
|
|
35510
|
-
const uniqueName = `${Date.now()}-${
|
|
35597
|
+
const uniqueName = `${Date.now()}-${import_node_crypto13.default.randomBytes(4).toString("hex")}${ext}`;
|
|
35511
35598
|
const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
|
|
35512
35599
|
import_node_fs18.default.writeFileSync(filePath, buffer);
|
|
35513
35600
|
writtenPaths.push(filePath);
|
|
@@ -35537,7 +35624,7 @@ function resolveAssetPlaceholders(value, assets) {
|
|
|
35537
35624
|
return value.replace(ASSET_PLACEHOLDER_PATTERN, (match, assetId) => {
|
|
35538
35625
|
const asset = assets.get(assetId);
|
|
35539
35626
|
if (!asset) {
|
|
35540
|
-
throw new Error(`\
|
|
35627
|
+
throw new Error(`\u8BFE\u7A0B\u5185\u5BB9\u5F15\u7528\u4E86\u4E0D\u5B58\u5728\u7684\u8BFE\u7A0B\u5305\u8D44\u4EA7\uFF1A${assetId}`);
|
|
35541
35628
|
}
|
|
35542
35629
|
return asset.fileUrl;
|
|
35543
35630
|
});
|
|
@@ -35659,6 +35746,28 @@ function parseCoursePackage(input) {
|
|
|
35659
35746
|
}
|
|
35660
35747
|
return { ok: true, package: parsed.data };
|
|
35661
35748
|
}
|
|
35749
|
+
var KNOWN_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
35750
|
+
"pen",
|
|
35751
|
+
"highlighter",
|
|
35752
|
+
"rectangle",
|
|
35753
|
+
"circle",
|
|
35754
|
+
"text",
|
|
35755
|
+
"quiz",
|
|
35756
|
+
"rollcall",
|
|
35757
|
+
"timer",
|
|
35758
|
+
"assignment",
|
|
35759
|
+
"hello-world",
|
|
35760
|
+
"html-applet",
|
|
35761
|
+
"code-sandbox",
|
|
35762
|
+
"math-graph",
|
|
35763
|
+
"presentation",
|
|
35764
|
+
"plugin",
|
|
35765
|
+
"page_meta"
|
|
35766
|
+
]);
|
|
35767
|
+
function isSupportedElementType(type) {
|
|
35768
|
+
if (KNOWN_ELEMENT_TYPES.has(type)) return true;
|
|
35769
|
+
return typeof type === "string" && (type.includes("/") || type.startsWith("plugin:") || type.startsWith("ext-") || type.startsWith("plugin-"));
|
|
35770
|
+
}
|
|
35662
35771
|
var SEG = {
|
|
35663
35772
|
intro: "seg-intro",
|
|
35664
35773
|
lecture: "seg-lecture",
|
|
@@ -38966,6 +39075,71 @@ function registerResourcesRoutes(ctx) {
|
|
|
38966
39075
|
sendSafeError(res, e);
|
|
38967
39076
|
}
|
|
38968
39077
|
});
|
|
39078
|
+
const actionRegistry = kernelContainer.actionRegistry;
|
|
39079
|
+
const resourceIdProp = { id: { type: "STRING", description: "\u8D44\u6E90 ID\uFF08res_ \u524D\u7F00\uFF09" } };
|
|
39080
|
+
try {
|
|
39081
|
+
actionRegistry.register({
|
|
39082
|
+
id: "core-resource-list",
|
|
39083
|
+
commandType: "resource.list",
|
|
39084
|
+
description: "\u5217\u51FA\u5168\u90E8\u7CFB\u7EDF\u516C\u5171\u6559\u5B66\u8D44\u6E90\uFF08\u6309\u521B\u5EFA\u65F6\u95F4\u5012\u5E8F\uFF0C\u4E0D\u542B\u6B63\u6587\uFF09",
|
|
39085
|
+
capabilityRequired: "management:read",
|
|
39086
|
+
// 无参命令沿用仓库既有写法(同 courseware.list / courseware.list_score_configs):
|
|
39087
|
+
// schema 声明为 OBJECT,payload 必须是对象 —— 调用方传 `{}`,不要省略 payload,
|
|
39088
|
+
// 否则 validateJsonSchema 会以 "Expected object, got undefined" 拒绝。
|
|
39089
|
+
inputSchema: { type: "OBJECT", properties: {} }
|
|
39090
|
+
});
|
|
39091
|
+
} catch {
|
|
39092
|
+
}
|
|
39093
|
+
try {
|
|
39094
|
+
actionRegistry.register({
|
|
39095
|
+
id: "core-resource-get",
|
|
39096
|
+
commandType: "resource.get",
|
|
39097
|
+
description: "\u6309 ID \u8BFB\u53D6\u5355\u4E2A\u6559\u5B66\u8D44\u6E90\u7684\u5B8C\u6574\u8BB0\u5F55\uFF08\u542B content \u6B63\u6587\uFF09",
|
|
39098
|
+
capabilityRequired: "management:read",
|
|
39099
|
+
inputSchema: {
|
|
39100
|
+
type: "OBJECT",
|
|
39101
|
+
properties: { ...resourceIdProp },
|
|
39102
|
+
required: ["id"]
|
|
39103
|
+
}
|
|
39104
|
+
});
|
|
39105
|
+
} catch {
|
|
39106
|
+
}
|
|
39107
|
+
try {
|
|
39108
|
+
actionRegistry.register({
|
|
39109
|
+
id: "core-resource-create",
|
|
39110
|
+
commandType: "resource.create",
|
|
39111
|
+
description: "\u521B\u5EFA\u6559\u5B66\u8D44\u6E90\u3002type=html \u65F6 content \u4E3A\u5B8C\u6574 HTML \u6587\u672C\uFF1Btype=folder \u65F6 content \u4E3A [{path, content}] \u6587\u4EF6\u6570\u7EC4\u7684 JSON \u5B57\u7B26\u4E32\u3002HTML \u4E2D\u82E5\u542B\u6210\u7EE9\u5C55\u793A\u4F46\u7F3A\u63D0\u4EA4\u57CB\u70B9\uFF0C\u5E73\u53F0\u4F1A\u81EA\u52A8\u6D3E\u751F\u300C[\u81EA\u52A8\u63D0\u4EA4\u7248]\u300D\u3002",
|
|
39112
|
+
capabilityRequired: "management:write",
|
|
39113
|
+
inputSchema: {
|
|
39114
|
+
type: "OBJECT",
|
|
39115
|
+
properties: {
|
|
39116
|
+
name: { type: "STRING", description: "\u8D44\u6E90\u540D\u79F0\uFF1Btype=html \u65F6\u5EFA\u8BAE\u4EE5 .html \u7ED3\u5C3E" },
|
|
39117
|
+
type: { type: "STRING", description: "\u8D44\u6E90\u7C7B\u578B\uFF1A'html' \u6216 'folder'" },
|
|
39118
|
+
content: {
|
|
39119
|
+
type: "STRING",
|
|
39120
|
+
description: "html \u4E3A HTML \u6587\u672C\uFF1Bfolder \u4E3A [{path, content}] \u7684 JSON \u5B57\u7B26\u4E32"
|
|
39121
|
+
}
|
|
39122
|
+
},
|
|
39123
|
+
required: ["name", "type", "content"]
|
|
39124
|
+
}
|
|
39125
|
+
});
|
|
39126
|
+
} catch {
|
|
39127
|
+
}
|
|
39128
|
+
try {
|
|
39129
|
+
actionRegistry.register({
|
|
39130
|
+
id: "core-resource-delete",
|
|
39131
|
+
commandType: "resource.delete",
|
|
39132
|
+
description: "\u6309 ID \u5220\u9664\u6559\u5B66\u8D44\u6E90\uFF08\u542B system_resources \u884C\u4E0E\u78C1\u76D8\u7F13\u5B58\uFF09\u3002\u6B64\u4E3A\u9AD8\u98CE\u9669\u64CD\u4F5C\u3002",
|
|
39133
|
+
capabilityRequired: "management:write",
|
|
39134
|
+
isHighRisk: true,
|
|
39135
|
+
inputSchema: {
|
|
39136
|
+
type: "OBJECT",
|
|
39137
|
+
properties: { ...resourceIdProp },
|
|
39138
|
+
required: ["id"]
|
|
39139
|
+
}
|
|
39140
|
+
});
|
|
39141
|
+
} catch {
|
|
39142
|
+
}
|
|
38969
39143
|
const RESOURCE_HANDLERS = {
|
|
38970
39144
|
"resource.list": {
|
|
38971
39145
|
execute: async () => {
|
|
@@ -42612,7 +42786,7 @@ var import_node_path20 = __toESM(require("node:path"), 1);
|
|
|
42612
42786
|
var import_uuid18 = require("uuid");
|
|
42613
42787
|
|
|
42614
42788
|
// server/services/course-package-identity.ts
|
|
42615
|
-
var
|
|
42789
|
+
var import_node_crypto14 = require("node:crypto");
|
|
42616
42790
|
function stableStringify(value) {
|
|
42617
42791
|
if (value === null || typeof value !== "object") {
|
|
42618
42792
|
return JSON.stringify(value ?? null) ?? "null";
|
|
@@ -42651,7 +42825,7 @@ function canonicalContent(pkg) {
|
|
|
42651
42825
|
};
|
|
42652
42826
|
}
|
|
42653
42827
|
function computeCoursePackageHash(pkg) {
|
|
42654
|
-
return (0,
|
|
42828
|
+
return (0, import_node_crypto14.createHash)("sha256").update(stableStringify(canonicalContent(pkg))).digest("hex");
|
|
42655
42829
|
}
|
|
42656
42830
|
function resolveCoursePackageIdentity(pkg) {
|
|
42657
42831
|
const hash = computeCoursePackageHash(pkg);
|
|
@@ -42688,24 +42862,6 @@ var CoursePackageDuplicateError = class extends Error {
|
|
|
42688
42862
|
this.hasDeclaredId = params.hasDeclaredId;
|
|
42689
42863
|
}
|
|
42690
42864
|
};
|
|
42691
|
-
var KNOWN_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
42692
|
-
"pen",
|
|
42693
|
-
"highlighter",
|
|
42694
|
-
"rectangle",
|
|
42695
|
-
"circle",
|
|
42696
|
-
"text",
|
|
42697
|
-
"quiz",
|
|
42698
|
-
"rollcall",
|
|
42699
|
-
"timer",
|
|
42700
|
-
"assignment",
|
|
42701
|
-
"hello-world",
|
|
42702
|
-
"html-applet",
|
|
42703
|
-
"code-sandbox",
|
|
42704
|
-
"math-graph",
|
|
42705
|
-
"presentation",
|
|
42706
|
-
"plugin",
|
|
42707
|
-
"page_meta"
|
|
42708
|
-
]);
|
|
42709
42865
|
async function importCoursePackage(pkg, options) {
|
|
42710
42866
|
const db2 = options.db ?? kernelContainer.db;
|
|
42711
42867
|
const creatorId = options.creatorId || "teacher";
|
|
@@ -42747,8 +42903,7 @@ async function importCoursePackage(pkg, options) {
|
|
|
42747
42903
|
let elements = [];
|
|
42748
42904
|
try {
|
|
42749
42905
|
elements = pkg.elements.map((element) => {
|
|
42750
|
-
|
|
42751
|
-
if (!isAllowed) {
|
|
42906
|
+
if (!isSupportedElementType(element.type)) {
|
|
42752
42907
|
throw new Error(`\u8BFE\u7A0B\u5305\u5305\u542B\u4E0D\u652F\u6301\u7684\u767D\u677F\u5143\u7D20\u7C7B\u578B\uFF1A${element.type}`);
|
|
42753
42908
|
}
|
|
42754
42909
|
const data = resolveAssetPlaceholders(element.data, materialized);
|
|
@@ -42770,6 +42925,7 @@ async function importCoursePackage(pkg, options) {
|
|
|
42770
42925
|
if (data.page === void 0 && data.pageId === void 0) data.page = 0;
|
|
42771
42926
|
return { type: element.type, data: JSON.stringify(data) };
|
|
42772
42927
|
});
|
|
42928
|
+
const lessonContent = resolveAssetPlaceholders(pkg.lesson.content ?? "", materialized);
|
|
42773
42929
|
const write = db2.transaction(() => {
|
|
42774
42930
|
const inTxDuplicate = findExistingImport(db2, creatorId, identity);
|
|
42775
42931
|
if (inTxDuplicate && !allowDuplicate) {
|
|
@@ -42784,7 +42940,7 @@ async function importCoursePackage(pkg, options) {
|
|
|
42784
42940
|
insertLesson.run(
|
|
42785
42941
|
lessonId,
|
|
42786
42942
|
pkg.lesson.title,
|
|
42787
|
-
|
|
42943
|
+
lessonContent,
|
|
42788
42944
|
JSON.stringify(timeline),
|
|
42789
42945
|
pkg.lesson.progressMode ?? "manual",
|
|
42790
42946
|
creatorId,
|
|
@@ -42896,6 +43052,307 @@ function cleanupMaterializedAssets(materialized) {
|
|
|
42896
43052
|
}
|
|
42897
43053
|
}
|
|
42898
43054
|
|
|
43055
|
+
// server/services/course-package-export.ts
|
|
43056
|
+
var import_node_fs21 = __toESM(require("node:fs"), 1);
|
|
43057
|
+
var import_node_path21 = __toESM(require("node:path"), 1);
|
|
43058
|
+
var import_node_crypto15 = require("node:crypto");
|
|
43059
|
+
var CoursePackageExportError = class extends Error {
|
|
43060
|
+
constructor(message, status = 500, code = "COURSE_PACKAGE_EXPORT_FAILED") {
|
|
43061
|
+
super(message);
|
|
43062
|
+
this.name = "CoursePackageExportError";
|
|
43063
|
+
this.status = status;
|
|
43064
|
+
this.code = code;
|
|
43065
|
+
}
|
|
43066
|
+
};
|
|
43067
|
+
var UPLOAD_URL_PATTERN = /(?:https?:\/\/[A-Za-z0-9.:[\]_-]+)?\/uploads\/([A-Za-z0-9][A-Za-z0-9._-]*)/g;
|
|
43068
|
+
var EXT_TO_ASSET_TYPE = {
|
|
43069
|
+
".pdf": "pdf",
|
|
43070
|
+
".pptx": "pptx",
|
|
43071
|
+
".docx": "docx",
|
|
43072
|
+
".xlsx": "xlsx",
|
|
43073
|
+
".zip": "zip",
|
|
43074
|
+
".png": "png",
|
|
43075
|
+
".jpg": "jpg",
|
|
43076
|
+
".jpeg": "jpg",
|
|
43077
|
+
".md": "md",
|
|
43078
|
+
".txt": "txt",
|
|
43079
|
+
".csv": "csv"
|
|
43080
|
+
};
|
|
43081
|
+
async function exportLessonAsCoursePackage(lessonId, options = {}) {
|
|
43082
|
+
const db2 = options.db ?? kernelContainer.db;
|
|
43083
|
+
const lesson = db2.prepare(
|
|
43084
|
+
"SELECT id, title, content, timeline, progress_mode, creator_id, source_package_id FROM lessons WHERE id = ?"
|
|
43085
|
+
).get(lessonId);
|
|
43086
|
+
if (!lesson) {
|
|
43087
|
+
throw new CoursePackageExportError("\u8BFE\u7A0B\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664", 404, "LESSON_NOT_FOUND");
|
|
43088
|
+
}
|
|
43089
|
+
const elementRows = db2.prepare(
|
|
43090
|
+
"SELECT type, data FROM whiteboard_elements WHERE lesson_id = ? AND type != 'snapshot_marker' ORDER BY created_at ASC, rowid ASC"
|
|
43091
|
+
).all(lessonId);
|
|
43092
|
+
const rawTimeline = safeJsonParse(lesson.timeline);
|
|
43093
|
+
const { timeline, idRemap } = normalizeTimeline(rawTimeline);
|
|
43094
|
+
const stats = { assets: 0, skipped: [], skippedElements: 0 };
|
|
43095
|
+
const elements = [];
|
|
43096
|
+
const preferredFilenames = /* @__PURE__ */ new Map();
|
|
43097
|
+
for (const row of elementRows) {
|
|
43098
|
+
if (!isSupportedElementType(row.type)) {
|
|
43099
|
+
stats.skippedElements += 1;
|
|
43100
|
+
continue;
|
|
43101
|
+
}
|
|
43102
|
+
const data = safeJsonParse(row.data);
|
|
43103
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
43104
|
+
stats.skippedElements += 1;
|
|
43105
|
+
continue;
|
|
43106
|
+
}
|
|
43107
|
+
const elementData = data;
|
|
43108
|
+
collectPreferredFilenames(elementData, preferredFilenames);
|
|
43109
|
+
elements.push({
|
|
43110
|
+
type: row.type,
|
|
43111
|
+
...liftElementGeometry(elementData),
|
|
43112
|
+
data: elementData
|
|
43113
|
+
});
|
|
43114
|
+
}
|
|
43115
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
43116
|
+
collectUploadReferences(lesson.content ?? "", referenced);
|
|
43117
|
+
for (const element of elements) collectUploadReferences(element.data, referenced);
|
|
43118
|
+
const pending = [];
|
|
43119
|
+
const placeholderByStoredName = /* @__PURE__ */ new Map();
|
|
43120
|
+
let totalBytes = 0;
|
|
43121
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
43122
|
+
for (const storedName of referenced) {
|
|
43123
|
+
const ext = import_node_path21.default.extname(storedName).toLowerCase();
|
|
43124
|
+
const contentType = EXT_TO_ASSET_TYPE[ext];
|
|
43125
|
+
if (!contentType || !PACKAGE_ASSET_TYPES.includes(contentType)) {
|
|
43126
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: `\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B\uFF1A${ext || "(\u65E0\u6269\u5C55\u540D)"}` });
|
|
43127
|
+
continue;
|
|
43128
|
+
}
|
|
43129
|
+
const filePath = resolveUploadsPath(storedName);
|
|
43130
|
+
if (!filePath) {
|
|
43131
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
43132
|
+
continue;
|
|
43133
|
+
}
|
|
43134
|
+
let buffer;
|
|
43135
|
+
try {
|
|
43136
|
+
buffer = import_node_fs21.default.readFileSync(filePath);
|
|
43137
|
+
} catch (err) {
|
|
43138
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: `\u8BFB\u53D6\u5931\u8D25\uFF1A${err.message}` });
|
|
43139
|
+
continue;
|
|
43140
|
+
}
|
|
43141
|
+
if (buffer.length === 0) {
|
|
43142
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: "\u6587\u4EF6\u4E3A\u7A7A" });
|
|
43143
|
+
continue;
|
|
43144
|
+
}
|
|
43145
|
+
if (!isTextAssetType(contentType) && !validateMagicBytes(buffer, `asset.${contentType}`)) {
|
|
43146
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: "\u6587\u4EF6\u7B7E\u540D\u4E0E\u6269\u5C55\u540D\u4E0D\u7B26" });
|
|
43147
|
+
continue;
|
|
43148
|
+
}
|
|
43149
|
+
if (isTextAssetType(contentType) && buffer.includes(0)) {
|
|
43150
|
+
stats.skipped.push({ url: `/uploads/${storedName}`, reason: "\u6587\u672C\u6587\u4EF6\u5305\u542B\u4E8C\u8FDB\u5236\u5185\u5BB9" });
|
|
43151
|
+
continue;
|
|
43152
|
+
}
|
|
43153
|
+
totalBytes += buffer.length;
|
|
43154
|
+
if (totalBytes > COURSE_PACKAGE_MAX_FILE_BYTES) {
|
|
43155
|
+
throw new CoursePackageExportError(
|
|
43156
|
+
`\u8BFE\u7A0B\u5F15\u7528\u7684\u9644\u4EF6\u5408\u8BA1\u8D85\u8FC7 ${COURSE_PACKAGE_MAX_FILE_BYTES / (1024 * 1024)}MB\uFF0C\u5BFC\u51FA\u540E\u65E0\u6CD5\u518D\u88AB\u5BFC\u5165\u3002\u8BF7\u7CBE\u7B80\u6559\u6848\u4E2D\u7684\u5927\u6587\u4EF6\u540E\u91CD\u8BD5\u3002`,
|
|
43157
|
+
413,
|
|
43158
|
+
"COURSE_PACKAGE_EXPORT_TOO_LARGE"
|
|
43159
|
+
);
|
|
43160
|
+
}
|
|
43161
|
+
const assetId = buildAssetId(storedName, usedIds);
|
|
43162
|
+
pending.push({
|
|
43163
|
+
id: assetId,
|
|
43164
|
+
filename: preferredFilenames.get(storedName) || storedName,
|
|
43165
|
+
contentType,
|
|
43166
|
+
buffer,
|
|
43167
|
+
storedName
|
|
43168
|
+
});
|
|
43169
|
+
placeholderByStoredName.set(storedName, `\${asset:${assetId}}`);
|
|
43170
|
+
}
|
|
43171
|
+
const content = replaceUploadReferences(lesson.content ?? "", placeholderByStoredName);
|
|
43172
|
+
const validSegmentIds = new Set(timeline.map((segment) => segment.id));
|
|
43173
|
+
const resolvedElements = elements.map((element) => {
|
|
43174
|
+
const { segmentId: rawSegmentId, ...rest } = element;
|
|
43175
|
+
const segmentId = rawSegmentId ? idRemap.get(rawSegmentId) ?? rawSegmentId : void 0;
|
|
43176
|
+
return {
|
|
43177
|
+
...rest,
|
|
43178
|
+
...segmentId && validSegmentIds.has(segmentId) ? { segmentId } : {},
|
|
43179
|
+
data: replaceUploadReferences(element.data, placeholderByStoredName)
|
|
43180
|
+
};
|
|
43181
|
+
});
|
|
43182
|
+
const assets = pending.map((asset) => ({
|
|
43183
|
+
id: asset.id,
|
|
43184
|
+
filename: asset.filename,
|
|
43185
|
+
contentType: asset.contentType,
|
|
43186
|
+
base64: asset.buffer.toString("base64")
|
|
43187
|
+
}));
|
|
43188
|
+
stats.assets = assets.length;
|
|
43189
|
+
const pkg = {
|
|
43190
|
+
format: COURSE_PACKAGE_FORMAT,
|
|
43191
|
+
version: COURSE_PACKAGE_VERSION,
|
|
43192
|
+
meta: {
|
|
43193
|
+
// 从课程包导入来的课程沿用原包身份;自行创建的课程用 lesson id ——
|
|
43194
|
+
// 改了标题再导出,身份不变(导入端去重仍然认得「同一个包」)。
|
|
43195
|
+
id: lesson.source_package_id || lesson.id,
|
|
43196
|
+
name: lesson.title,
|
|
43197
|
+
description: `\u7531 OpenLearn \u8BFE\u7A0B\u300C${lesson.title}\u300D\u5BFC\u51FA\uFF1A\u5305\u542B\u6559\u5B66\u73AF\u8282\u3001\u767D\u677F\u5143\u7D20\u4E0E ${assets.length} \u4E2A\u5185\u8054\u9644\u4EF6\u3002`,
|
|
43198
|
+
author: lesson.creator_id ?? "",
|
|
43199
|
+
tags: ["\u5BFC\u51FA", "\u8BFE\u7A0B\u5305"]
|
|
43200
|
+
},
|
|
43201
|
+
lesson: {
|
|
43202
|
+
title: lesson.title,
|
|
43203
|
+
content,
|
|
43204
|
+
progressMode: lesson.progress_mode || "manual",
|
|
43205
|
+
timeline
|
|
43206
|
+
},
|
|
43207
|
+
assets,
|
|
43208
|
+
elements: resolvedElements
|
|
43209
|
+
};
|
|
43210
|
+
const parsed = parseCoursePackage(pkg);
|
|
43211
|
+
if (!parsed.ok) {
|
|
43212
|
+
throw new CoursePackageExportError(`\u5BFC\u51FA\u7684\u8BFE\u7A0B\u5305\u672A\u901A\u8FC7\u7ED3\u6784\u6821\u9A8C\uFF1A${parsed.error}`, 500, "EXPORT_INVALID_PACKAGE");
|
|
43213
|
+
}
|
|
43214
|
+
return { package: parsed.package, stats };
|
|
43215
|
+
}
|
|
43216
|
+
function buildExportFilename(title) {
|
|
43217
|
+
const safe = Array.from(title).filter((ch) => {
|
|
43218
|
+
const code = ch.charCodeAt(0);
|
|
43219
|
+
return code >= 32 && code !== 127;
|
|
43220
|
+
}).join("").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
43221
|
+
return `${safe || "course"}.openlearn-package.json`;
|
|
43222
|
+
}
|
|
43223
|
+
function collectUploadReferences(value, out) {
|
|
43224
|
+
if (typeof value === "string") {
|
|
43225
|
+
for (const match of value.matchAll(UPLOAD_URL_PATTERN)) {
|
|
43226
|
+
out.add(match[1]);
|
|
43227
|
+
}
|
|
43228
|
+
return;
|
|
43229
|
+
}
|
|
43230
|
+
if (Array.isArray(value)) {
|
|
43231
|
+
for (const item of value) collectUploadReferences(item, out);
|
|
43232
|
+
return;
|
|
43233
|
+
}
|
|
43234
|
+
if (value && typeof value === "object") {
|
|
43235
|
+
for (const item of Object.values(value)) collectUploadReferences(item, out);
|
|
43236
|
+
}
|
|
43237
|
+
}
|
|
43238
|
+
function replaceUploadReferences(value, placeholders) {
|
|
43239
|
+
if (typeof value === "string") {
|
|
43240
|
+
return value.replace(UPLOAD_URL_PATTERN, (match, storedName) => placeholders.get(storedName) ?? match);
|
|
43241
|
+
}
|
|
43242
|
+
if (Array.isArray(value)) {
|
|
43243
|
+
return value.map((item) => replaceUploadReferences(item, placeholders));
|
|
43244
|
+
}
|
|
43245
|
+
if (value && typeof value === "object") {
|
|
43246
|
+
const out = {};
|
|
43247
|
+
for (const [key, item] of Object.entries(value)) {
|
|
43248
|
+
out[key] = replaceUploadReferences(item, placeholders);
|
|
43249
|
+
}
|
|
43250
|
+
return out;
|
|
43251
|
+
}
|
|
43252
|
+
return value;
|
|
43253
|
+
}
|
|
43254
|
+
function collectPreferredFilenames(value, out) {
|
|
43255
|
+
if (!value || typeof value !== "object") return;
|
|
43256
|
+
if (Array.isArray(value)) {
|
|
43257
|
+
for (const item of value) collectPreferredFilenames(item, out);
|
|
43258
|
+
return;
|
|
43259
|
+
}
|
|
43260
|
+
const obj = value;
|
|
43261
|
+
if (typeof obj.fileUrl === "string" && typeof obj.fileName === "string" && obj.fileName.trim()) {
|
|
43262
|
+
for (const match of obj.fileUrl.matchAll(UPLOAD_URL_PATTERN)) {
|
|
43263
|
+
if (!out.has(match[1])) out.set(match[1], obj.fileName.trim());
|
|
43264
|
+
}
|
|
43265
|
+
}
|
|
43266
|
+
for (const item of Object.values(obj)) collectPreferredFilenames(item, out);
|
|
43267
|
+
}
|
|
43268
|
+
function liftElementGeometry(data) {
|
|
43269
|
+
const lifted = {};
|
|
43270
|
+
const take = (key) => {
|
|
43271
|
+
const value = data[key];
|
|
43272
|
+
delete data[key];
|
|
43273
|
+
return value;
|
|
43274
|
+
};
|
|
43275
|
+
const segmentId = take("segmentId");
|
|
43276
|
+
if (typeof segmentId === "string" && segmentId) lifted.segmentId = segmentId;
|
|
43277
|
+
const page = take("page");
|
|
43278
|
+
if (typeof page === "number" && Number.isInteger(page) && page >= 0) lifted.page = page;
|
|
43279
|
+
const pageId = take("pageId");
|
|
43280
|
+
if (typeof pageId === "string" && pageId) lifted.pageId = pageId;
|
|
43281
|
+
const x = take("x");
|
|
43282
|
+
if (typeof x === "number" && Number.isFinite(x)) lifted.x = x;
|
|
43283
|
+
const y = take("y");
|
|
43284
|
+
if (typeof y === "number" && Number.isFinite(y)) lifted.y = y;
|
|
43285
|
+
const width = take("width");
|
|
43286
|
+
if (typeof width === "number" && width > 0) lifted.width = width;
|
|
43287
|
+
const height = take("height");
|
|
43288
|
+
if (typeof height === "number" && height > 0) lifted.height = height;
|
|
43289
|
+
return lifted;
|
|
43290
|
+
}
|
|
43291
|
+
function normalizeTimeline(raw) {
|
|
43292
|
+
const idRemap = /* @__PURE__ */ new Map();
|
|
43293
|
+
if (!Array.isArray(raw)) return { timeline: [], idRemap };
|
|
43294
|
+
const timeline = [];
|
|
43295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
43296
|
+
raw.forEach((item, index) => {
|
|
43297
|
+
if (!item || typeof item !== "object") return;
|
|
43298
|
+
const source = item;
|
|
43299
|
+
const originalId = typeof source.id === "string" && source.id.trim() ? source.id.trim() : "";
|
|
43300
|
+
let id = originalId || `seg-${index + 1}`;
|
|
43301
|
+
if (seen.has(id)) {
|
|
43302
|
+
let suffix = 2;
|
|
43303
|
+
while (seen.has(`${id}-${suffix}`)) suffix += 1;
|
|
43304
|
+
id = `${id}-${suffix}`;
|
|
43305
|
+
}
|
|
43306
|
+
if (originalId && originalId !== id) idRemap.set(originalId, id);
|
|
43307
|
+
seen.add(id);
|
|
43308
|
+
const title = typeof source.title === "string" && source.title.trim() ? source.title.trim() : `\u73AF\u8282 ${index + 1}`;
|
|
43309
|
+
const type = typeof source.type === "string" && source.type.trim() ? source.type.trim() : "lecture";
|
|
43310
|
+
const segment = { id, title, type };
|
|
43311
|
+
if (typeof source.duration === "string" && source.duration.trim()) segment.duration = source.duration.trim();
|
|
43312
|
+
if (typeof source.notes === "string") segment.notes = source.notes;
|
|
43313
|
+
if (typeof source.color === "string" && source.color.trim()) segment.color = source.color.trim();
|
|
43314
|
+
if (Array.isArray(source.gates)) {
|
|
43315
|
+
const gates = source.gates.filter(
|
|
43316
|
+
(gate) => Boolean(gate) && typeof gate === "object" && typeof gate.type === "string"
|
|
43317
|
+
);
|
|
43318
|
+
if (gates.length > 0) segment.gates = gates;
|
|
43319
|
+
}
|
|
43320
|
+
timeline.push(segment);
|
|
43321
|
+
});
|
|
43322
|
+
return { timeline, idRemap };
|
|
43323
|
+
}
|
|
43324
|
+
function resolveUploadsPath(storedName) {
|
|
43325
|
+
const uploadsDir = import_node_path21.default.resolve(getUploadsDir());
|
|
43326
|
+
if (import_node_path21.default.basename(storedName) !== storedName) return null;
|
|
43327
|
+
const target = import_node_path21.default.resolve(uploadsDir, storedName);
|
|
43328
|
+
if (import_node_path21.default.dirname(target) !== uploadsDir) return null;
|
|
43329
|
+
try {
|
|
43330
|
+
const stat = import_node_fs21.default.statSync(target);
|
|
43331
|
+
return stat.isFile() ? target : null;
|
|
43332
|
+
} catch {
|
|
43333
|
+
return null;
|
|
43334
|
+
}
|
|
43335
|
+
}
|
|
43336
|
+
function buildAssetId(storedName, used) {
|
|
43337
|
+
const digest2 = (0, import_node_crypto15.createHash)("sha1").update(storedName).digest("hex").slice(0, 12);
|
|
43338
|
+
let id = `asset-${digest2}`;
|
|
43339
|
+
let suffix = 2;
|
|
43340
|
+
while (used.has(id)) {
|
|
43341
|
+
id = `asset-${digest2}-${suffix}`;
|
|
43342
|
+
suffix += 1;
|
|
43343
|
+
}
|
|
43344
|
+
used.add(id);
|
|
43345
|
+
return id;
|
|
43346
|
+
}
|
|
43347
|
+
function safeJsonParse(raw) {
|
|
43348
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
43349
|
+
try {
|
|
43350
|
+
return JSON.parse(raw);
|
|
43351
|
+
} catch {
|
|
43352
|
+
return null;
|
|
43353
|
+
}
|
|
43354
|
+
}
|
|
43355
|
+
|
|
42899
43356
|
// server/routes/course-packages.ts
|
|
42900
43357
|
var TEMPLATE_FILENAME = "openlearn-course-template.json";
|
|
42901
43358
|
var MAX_DECODED_ASSET_BYTES = COURSE_PACKAGE_MAX_FILE_BYTES;
|
|
@@ -42955,6 +43412,32 @@ function registerCoursePackageRoutes(ctx) {
|
|
|
42955
43412
|
sendSafeError(res, e);
|
|
42956
43413
|
}
|
|
42957
43414
|
});
|
|
43415
|
+
app.get("/api/lessons/:id/export-package", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
43416
|
+
try {
|
|
43417
|
+
const { id } = req.params;
|
|
43418
|
+
const ownership = checkLessonOwnership(req, id);
|
|
43419
|
+
if (!ownership.allowed) {
|
|
43420
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
43421
|
+
}
|
|
43422
|
+
const { package: pkg, stats } = await exportLessonAsCoursePackage(id);
|
|
43423
|
+
const filename = buildExportFilename(pkg.lesson.title);
|
|
43424
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
43425
|
+
res.setHeader(
|
|
43426
|
+
"Content-Disposition",
|
|
43427
|
+
`attachment; filename="course-package.json"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
43428
|
+
);
|
|
43429
|
+
res.setHeader("X-Course-Package-Filename", encodeURIComponent(filename));
|
|
43430
|
+
res.setHeader("X-Course-Package-Assets", String(stats.assets));
|
|
43431
|
+
res.setHeader("X-Course-Package-Skipped", String(stats.skipped.length + stats.skippedElements));
|
|
43432
|
+
res.send(JSON.stringify(pkg, null, 2));
|
|
43433
|
+
} catch (e) {
|
|
43434
|
+
if (e instanceof CoursePackageExportError) {
|
|
43435
|
+
return res.status(e.status).json({ success: false, code: e.code, error: e.message });
|
|
43436
|
+
}
|
|
43437
|
+
console.error("[CoursePackage] \u5BFC\u51FA\u5931\u8D25:", e);
|
|
43438
|
+
sendSafeError(res, e);
|
|
43439
|
+
}
|
|
43440
|
+
});
|
|
42958
43441
|
}
|
|
42959
43442
|
|
|
42960
43443
|
// server/routes/workspace.ts
|
|
@@ -44991,6 +45474,26 @@ function resolveWritableStudentId(session, rawId) {
|
|
|
44991
45474
|
if (isTeacher(session) && teacherCanSeeStudent(kernelContainer.getReadDb(), session, studentId)) return studentId;
|
|
44992
45475
|
return null;
|
|
44993
45476
|
}
|
|
45477
|
+
var SEED_STATUS_TTL_MS = 15e3;
|
|
45478
|
+
var seedStatusCache = null;
|
|
45479
|
+
function readSeedDefaultStatus() {
|
|
45480
|
+
const now = Date.now();
|
|
45481
|
+
if (seedStatusCache && seedStatusCache.expiresAt > now) return seedStatusCache.payload;
|
|
45482
|
+
const accounts = [];
|
|
45483
|
+
try {
|
|
45484
|
+
const rows = kernelContainer.db.prepare("SELECT username, password_hash FROM users WHERE username IN (?, ?)").all("admin", "teacher");
|
|
45485
|
+
for (const row of rows) {
|
|
45486
|
+
if (verifyPassword(row.username, row.password_hash).valid) accounts.push(row.username);
|
|
45487
|
+
}
|
|
45488
|
+
} catch {
|
|
45489
|
+
}
|
|
45490
|
+
const payload = { active: accounts.length > 0, accounts };
|
|
45491
|
+
seedStatusCache = { payload, expiresAt: now + SEED_STATUS_TTL_MS };
|
|
45492
|
+
return payload;
|
|
45493
|
+
}
|
|
45494
|
+
function invalidateSeedStatusCache() {
|
|
45495
|
+
seedStatusCache = null;
|
|
45496
|
+
}
|
|
44994
45497
|
function registerRosterRoutes(ctx) {
|
|
44995
45498
|
const { app, io, loginLimiter } = ctx;
|
|
44996
45499
|
const rosterService = new RosterService(kernelContainer.db, kernelContainer.getReadDb());
|
|
@@ -45272,6 +45775,7 @@ function registerRosterRoutes(ctx) {
|
|
|
45272
45775
|
return res.status(400).json({ error: "New password must be different from the username" });
|
|
45273
45776
|
}
|
|
45274
45777
|
kernelContainer.db.prepare("UPDATE users SET password_hash = ?, must_change_password = 0 WHERE id = ?").run(hashPassword(newPassword), session.userId);
|
|
45778
|
+
invalidateSeedStatusCache();
|
|
45275
45779
|
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id != ? AND session_data LIKE ?").run(token, `%${session.userId}%`);
|
|
45276
45780
|
try {
|
|
45277
45781
|
const updatedSession = { ...session, mustChangePassword: false };
|
|
@@ -45439,6 +45943,13 @@ function registerRosterRoutes(ctx) {
|
|
|
45439
45943
|
sendSafeError(res, e);
|
|
45440
45944
|
}
|
|
45441
45945
|
});
|
|
45946
|
+
app.get("/api/auth/seed-status", (_req, res) => {
|
|
45947
|
+
try {
|
|
45948
|
+
res.json(readSeedDefaultStatus());
|
|
45949
|
+
} catch (e) {
|
|
45950
|
+
sendSafeError(res, e);
|
|
45951
|
+
}
|
|
45952
|
+
});
|
|
45442
45953
|
app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
45443
45954
|
try {
|
|
45444
45955
|
const { entrance, username, password, studentId } = req.body;
|
|
@@ -46457,6 +46968,98 @@ function parseAllowedExt(raw) {
|
|
|
46457
46968
|
return set.size > 0 ? set : null;
|
|
46458
46969
|
}
|
|
46459
46970
|
|
|
46971
|
+
// packages/core/db/assignment-resubmit.ts
|
|
46972
|
+
var import_node_crypto16 = __toESM(require("node:crypto"), 1);
|
|
46973
|
+
var RESUBMIT_CONTENT_REPLACED_REASON = "resubmit-content-replaced";
|
|
46974
|
+
function keepStatus(current) {
|
|
46975
|
+
return current === "graded" || current === "absent" ? current : "submitted";
|
|
46976
|
+
}
|
|
46977
|
+
function writeContentReplacementHistory(db2, input) {
|
|
46978
|
+
const historyId = `agh-${import_node_crypto16.default.randomUUID()}`;
|
|
46979
|
+
try {
|
|
46980
|
+
db2.prepare(
|
|
46981
|
+
`INSERT INTO assignment_grade_history
|
|
46982
|
+
(id, assignment_id, student_id, old_score, new_score, old_status, new_status,
|
|
46983
|
+
old_source, new_source, actor_id, actor_role, reason, created_at, old_content)
|
|
46984
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
46985
|
+
).run(
|
|
46986
|
+
historyId,
|
|
46987
|
+
input.assignmentId,
|
|
46988
|
+
input.studentId,
|
|
46989
|
+
input.score,
|
|
46990
|
+
input.score,
|
|
46991
|
+
input.status,
|
|
46992
|
+
input.status,
|
|
46993
|
+
input.source,
|
|
46994
|
+
input.source,
|
|
46995
|
+
input.actorId ?? null,
|
|
46996
|
+
input.actorRole ?? null,
|
|
46997
|
+
RESUBMIT_CONTENT_REPLACED_REASON,
|
|
46998
|
+
input.now,
|
|
46999
|
+
input.oldContent
|
|
47000
|
+
);
|
|
47001
|
+
return historyId;
|
|
47002
|
+
} catch (err) {
|
|
47003
|
+
console.warn("[assignment.resubmit] \u5185\u5BB9\u66FF\u6362\u7559\u75D5\u5931\u8D25:", err.message);
|
|
47004
|
+
return void 0;
|
|
47005
|
+
}
|
|
47006
|
+
}
|
|
47007
|
+
function applyResubmission(db2, input) {
|
|
47008
|
+
const assignmentId = String(input.assignmentId ?? "").trim();
|
|
47009
|
+
const studentId = String(input.studentId ?? "").trim();
|
|
47010
|
+
if (!assignmentId || !studentId) {
|
|
47011
|
+
return { ok: false, contentReplaced: false };
|
|
47012
|
+
}
|
|
47013
|
+
const now = input.now ?? Date.now();
|
|
47014
|
+
const content = input.content ?? "";
|
|
47015
|
+
let result = { ok: false, contentReplaced: false };
|
|
47016
|
+
const write = () => {
|
|
47017
|
+
const existing = db2.prepare(
|
|
47018
|
+
`SELECT content, score, feedback, status, source, academic_year
|
|
47019
|
+
FROM assignment_submissions
|
|
47020
|
+
WHERE assignment_id = ? AND student_id = ?
|
|
47021
|
+
LIMIT 1`
|
|
47022
|
+
).get(assignmentId, studentId);
|
|
47023
|
+
if (!existing) {
|
|
47024
|
+
db2.prepare(
|
|
47025
|
+
`INSERT INTO assignment_submissions
|
|
47026
|
+
(assignment_id, student_id, content, score, feedback, submitted_at, status)
|
|
47027
|
+
VALUES (?, ?, ?, NULL, NULL, ?, 'submitted')`
|
|
47028
|
+
).run(assignmentId, studentId, content, now);
|
|
47029
|
+
result = { ok: true, contentReplaced: false, status: "submitted" };
|
|
47030
|
+
return;
|
|
47031
|
+
}
|
|
47032
|
+
const nextStatus = keepStatus(existing.status);
|
|
47033
|
+
const contentChanged = (existing.content ?? "") !== content;
|
|
47034
|
+
const gradedRow = existing.status === "graded" || existing.status === "absent";
|
|
47035
|
+
const contentReplaced = gradedRow && contentChanged;
|
|
47036
|
+
let historyId;
|
|
47037
|
+
if (contentReplaced) {
|
|
47038
|
+
historyId = writeContentReplacementHistory(db2, {
|
|
47039
|
+
assignmentId,
|
|
47040
|
+
studentId,
|
|
47041
|
+
oldContent: existing.content,
|
|
47042
|
+
score: existing.score,
|
|
47043
|
+
status: existing.status,
|
|
47044
|
+
source: existing.source,
|
|
47045
|
+
actorId: input.actorId ?? null,
|
|
47046
|
+
actorRole: input.actorRole ?? null,
|
|
47047
|
+
now
|
|
47048
|
+
});
|
|
47049
|
+
}
|
|
47050
|
+
db2.prepare(
|
|
47051
|
+
`UPDATE assignment_submissions
|
|
47052
|
+
SET content = ?, submitted_at = ?, status = ?
|
|
47053
|
+
WHERE assignment_id = ? AND student_id = ?`
|
|
47054
|
+
).run(content, now, nextStatus, assignmentId, studentId);
|
|
47055
|
+
result = { ok: true, contentReplaced, historyId, status: nextStatus };
|
|
47056
|
+
};
|
|
47057
|
+
const tx = typeof db2.transaction === "function" ? db2.transaction(write) : null;
|
|
47058
|
+
if (typeof tx === "function") tx();
|
|
47059
|
+
else write();
|
|
47060
|
+
return result;
|
|
47061
|
+
}
|
|
47062
|
+
|
|
46460
47063
|
// server/services/assignment-service.ts
|
|
46461
47064
|
function safeSegment(value) {
|
|
46462
47065
|
const cleaned = String(value || "").replace(/[^A-Za-z0-9._-]/g, "");
|
|
@@ -47174,8 +47777,22 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47174
47777
|
const row = kernelContainer.db.prepare("SELECT class_id FROM plugin_assignments WHERE id = ?").get(assignmentId);
|
|
47175
47778
|
return row?.class_id;
|
|
47176
47779
|
};
|
|
47780
|
+
const requireAssignmentCommand = (res, ...types) => {
|
|
47781
|
+
for (const type of types) {
|
|
47782
|
+
if (!kernelContainer.commandBus.hasHandler(type)) {
|
|
47783
|
+
res.status(503).json({
|
|
47784
|
+
success: false,
|
|
47785
|
+
code: "ASSIGNMENT_SERVICE_UNAVAILABLE",
|
|
47786
|
+
error: "Assignment service is unavailable: the assignment-eval plugin is not active."
|
|
47787
|
+
});
|
|
47788
|
+
return true;
|
|
47789
|
+
}
|
|
47790
|
+
}
|
|
47791
|
+
return false;
|
|
47792
|
+
};
|
|
47177
47793
|
app.post("/api/assignments", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
47178
47794
|
try {
|
|
47795
|
+
if (requireAssignmentCommand(res, "assignment.create")) return;
|
|
47179
47796
|
const classId = (req.body || {}).classId || (req.body || {}).class_id;
|
|
47180
47797
|
if (classId && rejectUnlessHubClass(req, res, String(classId), "staff")) return;
|
|
47181
47798
|
const cmd = kernelContainer.commandBus.createCommand("assignment.create", req.body || {}, getActorId(req));
|
|
@@ -47187,6 +47804,7 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47187
47804
|
});
|
|
47188
47805
|
app.get("/api/assignments", requireScopedAuth("class-member"), async (req, res) => {
|
|
47189
47806
|
try {
|
|
47807
|
+
if (requireAssignmentCommand(res, "assignment.list")) return;
|
|
47190
47808
|
const payload = {
|
|
47191
47809
|
lessonId: req.query.lessonId,
|
|
47192
47810
|
classId: req.query.classId,
|
|
@@ -47217,6 +47835,7 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47217
47835
|
});
|
|
47218
47836
|
app.get("/api/assignments/:assignmentId", requireScopedAuth("class-member"), async (req, res) => {
|
|
47219
47837
|
try {
|
|
47838
|
+
if (requireAssignmentCommand(res, "assignment.get")) return;
|
|
47220
47839
|
const { isPrivileged: isPrivileged2, studentId } = describeRequester(req);
|
|
47221
47840
|
const payload = { assignmentId: req.params.assignmentId };
|
|
47222
47841
|
const requestedStudent = typeof req.query.studentId === "string" ? req.query.studentId : void 0;
|
|
@@ -47334,6 +47953,7 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47334
47953
|
});
|
|
47335
47954
|
app.post("/api/assignments/:assignmentId/submit", requireScopedAuth("class-member"), async (req, res) => {
|
|
47336
47955
|
try {
|
|
47956
|
+
if (requireAssignmentCommand(res, "assignment.submit")) return;
|
|
47337
47957
|
const { isPrivileged: isPrivileged2, studentId } = describeRequester(req);
|
|
47338
47958
|
const body = req.body || {};
|
|
47339
47959
|
const targetStudentId = isPrivileged2 ? String(body.studentId || "") : String(studentId || "");
|
|
@@ -47362,6 +47982,7 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47362
47982
|
});
|
|
47363
47983
|
app.post("/api/assignments/:assignmentId/peer-review", requireScopedAuth("class-member"), async (req, res) => {
|
|
47364
47984
|
try {
|
|
47985
|
+
if (requireAssignmentCommand(res, "assignment.peer_review")) return;
|
|
47365
47986
|
const { isPrivileged: isPrivileged2, studentId } = describeRequester(req);
|
|
47366
47987
|
const body = req.body || {};
|
|
47367
47988
|
const reviewerId = isPrivileged2 ? String(body.reviewerId || body.studentId || "") : String(studentId || "");
|
|
@@ -47397,6 +48018,7 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
47397
48018
|
requireAuth("teacher", "administrator"),
|
|
47398
48019
|
async (req, res) => {
|
|
47399
48020
|
try {
|
|
48021
|
+
if (requireAssignmentCommand(res, "assignment.assign_peer_reviews")) return;
|
|
47400
48022
|
if (rejectUnlessHubClass(req, res, hubAssignmentClassId(req.params.assignmentId), "staff")) return;
|
|
47401
48023
|
const body = req.body || {};
|
|
47402
48024
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
@@ -48899,7 +49521,7 @@ function registerGradingRoutes(ctx) {
|
|
|
48899
49521
|
}
|
|
48900
49522
|
|
|
48901
49523
|
// server/routes/plugins.ts
|
|
48902
|
-
var
|
|
49524
|
+
var import_node_crypto18 = require("node:crypto");
|
|
48903
49525
|
init_types();
|
|
48904
49526
|
init_install_utils();
|
|
48905
49527
|
var import_path22 = __toESM(require("path"), 1);
|
|
@@ -49081,7 +49703,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
49081
49703
|
init_audit_log();
|
|
49082
49704
|
|
|
49083
49705
|
// server/routes/plugin-api-gateway.ts
|
|
49084
|
-
var
|
|
49706
|
+
var import_node_path22 = __toESM(require("node:path"), 1);
|
|
49085
49707
|
init_http_router();
|
|
49086
49708
|
init_interfaces();
|
|
49087
49709
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
|
|
@@ -49255,7 +49877,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
49255
49877
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
49256
49878
|
return;
|
|
49257
49879
|
}
|
|
49258
|
-
const normalizedSubPath =
|
|
49880
|
+
const normalizedSubPath = import_node_path22.default.posix.normalize("/" + rawSubPath);
|
|
49259
49881
|
if (normalizedSubPath.includes("..")) {
|
|
49260
49882
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
49261
49883
|
return;
|
|
@@ -49692,10 +50314,10 @@ init_fetch_with_retry();
|
|
|
49692
50314
|
|
|
49693
50315
|
// server/services/community-registry.ts
|
|
49694
50316
|
var import_semver7 = __toESM(require("semver"), 1);
|
|
49695
|
-
var
|
|
49696
|
-
var
|
|
50317
|
+
var import_node_fs22 = __toESM(require("node:fs"), 1);
|
|
50318
|
+
var import_node_path23 = __toESM(require("node:path"), 1);
|
|
49697
50319
|
var import_node_os3 = __toESM(require("node:os"), 1);
|
|
49698
|
-
var
|
|
50320
|
+
var import_node_crypto17 = require("node:crypto");
|
|
49699
50321
|
var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
|
|
49700
50322
|
var REGISTRY_FETCH_TIMEOUT_MS = 1e4;
|
|
49701
50323
|
var PACKAGE_FETCH_TIMEOUT_MS = 6e4;
|
|
@@ -49915,8 +50537,8 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49915
50537
|
if (!response.body) {
|
|
49916
50538
|
throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
49917
50539
|
}
|
|
49918
|
-
const zipPath =
|
|
49919
|
-
const out =
|
|
50540
|
+
const zipPath = import_node_path23.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto17.randomUUID)()}.zip`);
|
|
50541
|
+
const out = import_node_fs22.default.createWriteStream(zipPath);
|
|
49920
50542
|
let received = 0;
|
|
49921
50543
|
try {
|
|
49922
50544
|
for await (const chunk of response.body) {
|
|
@@ -49938,13 +50560,13 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49938
50560
|
} catch (err) {
|
|
49939
50561
|
out.destroy();
|
|
49940
50562
|
try {
|
|
49941
|
-
|
|
50563
|
+
import_node_fs22.default.rmSync(zipPath, { force: true });
|
|
49942
50564
|
} catch {
|
|
49943
50565
|
}
|
|
49944
50566
|
throw err;
|
|
49945
50567
|
}
|
|
49946
50568
|
if (received === 0) {
|
|
49947
|
-
|
|
50569
|
+
import_node_fs22.default.rmSync(zipPath, { force: true });
|
|
49948
50570
|
throw new Error("\u63D2\u4EF6\u5305\u4E3A\u7A7A");
|
|
49949
50571
|
}
|
|
49950
50572
|
return { zipPath, filename: resolveFilename(response, downloadUrl), bytes: received };
|
|
@@ -50175,7 +50797,7 @@ function discardWriteStream(out, zipPath) {
|
|
|
50175
50797
|
}
|
|
50176
50798
|
function streamBodyToTempZip(req, res, maxBytes) {
|
|
50177
50799
|
return new Promise((resolve2, reject) => {
|
|
50178
|
-
const zipPath = import_path22.default.join(import_os2.default.tmpdir(), `plugin-upload-${(0,
|
|
50800
|
+
const zipPath = import_path22.default.join(import_os2.default.tmpdir(), `plugin-upload-${(0, import_node_crypto18.randomUUID)()}.zip`);
|
|
50179
50801
|
const out = import_fs19.default.createWriteStream(zipPath);
|
|
50180
50802
|
let received = 0;
|
|
50181
50803
|
let settled = false;
|
|
@@ -50230,7 +50852,7 @@ async function streamHttpBodyToFile(resp, maxBytes) {
|
|
|
50230
50852
|
if (!resp.body) {
|
|
50231
50853
|
throw new Error("\u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
50232
50854
|
}
|
|
50233
|
-
const zipPath = import_path22.default.join(import_os2.default.tmpdir(), `plugin-download-${(0,
|
|
50855
|
+
const zipPath = import_path22.default.join(import_os2.default.tmpdir(), `plugin-download-${(0, import_node_crypto18.randomUUID)()}.zip`);
|
|
50234
50856
|
const out = import_fs19.default.createWriteStream(zipPath);
|
|
50235
50857
|
let received = 0;
|
|
50236
50858
|
try {
|
|
@@ -51677,12 +52299,12 @@ function buildItemSnapshot(input) {
|
|
|
51677
52299
|
}
|
|
51678
52300
|
|
|
51679
52301
|
// packages/core/assessment/anonymous.ts
|
|
51680
|
-
var
|
|
52302
|
+
var import_node_crypto19 = require("node:crypto");
|
|
51681
52303
|
function deriveAnonKey(secret, assignmentId, studentId) {
|
|
51682
52304
|
if (!secret) throw new Error("[assessment] anonymous surveys require a server secret");
|
|
51683
52305
|
if (!assignmentId) throw new Error("[assessment] assignmentId is required to derive anon key");
|
|
51684
52306
|
if (!studentId) throw new Error("[assessment] studentId is required to derive anon key");
|
|
51685
|
-
return (0,
|
|
52307
|
+
return (0, import_node_crypto19.createHmac)("sha256", secret).update(`${assignmentId}:${studentId}`).digest("hex");
|
|
51686
52308
|
}
|
|
51687
52309
|
function anonymousLabel(anonKey, index) {
|
|
51688
52310
|
if (typeof index === "number") return `\u533F\u540D ${index + 1}`;
|
|
@@ -51905,7 +52527,7 @@ var notFound = (what) => new AssessmentError(404, `${what} not found`);
|
|
|
51905
52527
|
var badRequest = (msg) => new AssessmentError(400, msg);
|
|
51906
52528
|
var conflict = (msg) => new AssessmentError(409, msg);
|
|
51907
52529
|
var forbidden = (msg) => new AssessmentError(403, msg);
|
|
51908
|
-
function
|
|
52530
|
+
function safeJsonParse2(raw, fallback) {
|
|
51909
52531
|
if (!raw) return fallback;
|
|
51910
52532
|
try {
|
|
51911
52533
|
return JSON.parse(raw);
|
|
@@ -51989,8 +52611,8 @@ function toItemRecord(row) {
|
|
|
51989
52611
|
sortOrder: row.sort_order,
|
|
51990
52612
|
type: row.type,
|
|
51991
52613
|
stem: row.stem,
|
|
51992
|
-
config:
|
|
51993
|
-
answerKey:
|
|
52614
|
+
config: safeJsonParse2(row.config_json, {}),
|
|
52615
|
+
answerKey: safeJsonParse2(row.answer_key_json, null),
|
|
51994
52616
|
maxScore: row.max_score
|
|
51995
52617
|
};
|
|
51996
52618
|
}
|
|
@@ -52061,7 +52683,7 @@ var AssessmentService = class {
|
|
|
52061
52683
|
const items = this.readItems(id);
|
|
52062
52684
|
return {
|
|
52063
52685
|
...this.toSummary(row),
|
|
52064
|
-
settings: parseAssessmentSettings(row.kind,
|
|
52686
|
+
settings: parseAssessmentSettings(row.kind, safeJsonParse2(row.settings_json, {})),
|
|
52065
52687
|
items: items.map(toTeacherItemView)
|
|
52066
52688
|
};
|
|
52067
52689
|
}
|
|
@@ -52339,7 +52961,7 @@ var AssessmentService = class {
|
|
|
52339
52961
|
if (attempt.status !== "in_progress") {
|
|
52340
52962
|
throw badRequest("\u4F5C\u7B54\u5DF2\u63D0\u4EA4\u6216\u7ED3\u675F\uFF0C\u65E0\u6CD5\u8BB0\u5F55\u76D1\u8003\u4E8B\u4EF6");
|
|
52341
52963
|
}
|
|
52342
|
-
const currentProctor =
|
|
52964
|
+
const currentProctor = safeJsonParse2(attempt.proctor_json, {
|
|
52343
52965
|
blurCount: 0,
|
|
52344
52966
|
switchTabCount: 0,
|
|
52345
52967
|
fullscreenExitCount: 0,
|
|
@@ -52412,7 +53034,7 @@ var AssessmentService = class {
|
|
|
52412
53034
|
if (!att) {
|
|
52413
53035
|
notStartedCount++;
|
|
52414
53036
|
} else {
|
|
52415
|
-
proctor =
|
|
53037
|
+
proctor = safeJsonParse2(att.proctor_json, {});
|
|
52416
53038
|
submittedAt = att.submitted_at;
|
|
52417
53039
|
if (att.status === "in_progress") {
|
|
52418
53040
|
inProgressCount++;
|
|
@@ -52520,7 +53142,7 @@ var AssessmentService = class {
|
|
|
52520
53142
|
).all(...classIds);
|
|
52521
53143
|
const now = this.now();
|
|
52522
53144
|
const result = rows.map((row) => {
|
|
52523
|
-
const settings = parseAssessmentSettings(row.kind,
|
|
53145
|
+
const settings = parseAssessmentSettings(row.kind, safeJsonParse2(row.settings_json, {}));
|
|
52524
53146
|
const attempt = settings.anonymous ? this.findAttemptByIdentity(row.id, null, this.anonKeyFor(row.id, studentId)) : this.findAttemptByIdentity(row.id, studentId, null);
|
|
52525
53147
|
const settled = attempt ? this.settleIfExpired(attempt, now) : null;
|
|
52526
53148
|
const completed = Boolean(settled && settled.status !== "in_progress");
|
|
@@ -52571,7 +53193,7 @@ var AssessmentService = class {
|
|
|
52571
53193
|
const assessment = this.db.prepare("SELECT * FROM assessments WHERE id = ?").get(asg.assessmentId);
|
|
52572
53194
|
if (!assessment) throw notFound("assessment");
|
|
52573
53195
|
if (asg.status === "closed") throw conflict("assignment is closed");
|
|
52574
|
-
const settings = parseAssessmentSettings(assessment.kind,
|
|
53196
|
+
const settings = parseAssessmentSettings(assessment.kind, safeJsonParse2(assessment.settings_json, {}));
|
|
52575
53197
|
const now = this.now();
|
|
52576
53198
|
const windowState = readWindowState(this.windowOf(asg), now);
|
|
52577
53199
|
if (windowState === "before_open") throw conflict("assessment has not opened yet");
|
|
@@ -52683,7 +53305,7 @@ var AssessmentService = class {
|
|
|
52683
53305
|
type: item.type,
|
|
52684
53306
|
stem: item.stem,
|
|
52685
53307
|
maxScore: item.maxScore,
|
|
52686
|
-
answer: row ?
|
|
53308
|
+
answer: row ? safeJsonParse2(row.answer_json, null) : null,
|
|
52687
53309
|
answerKey: item.answerKey,
|
|
52688
53310
|
score: row?.score ?? null,
|
|
52689
53311
|
isCorrect: row?.is_correct === null || row?.is_correct === void 0 ? null : row.is_correct === 1,
|
|
@@ -52754,7 +53376,7 @@ var AssessmentService = class {
|
|
|
52754
53376
|
const definition = this.registry.get(item.type);
|
|
52755
53377
|
if (definition?.category !== "subjective") throw badRequest("AI \u5EFA\u8BAE\u4EC5\u9002\u7528\u4E8E\u4E3B\u89C2\u9898");
|
|
52756
53378
|
const answerRow = this.db.prepare("SELECT answer_json FROM assessment_answers WHERE attempt_id = ? AND item_id = ?").get(attemptId, itemId);
|
|
52757
|
-
const studentAnswer = answerRow?.answer_json ?
|
|
53379
|
+
const studentAnswer = answerRow?.answer_json ? safeJsonParse2(answerRow.answer_json, answerRow.answer_json) : null;
|
|
52758
53380
|
if (studentAnswer === null || studentAnswer === void 0 || typeof studentAnswer === "string" && studentAnswer.trim() === "") {
|
|
52759
53381
|
throw badRequest("\u8BE5\u9898\u5C1A\u65E0\u5B66\u751F\u4F5C\u7B54\uFF0C\u65E0\u6CD5\u751F\u6210\u6279\u6539\u5EFA\u8BAE");
|
|
52760
53382
|
}
|
|
@@ -52834,7 +53456,7 @@ var AssessmentService = class {
|
|
|
52834
53456
|
const correct = answers.filter((a) => a.is_correct === 1);
|
|
52835
53457
|
const distribution = {};
|
|
52836
53458
|
for (const answer of answers) {
|
|
52837
|
-
const parsed =
|
|
53459
|
+
const parsed = safeJsonParse2(answer.answer_json, null);
|
|
52838
53460
|
const key = Array.isArray(parsed) ? parsed.join(",") : String(parsed);
|
|
52839
53461
|
distribution[key] = (distribution[key] ?? 0) + 1;
|
|
52840
53462
|
}
|
|
@@ -52874,7 +53496,7 @@ var AssessmentService = class {
|
|
|
52874
53496
|
exportCsv(assignmentId, ownerId) {
|
|
52875
53497
|
const asg = this.requireOwnedAssignment(assignmentId, ownerId);
|
|
52876
53498
|
const assessment = this.db.prepare("SELECT * FROM assessments WHERE id = ?").get(asg.assessmentId);
|
|
52877
|
-
const settings = parseAssessmentSettings(assessment.kind,
|
|
53499
|
+
const settings = parseAssessmentSettings(assessment.kind, safeJsonParse2(assessment.settings_json, {}));
|
|
52878
53500
|
const items = this.readItems(asg.assessmentId).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
52879
53501
|
const attempts = this.db.prepare("SELECT * FROM assessment_attempts WHERE assignment_id = ? ORDER BY started_at ASC").all(assignmentId);
|
|
52880
53502
|
const now = this.now();
|
|
@@ -52895,7 +53517,7 @@ var AssessmentService = class {
|
|
|
52895
53517
|
const answerCells = items.map((item) => {
|
|
52896
53518
|
const answerRow = this.db.prepare("SELECT * FROM assessment_answers WHERE attempt_id = ? AND item_id = ?").get(row.id, item.id);
|
|
52897
53519
|
if (!answerRow) return "";
|
|
52898
|
-
const parsed =
|
|
53520
|
+
const parsed = safeJsonParse2(answerRow.answer_json, null);
|
|
52899
53521
|
const text = Array.isArray(parsed) ? parsed.map((v) => String(v)).join(" | ") : String(parsed ?? "");
|
|
52900
53522
|
const scorePart = answerRow.score === null ? "" : ` [${answerRow.score}]`;
|
|
52901
53523
|
const clipped = text.length > EXPORT_TEXT_LIMIT ? `${text.slice(0, EXPORT_TEXT_LIMIT)}\u2026` : text;
|
|
@@ -52935,7 +53557,7 @@ var AssessmentService = class {
|
|
|
52935
53557
|
async exportPaper(assessmentId, ownerId, format) {
|
|
52936
53558
|
const assessment = this.requireOwnedAssessment(assessmentId, ownerId);
|
|
52937
53559
|
const items = this.readItems(assessmentId).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
52938
|
-
const settings = parseAssessmentSettings(assessment.kind,
|
|
53560
|
+
const settings = parseAssessmentSettings(assessment.kind, safeJsonParse2(assessment.settings_json, {}));
|
|
52939
53561
|
const base = `assessment-${assessmentId}-paper`;
|
|
52940
53562
|
const paper = {
|
|
52941
53563
|
schema: PAPER_EXPORT_SCHEMA,
|
|
@@ -53291,7 +53913,7 @@ var AssessmentService = class {
|
|
|
53291
53913
|
description: row.description,
|
|
53292
53914
|
status: row.status,
|
|
53293
53915
|
academicYear: row.academic_year,
|
|
53294
|
-
settings: parseAssessmentSettings(row.kind,
|
|
53916
|
+
settings: parseAssessmentSettings(row.kind, safeJsonParse2(row.settings_json, {})),
|
|
53295
53917
|
itemCount: items.length,
|
|
53296
53918
|
totalScore: roundScore(items.reduce((s, i) => s + i.maxScore, 0)),
|
|
53297
53919
|
createdAt: row.created_at,
|
|
@@ -53320,7 +53942,7 @@ var AssessmentService = class {
|
|
|
53320
53942
|
return false;
|
|
53321
53943
|
}
|
|
53322
53944
|
if (rules.tags && rules.tags.length > 0) {
|
|
53323
|
-
const tags =
|
|
53945
|
+
const tags = safeJsonParse2(bank.tags_json, []);
|
|
53324
53946
|
if (!rules.tags.some((tag) => tags.includes(tag))) return false;
|
|
53325
53947
|
}
|
|
53326
53948
|
return true;
|
|
@@ -53336,7 +53958,7 @@ var AssessmentService = class {
|
|
|
53336
53958
|
paperItemIds(attempt) {
|
|
53337
53959
|
const raw = attempt.paper_json;
|
|
53338
53960
|
if (!raw) return null;
|
|
53339
|
-
const parsed =
|
|
53961
|
+
const parsed = safeJsonParse2(raw, {});
|
|
53340
53962
|
if (!Array.isArray(parsed.itemIds)) return null;
|
|
53341
53963
|
const ids = parsed.itemIds.filter((id) => typeof id === "string");
|
|
53342
53964
|
return ids.length > 0 ? ids : null;
|
|
@@ -53357,9 +53979,9 @@ var AssessmentService = class {
|
|
|
53357
53979
|
id: row.id,
|
|
53358
53980
|
type: row.type,
|
|
53359
53981
|
stem: row.stem,
|
|
53360
|
-
config:
|
|
53361
|
-
answerKey:
|
|
53362
|
-
tags:
|
|
53982
|
+
config: safeJsonParse2(row.config_json, {}),
|
|
53983
|
+
answerKey: safeJsonParse2(row.answer_key_json, null),
|
|
53984
|
+
tags: safeJsonParse2(row.tags_json, []),
|
|
53363
53985
|
difficulty: row.difficulty ?? null,
|
|
53364
53986
|
createdAt: row.created_at,
|
|
53365
53987
|
updatedAt: row.updated_at
|
|
@@ -53425,7 +54047,7 @@ var AssessmentService = class {
|
|
|
53425
54047
|
score: row.score,
|
|
53426
54048
|
maxScore: row.max_score,
|
|
53427
54049
|
needsManualReview: row.needs_manual_review === 1,
|
|
53428
|
-
proctor:
|
|
54050
|
+
proctor: safeJsonParse2(row.proctor_json, {})
|
|
53429
54051
|
};
|
|
53430
54052
|
}
|
|
53431
54053
|
/** 惰性结算:过期未交卷的尝试在这里落成 submitted(并立即判分)。 */
|
|
@@ -53449,11 +54071,11 @@ var AssessmentService = class {
|
|
|
53449
54071
|
regrade(attempt, now, submittedAt) {
|
|
53450
54072
|
const assessmentId = this.assignmentAssessmentId(attempt.assignment_id);
|
|
53451
54073
|
const assessment = this.db.prepare("SELECT * FROM assessments WHERE id = ?").get(assessmentId);
|
|
53452
|
-
const settings = parseAssessmentSettings(assessment.kind,
|
|
54074
|
+
const settings = parseAssessmentSettings(assessment.kind, safeJsonParse2(assessment.settings_json, {}));
|
|
53453
54075
|
const items = this.itemsForAttempt(attempt, this.readItems(assessmentId));
|
|
53454
54076
|
const answerRows = this.db.prepare("SELECT * FROM assessment_answers WHERE attempt_id = ?").all(attempt.id);
|
|
53455
54077
|
const answers = {};
|
|
53456
|
-
for (const row of answerRows) answers[row.item_id] =
|
|
54078
|
+
for (const row of answerRows) answers[row.item_id] = safeJsonParse2(row.answer_json, null);
|
|
53457
54079
|
const summary = gradeAttempt(this.registry, items, answers, {
|
|
53458
54080
|
assessmentId,
|
|
53459
54081
|
attemptId: attempt.id,
|
|
@@ -53526,7 +54148,7 @@ var AssessmentService = class {
|
|
|
53526
54148
|
const items = this.itemsForAttempt(attempt, this.readItems(assessment.id));
|
|
53527
54149
|
const answerRows = this.db.prepare("SELECT * FROM assessment_answers WHERE attempt_id = ?").all(attempt.id);
|
|
53528
54150
|
const answers = {};
|
|
53529
|
-
for (const row of answerRows) answers[row.item_id] =
|
|
54151
|
+
for (const row of answerRows) answers[row.item_id] = safeJsonParse2(row.answer_json, null);
|
|
53530
54152
|
let sortedItems = items.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
53531
54153
|
if (settings.shuffleItems) {
|
|
53532
54154
|
sortedItems = seededShuffle(sortedItems, attempt.id);
|
|
@@ -53571,7 +54193,7 @@ var AssessmentService = class {
|
|
|
53571
54193
|
buildResultPayload(attempt) {
|
|
53572
54194
|
const assignment = this.getAssignment(attempt.assignment_id);
|
|
53573
54195
|
const assessment = this.db.prepare("SELECT * FROM assessments WHERE id = ?").get(assignment.assessmentId);
|
|
53574
|
-
const settings = parseAssessmentSettings(assessment.kind,
|
|
54196
|
+
const settings = parseAssessmentSettings(assessment.kind, safeJsonParse2(assessment.settings_json, {}));
|
|
53575
54197
|
const items = this.itemsForAttempt(attempt, this.readItems(assessment.id)).sort(
|
|
53576
54198
|
(a, b) => a.sortOrder - b.sortOrder
|
|
53577
54199
|
);
|
|
@@ -53592,7 +54214,7 @@ var AssessmentService = class {
|
|
|
53592
54214
|
type: item.type,
|
|
53593
54215
|
stem: item.stem,
|
|
53594
54216
|
maxScore: item.maxScore,
|
|
53595
|
-
answer: row ?
|
|
54217
|
+
answer: row ? safeJsonParse2(row.answer_json, null) : null
|
|
53596
54218
|
};
|
|
53597
54219
|
if (!showAnalysis) {
|
|
53598
54220
|
return {
|