openlearn-next 0.3.18 → 0.3.20
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/dist/assets/{AdminDirectoryView-BtdFA-PR.js → AdminDirectoryView-CGo33RNh.js} +1 -1
- package/dist/assets/{BatchPickerModal-BQ_vsb5h.js → BatchPickerModal-CasfiwEQ.js} +1 -1
- package/dist/assets/{ClassesView-D_m_99Wk.js → ClassesView-DIyJwydJ.js} +1 -1
- package/dist/assets/{CloudDriveModal-BHWCchwE.js → CloudDriveModal-C6fpWLLE.js} +1 -1
- package/dist/assets/{ComputerLabView-n5hzKtEx.js → ComputerLabView-DGha_J8B.js} +1 -1
- package/dist/assets/CourseManagement-2o8YUTDb.js +1 -0
- package/dist/assets/{CourseWizardModal-1F_Ksxgf.js → CourseWizardModal-Clo7hCCp.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-B0JwbcQz.js → CoursewareHubPanel-CEK_QEc5.js} +1 -1
- package/dist/assets/{Dashboard-CgJNFUha.js → Dashboard-CBIiLzje.js} +1 -1
- package/dist/assets/{ExportWeightModal-B2wHglNt.js → ExportWeightModal-DkDuPsiM.js} +1 -1
- package/dist/assets/{HelpTour-C3G0EsYT.js → HelpTour-Dlc1vvy3.js} +1 -1
- package/dist/assets/{HelpView-Bgxlwpn_.js → HelpView-g1BzpeC9.js} +1 -1
- package/dist/assets/{ImportLessonsModal-xjfRYYa0.js → ImportLessonsModal-DMyN1EmC.js} +1 -1
- package/dist/assets/{ImportModal-ChLAX_xg.js → ImportModal-B2acbUT8.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-pi7sr3lt.js → InteractiveCoursewareViewer-DEicS9GQ.js} +1 -1
- package/dist/assets/{InteractiveWhiteboard-CsKeinZt.js → InteractiveWhiteboard-DbpOL5pI.js} +1 -1
- package/dist/assets/{LazyCourseware-Bw86DSzq.js → LazyCourseware-Bjy3qJWw.js} +2 -2
- package/dist/assets/{LazyWhiteboard-XI8iSIjM.js → LazyWhiteboard-D4Bj3dzG.js} +2 -2
- package/dist/assets/{LessonEditorView-3TnocL3v.js → LessonEditorView-DpU2g0Uq.js} +1 -1
- package/dist/assets/{LiveClassroomView-CQGaFkzD.js → LiveClassroomView-Bqrth_T2.js} +1 -1
- package/dist/assets/{NotificationDetailModal-CI4IfD2v.js → NotificationDetailModal-DC3AYUtD.js} +1 -1
- package/dist/assets/PluginView-S6TW3dLz.js +65 -0
- package/dist/assets/{ProcessLogsModal-uJqQQFyw.js → ProcessLogsModal-MtK-Pe4R.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-Bul00fIH.js → QuizGeneratorModal-Bh6XOC83.js} +1 -1
- package/dist/assets/{StudentAssignmentView-CaOXzBkt.js → StudentAssignmentView-BCwbft0k.js} +1 -1
- package/dist/assets/{StudentLessonView-6l-A6scB.js → StudentLessonView-CewqFUl5.js} +1 -1
- package/dist/assets/{StudentPreviewModal-CnWC2qso.js → StudentPreviewModal-Blww1Gv5.js} +1 -1
- package/dist/assets/{StudentView-BtGPu4nl.js → StudentView-CR-84lLY.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-CiRD12Rw.js → SystemResourceLibraryModal-Dyv2YJ7c.js} +1 -1
- package/dist/assets/{TeacherView-_iKPvpXI.js → TeacherView-Cszhv8bO.js} +2 -2
- package/dist/assets/{TimetableView-B0yL3omc.js → TimetableView-Do_LWpV2.js} +4 -4
- package/dist/assets/index-Ch_mD6B0.css +1 -0
- package/dist/assets/{index-AdOtUso0.js → index-CnuEzAfQ.js} +4 -4
- package/dist/assets/{vendor-icons-BHXru9a4.js → vendor-icons-B2DRJfZU.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/server.cjs +422 -232
- package/package.json +17 -17
- package/dist/assets/CourseManagement-DamuhaYN.js +0 -1
- package/dist/assets/PluginView-LokCtvB8.js +0 -65
- package/dist/assets/index-CM3Tw-Rg.css +0 -1
package/dist/server.cjs
CHANGED
|
@@ -549,15 +549,77 @@ var init_ai_event_bus = __esm({
|
|
|
549
549
|
}
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
-
// packages/core/di/
|
|
553
|
-
function
|
|
552
|
+
// packages/core/di/api-key-crypto.ts
|
|
553
|
+
function getEncryptionKey() {
|
|
554
|
+
if (_encryptionKey) return _encryptionKey;
|
|
555
|
+
const envPath = import_node_path.default.resolve(process.cwd(), ".env");
|
|
556
|
+
const keyHex = process.env.ENCRYPTION_KEY;
|
|
557
|
+
if (keyHex && keyHex.trim() !== "") {
|
|
558
|
+
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
559
|
+
return _encryptionKey;
|
|
560
|
+
}
|
|
561
|
+
if (import_node_fs.default.existsSync(envPath)) {
|
|
562
|
+
try {
|
|
563
|
+
const content = import_node_fs.default.readFileSync(envPath, "utf-8");
|
|
564
|
+
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
565
|
+
if (match && match[1].trim() !== "") {
|
|
566
|
+
const fileKeyHex = match[1].trim();
|
|
567
|
+
_encryptionKey = Buffer.from(fileKeyHex, "hex");
|
|
568
|
+
process.env.ENCRYPTION_KEY = fileKeyHex;
|
|
569
|
+
console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
|
|
570
|
+
return _encryptionKey;
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const newKey = import_node_crypto.default.randomBytes(32).toString("hex");
|
|
576
|
+
try {
|
|
577
|
+
if (import_node_fs.default.existsSync(envPath)) {
|
|
578
|
+
const existingContent = import_node_fs.default.readFileSync(envPath, "utf-8");
|
|
579
|
+
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
580
|
+
console.warn(
|
|
581
|
+
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
582
|
+
);
|
|
583
|
+
process.env.ENCRYPTION_KEY = newKey;
|
|
584
|
+
_encryptionKey = Buffer.from(newKey, "hex");
|
|
585
|
+
return _encryptionKey;
|
|
586
|
+
}
|
|
587
|
+
import_node_fs.default.appendFileSync(envPath, `
|
|
588
|
+
ENCRYPTION_KEY=${newKey}
|
|
589
|
+
`);
|
|
590
|
+
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
591
|
+
} else {
|
|
592
|
+
import_node_fs.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
593
|
+
`);
|
|
594
|
+
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
598
|
+
}
|
|
599
|
+
process.env.ENCRYPTION_KEY = newKey;
|
|
600
|
+
_encryptionKey = Buffer.from(newKey, "hex");
|
|
601
|
+
return _encryptionKey;
|
|
602
|
+
}
|
|
603
|
+
function encryptApiKey(plaintext) {
|
|
604
|
+
if (!plaintext) return "";
|
|
605
|
+
const key = getEncryptionKey();
|
|
606
|
+
const iv = import_node_crypto.default.randomBytes(16);
|
|
607
|
+
const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", key, iv);
|
|
608
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
609
|
+
const authTag = cipher.getAuthTag();
|
|
610
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
611
|
+
}
|
|
612
|
+
function looksLikeCiphertext(value) {
|
|
613
|
+
const parts = (value || "").split(":");
|
|
614
|
+
if (parts.length !== 3) return false;
|
|
615
|
+
return parts.every((p) => p.length > 0 && /^[0-9a-fA-F]+$/.test(p));
|
|
616
|
+
}
|
|
617
|
+
function decryptApiKey(encrypted) {
|
|
554
618
|
if (!encrypted) return "";
|
|
555
619
|
const parts = encrypted.split(":");
|
|
556
620
|
if (parts.length !== 3) return encrypted;
|
|
557
621
|
try {
|
|
558
|
-
const
|
|
559
|
-
if (!rawKey) return encrypted;
|
|
560
|
-
const key = rawKey.length === 64 ? Buffer.from(rawKey, "hex") : import_node_crypto.default.createHash("sha256").update(rawKey).digest();
|
|
622
|
+
const key = getEncryptionKey();
|
|
561
623
|
const iv = Buffer.from(parts[0], "hex");
|
|
562
624
|
const authTag = Buffer.from(parts[1], "hex");
|
|
563
625
|
const ciphertext = Buffer.from(parts[2], "hex");
|
|
@@ -565,15 +627,27 @@ function decryptKeyIfNeeded(encrypted) {
|
|
|
565
627
|
decipher.setAuthTag(authTag);
|
|
566
628
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
567
629
|
} catch {
|
|
630
|
+
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
568
631
|
return encrypted;
|
|
569
632
|
}
|
|
570
633
|
}
|
|
571
|
-
var import_node_crypto,
|
|
634
|
+
var import_node_crypto, import_node_fs, import_node_path, _encryptionKey;
|
|
635
|
+
var init_api_key_crypto = __esm({
|
|
636
|
+
"packages/core/di/api-key-crypto.ts"() {
|
|
637
|
+
import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
638
|
+
import_node_fs = __toESM(require("node:fs"), 1);
|
|
639
|
+
import_node_path = __toESM(require("node:path"), 1);
|
|
640
|
+
_encryptionKey = null;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// packages/core/di/ai-service.ts
|
|
645
|
+
var AIService;
|
|
572
646
|
var init_ai_service = __esm({
|
|
573
647
|
"packages/core/di/ai-service.ts"() {
|
|
574
|
-
import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
575
648
|
init_provider_gateway();
|
|
576
649
|
init_ai_event_bus();
|
|
650
|
+
init_api_key_crypto();
|
|
577
651
|
AIService = class {
|
|
578
652
|
constructor(db2) {
|
|
579
653
|
this.db = db2;
|
|
@@ -586,11 +660,20 @@ var init_ai_service = __esm({
|
|
|
586
660
|
if (!provider || !provider.api_key) {
|
|
587
661
|
throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002");
|
|
588
662
|
}
|
|
663
|
+
const apiKey = decryptApiKey(provider.api_key);
|
|
664
|
+
if (!apiKey || !apiKey.trim()) {
|
|
665
|
+
throw new Error("AI Provider API Key \u89E3\u5BC6\u4E3A\u7A7A\uFF0C\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58\u5BC6\u94A5\u3002");
|
|
666
|
+
}
|
|
667
|
+
if (looksLikeCiphertext(apiKey)) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
"AI Provider API Key \u89E3\u5BC6\u5931\u8D25\uFF1AENCRYPTION_KEY \u4E0E\u52A0\u5BC6\u65F6\u4E0D\u4E00\u81F4\uFF08\u5E38\u89C1\u4E8E PM2 \u5C06 ENCRYPTION_KEY \u7F6E\u7A7A\u6216\u5BC6\u94A5\u88AB\u8F6E\u6362\uFF09\u3002\u8BF7\u6062\u590D\u539F ENCRYPTION_KEY\uFF0C\u6216\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58 API Key\u3002"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
589
672
|
const config = {
|
|
590
673
|
id: provider.id,
|
|
591
674
|
name: provider.name,
|
|
592
675
|
apiUrl: provider.api_url,
|
|
593
|
-
apiKey
|
|
676
|
+
apiKey,
|
|
594
677
|
modelName: provider.model_name
|
|
595
678
|
};
|
|
596
679
|
return this.gateway.generateText(prompt, options, config);
|
|
@@ -969,18 +1052,18 @@ var init_http_router = __esm({
|
|
|
969
1052
|
function createLogger(component) {
|
|
970
1053
|
return logger.child({ component });
|
|
971
1054
|
}
|
|
972
|
-
var import_pino,
|
|
1055
|
+
var import_pino, import_node_fs2, import_node_path2, level, logDir, logFile, streams, logger;
|
|
973
1056
|
var init_logger = __esm({
|
|
974
1057
|
"server/utils/logger.ts"() {
|
|
975
1058
|
import_pino = __toESM(require("pino"), 1);
|
|
976
|
-
|
|
977
|
-
|
|
1059
|
+
import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
1060
|
+
import_node_path2 = __toESM(require("node:path"), 1);
|
|
978
1061
|
level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug");
|
|
979
|
-
logDir =
|
|
980
|
-
if (!
|
|
981
|
-
|
|
1062
|
+
logDir = import_node_path2.default.resolve(process.cwd(), "logs");
|
|
1063
|
+
if (!import_node_fs2.default.existsSync(logDir)) {
|
|
1064
|
+
import_node_fs2.default.mkdirSync(logDir, { recursive: true });
|
|
982
1065
|
}
|
|
983
|
-
logFile =
|
|
1066
|
+
logFile = import_node_path2.default.join(logDir, "openlearn.log");
|
|
984
1067
|
streams = [];
|
|
985
1068
|
if (process.env.NODE_ENV !== "production") {
|
|
986
1069
|
streams.push({
|
|
@@ -1002,7 +1085,7 @@ var init_logger = __esm({
|
|
|
1002
1085
|
}
|
|
1003
1086
|
streams.push({
|
|
1004
1087
|
level,
|
|
1005
|
-
stream:
|
|
1088
|
+
stream: import_node_fs2.default.createWriteStream(logFile, { flags: "a" })
|
|
1006
1089
|
});
|
|
1007
1090
|
logger = (0, import_pino.default)({ level }, import_pino.default.multistream(streams));
|
|
1008
1091
|
}
|
|
@@ -3509,12 +3592,12 @@ parentPort.on('message', async function(msg) {
|
|
|
3509
3592
|
});
|
|
3510
3593
|
`;
|
|
3511
3594
|
}
|
|
3512
|
-
var import_node_worker_threads, import_fs4,
|
|
3595
|
+
var import_node_worker_threads, import_fs4, import_node_path3, import_uuid12, ALL_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
|
|
3513
3596
|
var init_worker_manager = __esm({
|
|
3514
3597
|
"packages/core/worker-runtime/worker-manager.ts"() {
|
|
3515
3598
|
import_node_worker_threads = require("node:worker_threads");
|
|
3516
3599
|
import_fs4 = __toESM(require("fs"), 1);
|
|
3517
|
-
|
|
3600
|
+
import_node_path3 = __toESM(require("node:path"), 1);
|
|
3518
3601
|
init_transport();
|
|
3519
3602
|
init_service_host();
|
|
3520
3603
|
init_logger();
|
|
@@ -3802,7 +3885,7 @@ var init_worker_manager = __esm({
|
|
|
3802
3885
|
const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
|
|
3803
3886
|
let worker;
|
|
3804
3887
|
try {
|
|
3805
|
-
const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(
|
|
3888
|
+
const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(import_node_path3.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
|
|
3806
3889
|
worker = new import_node_worker_threads.Worker(new URL(bootstrapDataUrl), {
|
|
3807
3890
|
// Pass both the DB id (`pluginId` — used as the actor/registry key)
|
|
3808
3891
|
// and `manifestId` (used as the namespace prefix for command types).
|
|
@@ -4047,7 +4130,7 @@ __export(server_exports, {
|
|
|
4047
4130
|
});
|
|
4048
4131
|
module.exports = __toCommonJS(server_exports);
|
|
4049
4132
|
var import_express3 = __toESM(require("express"), 1);
|
|
4050
|
-
var
|
|
4133
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
4051
4134
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
4052
4135
|
var import_os2 = __toESM(require("os"), 1);
|
|
4053
4136
|
var import_child_process3 = require("child_process");
|
|
@@ -8698,13 +8781,14 @@ var import_fs5 = __toESM(require("fs"), 1);
|
|
|
8698
8781
|
var import_jszip2 = __toESM(require("jszip"), 1);
|
|
8699
8782
|
var import_path4 = __toESM(require("path"), 1);
|
|
8700
8783
|
var import_express = __toESM(require("express"), 1);
|
|
8784
|
+
var import_node_module = require("node:module");
|
|
8701
8785
|
var import_node_url = require("node:url");
|
|
8702
8786
|
|
|
8703
8787
|
// packages/core/esm-loader/install-utils.ts
|
|
8704
8788
|
var import_jszip = __toESM(require("jszip"), 1);
|
|
8705
|
-
var
|
|
8789
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
8706
8790
|
var os2 = __toESM(require("os"), 1);
|
|
8707
|
-
var
|
|
8791
|
+
var path5 = __toESM(require("path"), 1);
|
|
8708
8792
|
var import_uuid10 = require("uuid");
|
|
8709
8793
|
var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
|
|
8710
8794
|
async function bundlePlugin(entryCode, resolveDir) {
|
|
@@ -8727,12 +8811,12 @@ async function bundlePlugin(entryCode, resolveDir) {
|
|
|
8727
8811
|
setup(build) {
|
|
8728
8812
|
build.onResolve({ filter: /.*/ }, (args) => {
|
|
8729
8813
|
if (args.path.includes("core/di/interfaces")) {
|
|
8730
|
-
const tsPath =
|
|
8731
|
-
if (
|
|
8814
|
+
const tsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.ts");
|
|
8815
|
+
if (fs4.existsSync(tsPath)) {
|
|
8732
8816
|
return { path: tsPath };
|
|
8733
8817
|
}
|
|
8734
|
-
const jsPath =
|
|
8735
|
-
if (
|
|
8818
|
+
const jsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.js");
|
|
8819
|
+
if (fs4.existsSync(jsPath)) {
|
|
8736
8820
|
return { path: jsPath };
|
|
8737
8821
|
}
|
|
8738
8822
|
}
|
|
@@ -8788,22 +8872,22 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
8788
8872
|
throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
|
|
8789
8873
|
}
|
|
8790
8874
|
const entryCode = await entryFile.async("string");
|
|
8791
|
-
const tmpDir =
|
|
8875
|
+
const tmpDir = path5.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
|
|
8792
8876
|
try {
|
|
8793
|
-
|
|
8877
|
+
fs4.mkdirSync(tmpDir, { recursive: true });
|
|
8794
8878
|
for (const [name, file] of Object.entries(zip.files)) {
|
|
8795
8879
|
if (file.dir) continue;
|
|
8796
|
-
const filePath =
|
|
8797
|
-
const fileDir =
|
|
8798
|
-
|
|
8880
|
+
const filePath = path5.join(tmpDir, name);
|
|
8881
|
+
const fileDir = path5.dirname(filePath);
|
|
8882
|
+
fs4.mkdirSync(fileDir, { recursive: true });
|
|
8799
8883
|
const content = await file.async("nodebuffer");
|
|
8800
|
-
|
|
8884
|
+
fs4.writeFileSync(filePath, content);
|
|
8801
8885
|
}
|
|
8802
8886
|
const bundledCode = await bundlePlugin(entryCode, tmpDir);
|
|
8803
8887
|
return { manifest, bundledCode, entryFileName: manifest.main };
|
|
8804
8888
|
} finally {
|
|
8805
8889
|
try {
|
|
8806
|
-
|
|
8890
|
+
fs4.rmSync(tmpDir, { recursive: true, force: true });
|
|
8807
8891
|
} catch {
|
|
8808
8892
|
}
|
|
8809
8893
|
}
|
|
@@ -9605,6 +9689,7 @@ var PluginDistributionManager = class {
|
|
|
9605
9689
|
|
|
9606
9690
|
// packages/core/plugin-host/index.ts
|
|
9607
9691
|
init_http_router();
|
|
9692
|
+
var import_meta2 = {};
|
|
9608
9693
|
var VALID_TRANSITIONS = {
|
|
9609
9694
|
["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
|
|
9610
9695
|
["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
|
|
@@ -9635,6 +9720,22 @@ function createPluginStaticMiddleware(absDir) {
|
|
|
9635
9720
|
import_express.default.static(absDir)
|
|
9636
9721
|
];
|
|
9637
9722
|
}
|
|
9723
|
+
function resolveHostSdkDir() {
|
|
9724
|
+
const bases = [];
|
|
9725
|
+
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
9726
|
+
try {
|
|
9727
|
+
bases.push(import_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta2.url)));
|
|
9728
|
+
} catch {
|
|
9729
|
+
}
|
|
9730
|
+
for (const base of bases) {
|
|
9731
|
+
try {
|
|
9732
|
+
const req = (0, import_node_module.createRequire)(import_path4.default.join(base, "__openlearn_resolve__.js"));
|
|
9733
|
+
return import_path4.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
|
|
9734
|
+
} catch {
|
|
9735
|
+
}
|
|
9736
|
+
}
|
|
9737
|
+
return null;
|
|
9738
|
+
}
|
|
9638
9739
|
var PluginHost = class {
|
|
9639
9740
|
constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
|
|
9640
9741
|
this.serviceRegistry = serviceRegistry;
|
|
@@ -9721,6 +9822,33 @@ var PluginHost = class {
|
|
|
9721
9822
|
getPluginManifestPath(pluginId) {
|
|
9722
9823
|
return import_path4.default.join(this.getPluginDir(pluginId), "manifest.json");
|
|
9723
9824
|
}
|
|
9825
|
+
/**
|
|
9826
|
+
* 确保宿主的 @openlearn/plugin-sdk 能被插件解析到。
|
|
9827
|
+
*
|
|
9828
|
+
* 插件 index.js 位于 `<pluginsDir>/<id>/`,Node 只从该文件所在目录向上查找
|
|
9829
|
+
* node_modules。宿主的 node_modules(npx 缓存、仓库目录等)通常不在其祖先链上,
|
|
9830
|
+
* 于是 external 的 `@openlearn/plugin-sdk` 会抛 ERR_MODULE_NOT_FOUND。
|
|
9831
|
+
* 这里在 `<pluginsDir>/node_modules/@openlearn/plugin-sdk` 建立指向宿主 SDK 的
|
|
9832
|
+
* 链接,使该目录下所有插件共享宿主的同一份 SDK。
|
|
9833
|
+
*/
|
|
9834
|
+
ensureHostSdkResolution() {
|
|
9835
|
+
const linkPath = import_path4.default.join(this.pluginsDir, "node_modules", "@openlearn", "plugin-sdk");
|
|
9836
|
+
if (import_fs5.default.existsSync(linkPath)) return;
|
|
9837
|
+
const sdkDir = resolveHostSdkDir();
|
|
9838
|
+
if (!sdkDir) {
|
|
9839
|
+
console.warn(
|
|
9840
|
+
"[PluginHost] \u672A\u80FD\u5B9A\u4F4D\u5BBF\u4E3B\u7684 @openlearn/plugin-sdk\uFF0C\u63D2\u4EF6\u52A0\u8F7D\u53EF\u80FD\u56E0\u7F3A\u5C11\u8FD0\u884C\u65F6\u4F9D\u8D56\u800C\u5931\u8D25"
|
|
9841
|
+
);
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
try {
|
|
9845
|
+
import_fs5.default.mkdirSync(import_path4.default.dirname(linkPath), { recursive: true });
|
|
9846
|
+
import_fs5.default.symlinkSync(sdkDir, linkPath, "junction");
|
|
9847
|
+
console.log(`[PluginHost] Linked host @openlearn/plugin-sdk: ${linkPath} -> ${sdkDir}`);
|
|
9848
|
+
} catch (err) {
|
|
9849
|
+
console.warn("[PluginHost] \u5EFA\u7ACB @openlearn/plugin-sdk \u94FE\u63A5\u5931\u8D25:", err);
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9724
9852
|
// ── V5.2: RESTful API Gateway Dispatcher ───────────────────────────────
|
|
9725
9853
|
/**
|
|
9726
9854
|
* 向插件派发 HTTP 请求(自动路由到 Worker 隔离线程或 Inline 实例)
|
|
@@ -10421,6 +10549,7 @@ var PluginHost = class {
|
|
|
10421
10549
|
try {
|
|
10422
10550
|
let mod;
|
|
10423
10551
|
if (process.env.NODE_ENV !== "test" && row.file_path && import_fs5.default.existsSync(row.file_path)) {
|
|
10552
|
+
this.ensureHostSdkResolution();
|
|
10424
10553
|
const fileUrl = (0, import_node_url.pathToFileURL)(row.file_path);
|
|
10425
10554
|
mod = await import(`${fileUrl.href}?t=${Date.now()}`);
|
|
10426
10555
|
} else {
|
|
@@ -10843,8 +10972,14 @@ var PluginHost = class {
|
|
|
10843
10972
|
}
|
|
10844
10973
|
}
|
|
10845
10974
|
const state = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
|
|
10846
|
-
this.validateTransition(pluginId, state, "uninstalled" /* UNINSTALLED */);
|
|
10847
10975
|
const row = this.db.prepare("SELECT manifest, file_path FROM plugins WHERE id = ?").get(pluginId);
|
|
10976
|
+
if (state === "uninstalled" /* UNINSTALLED */ && !row) {
|
|
10977
|
+
this.pluginStates.delete(pluginId);
|
|
10978
|
+
return;
|
|
10979
|
+
}
|
|
10980
|
+
if (state !== "uninstalled" /* UNINSTALLED */) {
|
|
10981
|
+
this.validateTransition(pluginId, state, "uninstalled" /* UNINSTALLED */);
|
|
10982
|
+
}
|
|
10848
10983
|
const manifestId = (() => {
|
|
10849
10984
|
if (!row) return pluginId;
|
|
10850
10985
|
try {
|
|
@@ -19743,93 +19878,14 @@ function registerOfficialActivities(registry, actionRegistry) {
|
|
|
19743
19878
|
var IActivityRegistryToken = new Token("@openlearn/activity-ecosystem:IActivityRegistry");
|
|
19744
19879
|
|
|
19745
19880
|
// server/routes/os.ts
|
|
19746
|
-
var
|
|
19747
|
-
var
|
|
19881
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
19882
|
+
var import_fs8 = __toESM(require("fs"), 1);
|
|
19748
19883
|
var import_child_process = require("child_process");
|
|
19749
|
-
var
|
|
19884
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
19750
19885
|
var import_xss = require("xss");
|
|
19751
19886
|
|
|
19752
19887
|
// server/utils/crypto.ts
|
|
19753
|
-
|
|
19754
|
-
var import_fs8 = __toESM(require("fs"), 1);
|
|
19755
|
-
var import_path7 = __toESM(require("path"), 1);
|
|
19756
|
-
var _encryptionKey = null;
|
|
19757
|
-
function getEncryptionKey() {
|
|
19758
|
-
if (_encryptionKey) return _encryptionKey;
|
|
19759
|
-
const envPath = import_path7.default.resolve(process.cwd(), ".env");
|
|
19760
|
-
const keyHex = process.env.ENCRYPTION_KEY;
|
|
19761
|
-
if (keyHex && keyHex.trim() !== "") {
|
|
19762
|
-
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
19763
|
-
return _encryptionKey;
|
|
19764
|
-
}
|
|
19765
|
-
if (import_fs8.default.existsSync(envPath)) {
|
|
19766
|
-
try {
|
|
19767
|
-
const content = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
19768
|
-
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
19769
|
-
if (match && match[1].trim() !== "") {
|
|
19770
|
-
const fileKeyHex = match[1].trim();
|
|
19771
|
-
_encryptionKey = Buffer.from(fileKeyHex, "hex");
|
|
19772
|
-
process.env.ENCRYPTION_KEY = fileKeyHex;
|
|
19773
|
-
console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
|
|
19774
|
-
return _encryptionKey;
|
|
19775
|
-
}
|
|
19776
|
-
} catch {
|
|
19777
|
-
}
|
|
19778
|
-
}
|
|
19779
|
-
const newKey = import_crypto5.default.randomBytes(32).toString("hex");
|
|
19780
|
-
try {
|
|
19781
|
-
if (import_fs8.default.existsSync(envPath)) {
|
|
19782
|
-
const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
19783
|
-
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
19784
|
-
console.warn(
|
|
19785
|
-
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
19786
|
-
);
|
|
19787
|
-
process.env.ENCRYPTION_KEY = newKey;
|
|
19788
|
-
_encryptionKey = Buffer.from(newKey, "hex");
|
|
19789
|
-
return _encryptionKey;
|
|
19790
|
-
}
|
|
19791
|
-
import_fs8.default.appendFileSync(envPath, `
|
|
19792
|
-
ENCRYPTION_KEY=${newKey}
|
|
19793
|
-
`);
|
|
19794
|
-
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
19795
|
-
} else {
|
|
19796
|
-
import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
19797
|
-
`);
|
|
19798
|
-
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
19799
|
-
}
|
|
19800
|
-
} catch (e) {
|
|
19801
|
-
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
19802
|
-
}
|
|
19803
|
-
process.env.ENCRYPTION_KEY = newKey;
|
|
19804
|
-
_encryptionKey = Buffer.from(newKey, "hex");
|
|
19805
|
-
return _encryptionKey;
|
|
19806
|
-
}
|
|
19807
|
-
function encryptApiKey(plaintext) {
|
|
19808
|
-
if (!plaintext) return "";
|
|
19809
|
-
const key = getEncryptionKey();
|
|
19810
|
-
const iv = import_crypto5.default.randomBytes(16);
|
|
19811
|
-
const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
|
|
19812
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
19813
|
-
const authTag = cipher.getAuthTag();
|
|
19814
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
19815
|
-
}
|
|
19816
|
-
function decryptApiKey(encrypted) {
|
|
19817
|
-
if (!encrypted) return "";
|
|
19818
|
-
const parts = encrypted.split(":");
|
|
19819
|
-
if (parts.length !== 3) return encrypted;
|
|
19820
|
-
try {
|
|
19821
|
-
const key = getEncryptionKey();
|
|
19822
|
-
const iv = Buffer.from(parts[0], "hex");
|
|
19823
|
-
const authTag = Buffer.from(parts[1], "hex");
|
|
19824
|
-
const ciphertext = Buffer.from(parts[2], "hex");
|
|
19825
|
-
const decipher = import_crypto5.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
19826
|
-
decipher.setAuthTag(authTag);
|
|
19827
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
19828
|
-
} catch {
|
|
19829
|
-
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
19830
|
-
return encrypted;
|
|
19831
|
-
}
|
|
19832
|
-
}
|
|
19888
|
+
init_api_key_crypto();
|
|
19833
19889
|
function maskApiKey(key) {
|
|
19834
19890
|
if (!key || key.length <= 8) return key ? "****" : "";
|
|
19835
19891
|
return key.substring(0, 4) + "****" + key.substring(key.length - 4);
|
|
@@ -19847,8 +19903,8 @@ function detectPromptInjection(input) {
|
|
|
19847
19903
|
}
|
|
19848
19904
|
|
|
19849
19905
|
// server/routes/shared.ts
|
|
19850
|
-
var
|
|
19851
|
-
var
|
|
19906
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
19907
|
+
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
19852
19908
|
|
|
19853
19909
|
// server/utils/bridge-sdk.ts
|
|
19854
19910
|
var BRIDGE_SDK_CODE = `(function() {
|
|
@@ -20315,7 +20371,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20315
20371
|
const text = (el.textContent || el.innerText || '').trim();
|
|
20316
20372
|
if (text) {
|
|
20317
20373
|
logData.push("Selector '" + selector + "' matched text: '" + text + "'");
|
|
20318
|
-
const fractionMatch = text.match(/(
|
|
20374
|
+
const fractionMatch = text.match(/(\\d+(\\.\\d+)?)\\s*[\\/|\u4E4B]\\s*(\\d+)/);
|
|
20319
20375
|
if (fractionMatch) {
|
|
20320
20376
|
const num = parseFloat(fractionMatch[1]);
|
|
20321
20377
|
const den = parseFloat(fractionMatch[3]);
|
|
@@ -20325,7 +20381,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20325
20381
|
return { score: pct, log: logData };
|
|
20326
20382
|
}
|
|
20327
20383
|
}
|
|
20328
|
-
const match = text.match(
|
|
20384
|
+
const match = text.match(/\\d+(\\.\\d+)?/);
|
|
20329
20385
|
if (match) {
|
|
20330
20386
|
const num = parseFloat(match[0]);
|
|
20331
20387
|
if (!isNaN(num)) {
|
|
@@ -20366,7 +20422,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20366
20422
|
if (txt) {
|
|
20367
20423
|
const hasKey = txt.includes('\u5F97\u5206') || txt.includes('\u5206\u6570') || txt.includes('\u6210\u7EE9') || txt.toLowerCase().includes('score') || txt.toLowerCase().includes('points');
|
|
20368
20424
|
if (hasKey) {
|
|
20369
|
-
const m = txt.match(
|
|
20425
|
+
const m = txt.match(/\\d+(\\.\\d+)?/);
|
|
20370
20426
|
if (m) {
|
|
20371
20427
|
const val = parseFloat(m[0]);
|
|
20372
20428
|
logData.push("Fallback leaf <" + el.tagName + "> '" + txt + "' parsed: " + val);
|
|
@@ -20384,6 +20440,139 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20384
20440
|
return { score: null, log: logData };
|
|
20385
20441
|
}
|
|
20386
20442
|
|
|
20443
|
+
// --- RESULT-SCREEN AUTO SUBMIT ---------------------------------------
|
|
20444
|
+
// \u90E8\u5206\u8BFE\u4EF6\u7B54\u5B8C\u9898\u540E\u76F4\u63A5\u5207\u5230\u300C\u7ED3\u7B97 / \u7ED3\u679C\u9875\u300D\uFF0C\u5E76\u4E0D\u70B9\u51FB\u4EFB\u4F55\u63D0\u4EA4\u6309\u94AE\u3002
|
|
20445
|
+
// \u8FD9\u91CC\u76D1\u542C\u7ED3\u7B97\u9875\u51FA\u73B0\uFF0C\u81EA\u52A8\u6293\u53D6\u5206\u6570\u5E76\u4E0A\u62A5\u4E00\u6B21\u3002
|
|
20446
|
+
// \u53E3\u5F84\u4F18\u5148\u300C\u6B63\u786E\u9898\u6570 X/Y\u300D\u2192 \u767E\u5206\u5236\uFF0C\u5426\u5219\u56DE\u843D\u53EF\u89C1\u7684\u5206\u6570\u5143\u7D20\u3002
|
|
20447
|
+
var __lmsResultSubmitted = false;
|
|
20448
|
+
var __lmsResultScheduled = false;
|
|
20449
|
+
var __lmsResultPolling = false;
|
|
20450
|
+
|
|
20451
|
+
// \u5F3A\u7ED3\u675F\u4FE1\u53F7\uFF1A\u53EA\u8BA4\u7ED3\u7B97/\u7ED3\u679C\u9875\u6587\u6848\uFF0C\u907F\u514D\u7B54\u9898\u8FC7\u7A0B\u4E2D\u8BEF\u89E6\u53D1
|
|
20452
|
+
var __LMS_DONE_RE = /(\u95EF\u5173|\u6311\u6218|\u7B54\u9898|\u6D4B\u8BD5|\u6E38\u620F|\u672C\u8F6E|\u672C\u5173)(\u7ED3\u675F|\u5B8C\u6210|\u6210\u529F)|\u901A\u5173|\u7ED3\u7B97|\u67E5\u770B\u89E3\u6790|\u6B63\u786E\u7387|\u6700\u7EC8\u5F97\u5206|\u603B\u5F97\u5206|\u5168\u90E8\u7B54\u5B8C|\u7B54\u9898\u5B8C\u6BD5/;
|
|
20453
|
+
// \u7528 [0-9] / [^0-9] \u4EE3\u66FF \\d / \\s\uFF0C\u89C4\u907F\u6A21\u677F\u5B57\u7B26\u4E32\u7684\u8F6C\u4E49\u9677\u9631
|
|
20454
|
+
var __LMS_RATIO_RE = /([0-9]+(?:[.][0-9]+)?)[^0-9]*[/\uFF0F|\u4E4B][^0-9]*([0-9]+)/;
|
|
20455
|
+
var __LMS_NUM_RE = /[0-9]+(?:[.][0-9]+)?/;
|
|
20456
|
+
|
|
20457
|
+
function __lmsVisibleText() {
|
|
20458
|
+
try {
|
|
20459
|
+
if (!document.body) return '';
|
|
20460
|
+
if (typeof document.body.innerText === 'string') return document.body.innerText;
|
|
20461
|
+
return document.body.textContent || '';
|
|
20462
|
+
} catch (e) { return ''; }
|
|
20463
|
+
}
|
|
20464
|
+
|
|
20465
|
+
function __lmsIsShown(el) {
|
|
20466
|
+
try {
|
|
20467
|
+
if (!el || !el.getBoundingClientRect) return false;
|
|
20468
|
+
var rect = el.getBoundingClientRect();
|
|
20469
|
+
if (rect.width <= 0 && rect.height <= 0) return false;
|
|
20470
|
+
var node = el;
|
|
20471
|
+
while (node && node.nodeType === 1) {
|
|
20472
|
+
var st = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
20473
|
+
if (st && (st.display === 'none' || st.visibility === 'hidden')) return false;
|
|
20474
|
+
node = node.parentElement;
|
|
20475
|
+
}
|
|
20476
|
+
return true;
|
|
20477
|
+
} catch (e) { return false; }
|
|
20478
|
+
}
|
|
20479
|
+
|
|
20480
|
+
// \u4EC5\u4ECE\u300C\u53EF\u89C1\u300D\u7684\u5206\u6570\u5143\u7D20\u53D6\u503C\uFF0C\u907F\u514D\u7ED3\u7B97\u9875\u5C1A\u672A\u5C55\u5F00\u65F6\u8BFB\u5230\u9690\u85CF\u7684\u521D\u59CB\u503C 0
|
|
20481
|
+
function __lmsFindVisibleScore() {
|
|
20482
|
+
var selectors = [
|
|
20483
|
+
'#finalScore', '#score', '#scoreDisplay', '.score', '.final-score',
|
|
20484
|
+
'[id*="score" i]', '[id*="point" i]', '[id*="grade" i]'
|
|
20485
|
+
];
|
|
20486
|
+
for (var i = 0; i < selectors.length; i++) {
|
|
20487
|
+
var nodes = [];
|
|
20488
|
+
try { nodes = document.querySelectorAll(selectors[i]); } catch (e) { nodes = []; }
|
|
20489
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
20490
|
+
if (!__lmsIsShown(nodes[j])) continue;
|
|
20491
|
+
var txt = (nodes[j].textContent || '').trim();
|
|
20492
|
+
if (!txt) continue;
|
|
20493
|
+
var frac = txt.match(__LMS_RATIO_RE);
|
|
20494
|
+
if (frac) {
|
|
20495
|
+
var fn = parseFloat(frac[1]);
|
|
20496
|
+
var fd = parseFloat(frac[2]);
|
|
20497
|
+
if (fd > 0 && fn >= 0 && fn <= fd) return Math.round((fn / fd) * 10000) / 100;
|
|
20498
|
+
}
|
|
20499
|
+
var num = txt.match(__LMS_NUM_RE);
|
|
20500
|
+
if (num) return parseFloat(num[0]);
|
|
20501
|
+
}
|
|
20502
|
+
}
|
|
20503
|
+
return null;
|
|
20504
|
+
}
|
|
20505
|
+
|
|
20506
|
+
function __lmsExtractResultScore() {
|
|
20507
|
+
// 1) \u6B63\u786E\u9898\u6570 X/Y \u2192 \u767E\u5206\u5236\uFF08\u53E3\u5F84\u6700\u7A33\u5B9A\uFF09
|
|
20508
|
+
var ratioSelectors = [
|
|
20509
|
+
'#correctCount', '[id*="correct" i]', '[id*="accuracy" i]',
|
|
20510
|
+
'[class*="correct" i]', '[class*="accuracy" i]', '[id*="ratio" i]'
|
|
20511
|
+
];
|
|
20512
|
+
for (var i = 0; i < ratioSelectors.length; i++) {
|
|
20513
|
+
var nodes = [];
|
|
20514
|
+
try { nodes = document.querySelectorAll(ratioSelectors[i]); } catch (e) { nodes = []; }
|
|
20515
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
20516
|
+
if (!__lmsIsShown(nodes[j])) continue;
|
|
20517
|
+
var m = (nodes[j].textContent || '').trim().match(__LMS_RATIO_RE);
|
|
20518
|
+
if (m) {
|
|
20519
|
+
var num = parseFloat(m[1]);
|
|
20520
|
+
var den = parseFloat(m[2]);
|
|
20521
|
+
if (den > 0 && num >= 0 && num <= den) return Math.round((num / den) * 10000) / 100;
|
|
20522
|
+
}
|
|
20523
|
+
}
|
|
20524
|
+
}
|
|
20525
|
+
// 2) \u56DE\u843D\uFF1A\u53EF\u89C1\u5206\u6570\u5143\u7D20
|
|
20526
|
+
return __lmsFindVisibleScore();
|
|
20527
|
+
}
|
|
20528
|
+
|
|
20529
|
+
function __lmsMaybeSubmitResult() {
|
|
20530
|
+
if (__lmsResultSubmitted || __lmsResultPolling) return;
|
|
20531
|
+
if (!__LMS_DONE_RE.test(__lmsVisibleText())) return;
|
|
20532
|
+
|
|
20533
|
+
__lmsResultPolling = true;
|
|
20534
|
+
// \u7ED3\u7B97\u9875 DOM \u53EF\u80FD\u5206\u6B65\u6E32\u67D3\uFF0C\u77ED\u8F6E\u8BE2\u53D6\u6700\u540E\u4E00\u6B21\u6709\u6548\u5206\u6570
|
|
20535
|
+
var tries = 0;
|
|
20536
|
+
var delays = [0, 120, 250, 400, 700, 1100];
|
|
20537
|
+
var latest = null;
|
|
20538
|
+
|
|
20539
|
+
function poll() {
|
|
20540
|
+
var s = __lmsExtractResultScore();
|
|
20541
|
+
if (s !== null && !isNaN(s)) latest = s;
|
|
20542
|
+
tries++;
|
|
20543
|
+
if (tries < delays.length) {
|
|
20544
|
+
setTimeout(poll, delays[tries]);
|
|
20545
|
+
return;
|
|
20546
|
+
}
|
|
20547
|
+
__lmsResultPolling = false;
|
|
20548
|
+
if (latest !== null && !__lmsResultSubmitted) {
|
|
20549
|
+
__lmsResultSubmitted = true;
|
|
20550
|
+
logToServer('Result screen detected, auto-submitting score: ' + latest);
|
|
20551
|
+
window.LMS.submit({ score: latest, completion: 1.0, comment: '\u7ED3\u7B97\u9875\u81EA\u52A8\u63D0\u53D6\u5F97\u5206' });
|
|
20552
|
+
}
|
|
20553
|
+
}
|
|
20554
|
+
poll();
|
|
20555
|
+
}
|
|
20556
|
+
|
|
20557
|
+
function initResultWatcher() {
|
|
20558
|
+
try {
|
|
20559
|
+
if (!document.body) return;
|
|
20560
|
+
var observer = new MutationObserver(function() {
|
|
20561
|
+
if (__lmsResultSubmitted || __lmsResultScheduled) return;
|
|
20562
|
+
__lmsResultScheduled = true;
|
|
20563
|
+
setTimeout(function() {
|
|
20564
|
+
__lmsResultScheduled = false;
|
|
20565
|
+
__lmsMaybeSubmitResult();
|
|
20566
|
+
}, 600);
|
|
20567
|
+
});
|
|
20568
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
20569
|
+
// \u5C11\u6570\u8BFE\u4EF6\u9996\u5C4F\u5373\u7ED3\u7B97
|
|
20570
|
+
setTimeout(__lmsMaybeSubmitResult, 800);
|
|
20571
|
+
} catch (e) {
|
|
20572
|
+
logToServer('Error in initResultWatcher: ' + e.message);
|
|
20573
|
+
}
|
|
20574
|
+
}
|
|
20575
|
+
|
|
20387
20576
|
function attachListeners() {
|
|
20388
20577
|
try {
|
|
20389
20578
|
const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], .btn, .button');
|
|
@@ -20477,6 +20666,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20477
20666
|
});
|
|
20478
20667
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
20479
20668
|
attachListeners();
|
|
20669
|
+
initResultWatcher();
|
|
20480
20670
|
} catch (e) {
|
|
20481
20671
|
logToServer("Error in initAutoSubmit: " + e.message);
|
|
20482
20672
|
}
|
|
@@ -20509,7 +20699,7 @@ function validateMagicBytes(buffer, fileName) {
|
|
|
20509
20699
|
".webp": [[82, 73, 70, 70]]
|
|
20510
20700
|
// RIFF
|
|
20511
20701
|
};
|
|
20512
|
-
const ext =
|
|
20702
|
+
const ext = import_path7.default.extname(fileName || "").toLowerCase();
|
|
20513
20703
|
const signatures = MAGIC_BYTES[ext];
|
|
20514
20704
|
if (!signatures) return true;
|
|
20515
20705
|
return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
|
|
@@ -20531,7 +20721,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20531
20721
|
const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
|
|
20532
20722
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, session.studentId, "active");
|
|
20533
20723
|
if (!attempt) {
|
|
20534
|
-
const attemptId = "att_" +
|
|
20724
|
+
const attemptId = "att_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20535
20725
|
kernelContainer.db.prepare(
|
|
20536
20726
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20537
20727
|
).run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
|
|
@@ -20546,7 +20736,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20546
20736
|
} else if (session.role === "teacher" || session.role === "administrator") {
|
|
20547
20737
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
|
|
20548
20738
|
if (!attempt) {
|
|
20549
|
-
const attemptId = "att_teacher_" +
|
|
20739
|
+
const attemptId = "att_teacher_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20550
20740
|
kernelContainer.db.prepare(
|
|
20551
20741
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20552
20742
|
).run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
|
|
@@ -20564,7 +20754,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20564
20754
|
if (studentInfo.attempt_id === "guest-attempt") {
|
|
20565
20755
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
|
|
20566
20756
|
if (!attempt) {
|
|
20567
|
-
const attemptId = "att_guest_" +
|
|
20757
|
+
const attemptId = "att_guest_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20568
20758
|
kernelContainer.db.prepare(
|
|
20569
20759
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20570
20760
|
).run(attemptId, cwInfo.id, "guest", Date.now(), "active");
|
|
@@ -20648,7 +20838,7 @@ function registerOsRoutes(ctx) {
|
|
|
20648
20838
|
if (!filename || !base64Data) {
|
|
20649
20839
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
20650
20840
|
}
|
|
20651
|
-
const ext =
|
|
20841
|
+
const ext = import_path8.default.extname(filename).toLowerCase();
|
|
20652
20842
|
if (BLOCKED_EXTENSIONS.includes(ext)) {
|
|
20653
20843
|
return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
|
|
20654
20844
|
}
|
|
@@ -20660,13 +20850,13 @@ function registerOsRoutes(ctx) {
|
|
|
20660
20850
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
20661
20851
|
return res.status(400).json({ error: "File content does not match the declared file type." });
|
|
20662
20852
|
}
|
|
20663
|
-
const uploadsDir =
|
|
20664
|
-
if (!
|
|
20665
|
-
|
|
20853
|
+
const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
|
|
20854
|
+
if (!import_fs8.default.existsSync(uploadsDir)) {
|
|
20855
|
+
import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
|
|
20666
20856
|
}
|
|
20667
|
-
const uniqueName = `${Date.now()}-${
|
|
20668
|
-
const filePath =
|
|
20669
|
-
|
|
20857
|
+
const uniqueName = `${Date.now()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
|
|
20858
|
+
const filePath = import_path8.default.join(uploadsDir, uniqueName);
|
|
20859
|
+
import_fs8.default.writeFileSync(filePath, fileBuffer);
|
|
20670
20860
|
let slideCount = 1;
|
|
20671
20861
|
if (ext === ".pdf") {
|
|
20672
20862
|
try {
|
|
@@ -20879,8 +21069,8 @@ function registerOsRoutes(ctx) {
|
|
|
20879
21069
|
});
|
|
20880
21070
|
if (result && typeof result.agentText === "string") {
|
|
20881
21071
|
const now = Date.now();
|
|
20882
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
20883
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
21072
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "user", message, now);
|
|
21073
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
|
|
20884
21074
|
}
|
|
20885
21075
|
res.json({
|
|
20886
21076
|
success: true,
|
|
@@ -20929,8 +21119,8 @@ function registerOsRoutes(ctx) {
|
|
|
20929
21119
|
}
|
|
20930
21120
|
|
|
20931
21121
|
// server/routes/resources.ts
|
|
20932
|
-
var
|
|
20933
|
-
var
|
|
21122
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
21123
|
+
var import_fs9 = __toESM(require("fs"), 1);
|
|
20934
21124
|
function registerResourcesRoutes(ctx) {
|
|
20935
21125
|
const { app } = ctx;
|
|
20936
21126
|
app.get("/api/resources", (req, res) => {
|
|
@@ -20946,23 +21136,23 @@ function registerResourcesRoutes(ctx) {
|
|
|
20946
21136
|
const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
|
|
20947
21137
|
if (!resource) return res.status(404).send("Resource not found");
|
|
20948
21138
|
if (resource.type === "html") {
|
|
20949
|
-
const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ?
|
|
21139
|
+
const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ? import_path9.default.basename(resource.name.replace(/\\/g, "/")) : "index.html";
|
|
20950
21140
|
const existingCw2 = kernelContainer.db.prepare("SELECT id FROM courseware WHERE id = ?").get(resource.id);
|
|
20951
21141
|
if (!existingCw2) {
|
|
20952
21142
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "html", entryName, resource.created_at || Date.now());
|
|
20953
21143
|
}
|
|
20954
21144
|
try {
|
|
20955
|
-
const storageDir =
|
|
20956
|
-
if (!
|
|
20957
|
-
|
|
21145
|
+
const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
|
|
21146
|
+
if (!import_fs9.default.existsSync(storageDir)) {
|
|
21147
|
+
import_fs9.default.mkdirSync(storageDir, { recursive: true });
|
|
20958
21148
|
}
|
|
20959
|
-
const namedPath =
|
|
20960
|
-
if (!
|
|
20961
|
-
|
|
21149
|
+
const namedPath = import_path9.default.resolve(storageDir, entryName);
|
|
21150
|
+
if (!import_fs9.default.existsSync(namedPath)) {
|
|
21151
|
+
import_fs9.default.writeFileSync(namedPath, resource.content || "", "utf8");
|
|
20962
21152
|
}
|
|
20963
|
-
const indexHtmlPath =
|
|
20964
|
-
if (!
|
|
20965
|
-
|
|
21153
|
+
const indexHtmlPath = import_path9.default.resolve(storageDir, "index.html");
|
|
21154
|
+
if (!import_fs9.default.existsSync(indexHtmlPath)) {
|
|
21155
|
+
import_fs9.default.writeFileSync(indexHtmlPath, resource.content || "", "utf8");
|
|
20966
21156
|
}
|
|
20967
21157
|
} catch (storageErr) {
|
|
20968
21158
|
console.warn("[resources] Failed to write courseware storage cache:", storageErr);
|
|
@@ -21002,21 +21192,21 @@ function registerResourcesRoutes(ctx) {
|
|
|
21002
21192
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "folder", indexFile.path, resource.created_at || Date.now());
|
|
21003
21193
|
}
|
|
21004
21194
|
try {
|
|
21005
|
-
const storageDir =
|
|
21006
|
-
if (!
|
|
21007
|
-
|
|
21195
|
+
const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
|
|
21196
|
+
if (!import_fs9.default.existsSync(storageDir)) {
|
|
21197
|
+
import_fs9.default.mkdirSync(storageDir, { recursive: true });
|
|
21008
21198
|
for (const f of files) {
|
|
21009
21199
|
if (f.path && f.content !== void 0) {
|
|
21010
21200
|
const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21011
|
-
const target =
|
|
21201
|
+
const target = import_path9.default.resolve(storageDir, cleanRel);
|
|
21012
21202
|
if (target.startsWith(storageDir)) {
|
|
21013
|
-
|
|
21203
|
+
import_fs9.default.mkdirSync(import_path9.default.dirname(target), { recursive: true });
|
|
21014
21204
|
const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
|
|
21015
21205
|
if (isBin) {
|
|
21016
21206
|
const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
|
|
21017
|
-
|
|
21207
|
+
import_fs9.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
|
|
21018
21208
|
} else {
|
|
21019
|
-
|
|
21209
|
+
import_fs9.default.writeFileSync(target, f.content, "utf8");
|
|
21020
21210
|
}
|
|
21021
21211
|
}
|
|
21022
21212
|
}
|
|
@@ -21239,9 +21429,9 @@ function registerResourcesRoutes(ctx) {
|
|
|
21239
21429
|
}
|
|
21240
21430
|
|
|
21241
21431
|
// server/routes/courseware.ts
|
|
21242
|
-
var
|
|
21243
|
-
var
|
|
21244
|
-
var
|
|
21432
|
+
var import_path10 = __toESM(require("path"), 1);
|
|
21433
|
+
var import_fs10 = __toESM(require("fs"), 1);
|
|
21434
|
+
var import_crypto8 = __toESM(require("crypto"), 1);
|
|
21245
21435
|
function registerCoursewareRoutes(ctx) {
|
|
21246
21436
|
const { app, io } = ctx;
|
|
21247
21437
|
app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
@@ -21391,7 +21581,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21391
21581
|
return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
|
|
21392
21582
|
}
|
|
21393
21583
|
}
|
|
21394
|
-
const rawId = "raw_" +
|
|
21584
|
+
const rawId = "raw_" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
21395
21585
|
kernelContainer.db.prepare(
|
|
21396
21586
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
21397
21587
|
).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
|
|
@@ -21419,7 +21609,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21419
21609
|
kernelContainer.db.prepare(
|
|
21420
21610
|
"INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
21421
21611
|
).run(
|
|
21422
|
-
"res_" +
|
|
21612
|
+
"res_" + import_crypto8.default.randomBytes(8).toString("hex"),
|
|
21423
21613
|
attemptId,
|
|
21424
21614
|
parsedScore,
|
|
21425
21615
|
comment || null,
|
|
@@ -21445,7 +21635,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21445
21635
|
}
|
|
21446
21636
|
io.emit("courseware-attempt-updated", { attemptId, type: "log" });
|
|
21447
21637
|
void kernelContainer.eventBus.publish({
|
|
21448
|
-
id: "evt_" +
|
|
21638
|
+
id: "evt_" + import_crypto8.default.randomBytes(8).toString("hex"),
|
|
21449
21639
|
type: "courseware.event_logged",
|
|
21450
21640
|
source: "builtin.courseware",
|
|
21451
21641
|
payload: { attemptId, eventType, payload },
|
|
@@ -21542,8 +21732,8 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21542
21732
|
const { msg, url, student, courseware } = req.body;
|
|
21543
21733
|
const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
|
|
21544
21734
|
console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
|
|
21545
|
-
const logFile2 =
|
|
21546
|
-
|
|
21735
|
+
const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
|
|
21736
|
+
import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
|
|
21547
21737
|
`);
|
|
21548
21738
|
res.json({ success: true });
|
|
21549
21739
|
} catch (e) {
|
|
@@ -21619,7 +21809,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21619
21809
|
let assignment = kernelContainer.db.prepare("SELECT id FROM assignments WHERE class_id = ? AND lesson_id = ? AND title = ?").get(classId, lessonId, assignmentTitle);
|
|
21620
21810
|
let assignmentId = assignment?.id;
|
|
21621
21811
|
if (!assignmentId) {
|
|
21622
|
-
assignmentId = "ast-cw-" +
|
|
21812
|
+
assignmentId = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
21623
21813
|
kernelContainer.db.prepare(
|
|
21624
21814
|
"INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
21625
21815
|
).run(
|
|
@@ -21692,8 +21882,8 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21692
21882
|
}
|
|
21693
21883
|
|
|
21694
21884
|
// server/routes/bridge.ts
|
|
21695
|
-
var
|
|
21696
|
-
var
|
|
21885
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
21886
|
+
var import_fs11 = __toESM(require("fs"), 1);
|
|
21697
21887
|
function registerBridgeRoutes(ctx) {
|
|
21698
21888
|
const { app } = ctx;
|
|
21699
21889
|
app.get("/bridge.js", (_req, res) => {
|
|
@@ -21713,7 +21903,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21713
21903
|
if (!courseware) {
|
|
21714
21904
|
const resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(uuid);
|
|
21715
21905
|
if (resRow) {
|
|
21716
|
-
const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ?
|
|
21906
|
+
const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : "index.html";
|
|
21717
21907
|
const createdAt = resRow.created_at || Date.now();
|
|
21718
21908
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resRow.id, resRow.id, resRow.name, resRow.type || "html", entryName, createdAt);
|
|
21719
21909
|
courseware = {
|
|
@@ -21729,21 +21919,21 @@ function registerBridgeRoutes(ctx) {
|
|
|
21729
21919
|
if (!courseware) {
|
|
21730
21920
|
return res.status(404).send("Courseware not found");
|
|
21731
21921
|
}
|
|
21732
|
-
const storageDir =
|
|
21733
|
-
if (!
|
|
21922
|
+
const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", courseware.uuid);
|
|
21923
|
+
if (!import_fs11.default.existsSync(storageDir) || import_fs11.default.readdirSync(storageDir).length === 0) {
|
|
21734
21924
|
let resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ? OR id = ?").get(courseware.uuid, courseware.id);
|
|
21735
21925
|
if (!resRow && courseware.name) {
|
|
21736
21926
|
const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
|
|
21737
21927
|
resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE name = ? OR name = ?").get(courseware.name, nameCandidate);
|
|
21738
21928
|
}
|
|
21739
21929
|
if (resRow && resRow.content) {
|
|
21740
|
-
|
|
21930
|
+
import_fs11.default.mkdirSync(storageDir, { recursive: true });
|
|
21741
21931
|
if (resRow.type === "html") {
|
|
21742
|
-
const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ?
|
|
21743
|
-
const safeEntry =
|
|
21744
|
-
|
|
21932
|
+
const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : courseware.entry || "index.html";
|
|
21933
|
+
const safeEntry = import_path11.default.basename(originalName.replace(/\\/g, "/"));
|
|
21934
|
+
import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, safeEntry), resRow.content, "utf8");
|
|
21745
21935
|
if (safeEntry !== "index.html") {
|
|
21746
|
-
|
|
21936
|
+
import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
|
|
21747
21937
|
}
|
|
21748
21938
|
} else if (resRow.type === "folder") {
|
|
21749
21939
|
try {
|
|
@@ -21751,15 +21941,15 @@ function registerBridgeRoutes(ctx) {
|
|
|
21751
21941
|
for (const f of files) {
|
|
21752
21942
|
if (f.path && f.content !== void 0) {
|
|
21753
21943
|
const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21754
|
-
const target =
|
|
21944
|
+
const target = import_path11.default.resolve(storageDir, cleanRel);
|
|
21755
21945
|
if (target.startsWith(storageDir)) {
|
|
21756
|
-
|
|
21946
|
+
import_fs11.default.mkdirSync(import_path11.default.dirname(target), { recursive: true });
|
|
21757
21947
|
const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
|
|
21758
21948
|
if (isBin) {
|
|
21759
21949
|
const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
|
|
21760
|
-
|
|
21950
|
+
import_fs11.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
|
|
21761
21951
|
} else {
|
|
21762
|
-
|
|
21952
|
+
import_fs11.default.writeFileSync(target, f.content, "utf8");
|
|
21763
21953
|
}
|
|
21764
21954
|
}
|
|
21765
21955
|
}
|
|
@@ -21773,35 +21963,35 @@ function registerBridgeRoutes(ctx) {
|
|
|
21773
21963
|
if (!subpath || subpath === "") {
|
|
21774
21964
|
subpath = courseware.entry || "index.html";
|
|
21775
21965
|
}
|
|
21776
|
-
let filePath =
|
|
21966
|
+
let filePath = import_path11.default.resolve(storageDir, subpath);
|
|
21777
21967
|
if (!filePath.startsWith(storageDir)) {
|
|
21778
21968
|
return res.status(403).send("Access denied");
|
|
21779
21969
|
}
|
|
21780
|
-
if (!
|
|
21781
|
-
const dirEntries =
|
|
21970
|
+
if (!import_fs11.default.existsSync(filePath) && import_fs11.default.existsSync(storageDir)) {
|
|
21971
|
+
const dirEntries = import_fs11.default.readdirSync(storageDir);
|
|
21782
21972
|
const isLookingForEntry = !subpath || subpath === "" || subpath === "index.html" || subpath === "index.htm" || subpath === courseware.entry;
|
|
21783
21973
|
if (isLookingForEntry) {
|
|
21784
21974
|
let candidate = dirEntries.find(
|
|
21785
|
-
(f) => f === courseware.entry &&
|
|
21975
|
+
(f) => f === courseware.entry && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
|
|
21786
21976
|
);
|
|
21787
21977
|
if (!candidate && courseware.name) {
|
|
21788
21978
|
const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
|
|
21789
21979
|
candidate = dirEntries.find(
|
|
21790
|
-
(f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) &&
|
|
21980
|
+
(f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
|
|
21791
21981
|
);
|
|
21792
21982
|
}
|
|
21793
21983
|
if (!candidate) {
|
|
21794
21984
|
candidate = dirEntries.find((f) => {
|
|
21795
21985
|
const lower = f.toLowerCase();
|
|
21796
|
-
return (lower.endsWith(".html") || lower.endsWith(".htm")) &&
|
|
21986
|
+
return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile();
|
|
21797
21987
|
});
|
|
21798
21988
|
}
|
|
21799
21989
|
if (candidate) {
|
|
21800
|
-
filePath =
|
|
21990
|
+
filePath = import_path11.default.resolve(storageDir, candidate);
|
|
21801
21991
|
try {
|
|
21802
|
-
const indexHtmlPath =
|
|
21803
|
-
if (!
|
|
21804
|
-
|
|
21992
|
+
const indexHtmlPath = import_path11.default.resolve(storageDir, "index.html");
|
|
21993
|
+
if (!import_fs11.default.existsSync(indexHtmlPath)) {
|
|
21994
|
+
import_fs11.default.copyFileSync(filePath, indexHtmlPath);
|
|
21805
21995
|
}
|
|
21806
21996
|
if (courseware.entry !== candidate) {
|
|
21807
21997
|
kernelContainer.db.prepare("UPDATE courseware SET entry = ? WHERE id = ?").run(candidate, courseware.id);
|
|
@@ -21812,9 +22002,9 @@ function registerBridgeRoutes(ctx) {
|
|
|
21812
22002
|
}
|
|
21813
22003
|
} else {
|
|
21814
22004
|
for (const entry of dirEntries) {
|
|
21815
|
-
const subDirPath =
|
|
21816
|
-
if (
|
|
21817
|
-
const subEntries =
|
|
22005
|
+
const subDirPath = import_path11.default.join(storageDir, entry);
|
|
22006
|
+
if (import_fs11.default.statSync(subDirPath).isDirectory()) {
|
|
22007
|
+
const subEntries = import_fs11.default.readdirSync(subDirPath);
|
|
21818
22008
|
const subCandidate = subEntries.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || subEntries.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
|
|
21819
22009
|
if (subCandidate) {
|
|
21820
22010
|
const redirectPath = `${entry}/${subCandidate}`;
|
|
@@ -21825,12 +22015,12 @@ function registerBridgeRoutes(ctx) {
|
|
|
21825
22015
|
}
|
|
21826
22016
|
}
|
|
21827
22017
|
}
|
|
21828
|
-
if (!
|
|
22018
|
+
if (!import_fs11.default.existsSync(filePath)) {
|
|
21829
22019
|
return res.status(404).send(`File not found: ${subpath}`);
|
|
21830
22020
|
}
|
|
21831
|
-
const stat =
|
|
22021
|
+
const stat = import_fs11.default.statSync(filePath);
|
|
21832
22022
|
if (stat.isDirectory()) {
|
|
21833
|
-
const dirFiles =
|
|
22023
|
+
const dirFiles = import_fs11.default.readdirSync(filePath);
|
|
21834
22024
|
const htmlFile = dirFiles.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || dirFiles.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
|
|
21835
22025
|
if (htmlFile) {
|
|
21836
22026
|
const base = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
|
|
@@ -21838,7 +22028,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21838
22028
|
}
|
|
21839
22029
|
return res.status(404).send("Directory index not found");
|
|
21840
22030
|
}
|
|
21841
|
-
const ext =
|
|
22031
|
+
const ext = import_path11.default.extname(filePath).toLowerCase();
|
|
21842
22032
|
let contentType = "text/plain; charset=utf-8";
|
|
21843
22033
|
if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
|
|
21844
22034
|
else if (ext === ".css") contentType = "text/css; charset=utf-8";
|
|
@@ -21858,7 +22048,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21858
22048
|
"Content-Security-Policy",
|
|
21859
22049
|
"sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src-attr 'unsafe-inline';"
|
|
21860
22050
|
);
|
|
21861
|
-
let html =
|
|
22051
|
+
let html = import_fs11.default.readFileSync(filePath, "utf8");
|
|
21862
22052
|
html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
|
|
21863
22053
|
return res.send(html);
|
|
21864
22054
|
} else {
|
|
@@ -22448,7 +22638,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22448
22638
|
}
|
|
22449
22639
|
|
|
22450
22640
|
// server/routes/workspace.ts
|
|
22451
|
-
var
|
|
22641
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
22452
22642
|
function registerWorkspaceRoutes(ctx) {
|
|
22453
22643
|
const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
|
|
22454
22644
|
app.get("/api/events", requireAuth("administrator"), (req, res) => {
|
|
@@ -22597,7 +22787,7 @@ ${sub.content}`
|
|
|
22597
22787
|
}
|
|
22598
22788
|
const filename = parts[parts.length - 1];
|
|
22599
22789
|
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
|
|
22600
|
-
const ext =
|
|
22790
|
+
const ext = import_path12.default.extname(filename).toLowerCase();
|
|
22601
22791
|
const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
|
|
22602
22792
|
const content = foundNode.content || "";
|
|
22603
22793
|
if (binaryExtensions.includes(ext)) {
|
|
@@ -22839,9 +23029,9 @@ function registerAdminRoutes(ctx) {
|
|
|
22839
23029
|
}
|
|
22840
23030
|
|
|
22841
23031
|
// server/routes/roster.ts
|
|
22842
|
-
var
|
|
22843
|
-
var
|
|
22844
|
-
var
|
|
23032
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
23033
|
+
var import_fs12 = __toESM(require("fs"), 1);
|
|
23034
|
+
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
22845
23035
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
22846
23036
|
function registerRosterRoutes(ctx) {
|
|
22847
23037
|
const { app, io, loginLimiter } = ctx;
|
|
@@ -23095,7 +23285,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23095
23285
|
if (storedPwd.startsWith("$2")) {
|
|
23096
23286
|
matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
|
|
23097
23287
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
23098
|
-
matches =
|
|
23288
|
+
matches = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
|
|
23099
23289
|
} else {
|
|
23100
23290
|
matches = storedPwd === oldPassword;
|
|
23101
23291
|
}
|
|
@@ -23152,7 +23342,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23152
23342
|
if (!filename || !base64Data) {
|
|
23153
23343
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
23154
23344
|
}
|
|
23155
|
-
const ext =
|
|
23345
|
+
const ext = import_path13.default.extname(filename).toLowerCase();
|
|
23156
23346
|
const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
23157
23347
|
if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
|
|
23158
23348
|
return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
|
|
@@ -23165,11 +23355,11 @@ function registerRosterRoutes(ctx) {
|
|
|
23165
23355
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
23166
23356
|
return res.status(400).json({ error: "File content does not match the declared image type" });
|
|
23167
23357
|
}
|
|
23168
|
-
const avatarDir =
|
|
23169
|
-
|
|
23170
|
-
const uniqueName = `${Date.now()}-${
|
|
23171
|
-
const filePath =
|
|
23172
|
-
|
|
23358
|
+
const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
|
|
23359
|
+
import_fs12.default.mkdirSync(avatarDir, { recursive: true });
|
|
23360
|
+
const uniqueName = `${Date.now()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
|
|
23361
|
+
const filePath = import_path13.default.join(avatarDir, uniqueName);
|
|
23362
|
+
import_fs12.default.writeFileSync(filePath, fileBuffer);
|
|
23173
23363
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
23174
23364
|
let oldAvatar = null;
|
|
23175
23365
|
if (session.role === "student") {
|
|
@@ -23183,7 +23373,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23183
23373
|
}
|
|
23184
23374
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
23185
23375
|
try {
|
|
23186
|
-
|
|
23376
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
23187
23377
|
} catch {
|
|
23188
23378
|
}
|
|
23189
23379
|
}
|
|
@@ -23219,7 +23409,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23219
23409
|
}
|
|
23220
23410
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
23221
23411
|
try {
|
|
23222
|
-
|
|
23412
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
23223
23413
|
} catch {
|
|
23224
23414
|
}
|
|
23225
23415
|
}
|
|
@@ -23286,7 +23476,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23286
23476
|
if (storedPwd.startsWith("$2")) {
|
|
23287
23477
|
matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
|
|
23288
23478
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
23289
|
-
const sha256Hash =
|
|
23479
|
+
const sha256Hash = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
|
|
23290
23480
|
if (sha256Hash === storedPwd) {
|
|
23291
23481
|
matchesOwnPassword = true;
|
|
23292
23482
|
kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
|
|
@@ -23330,7 +23520,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23330
23520
|
};
|
|
23331
23521
|
}
|
|
23332
23522
|
if (sessionData) {
|
|
23333
|
-
const sessionToken = "token_" +
|
|
23523
|
+
const sessionToken = "token_" + import_crypto9.default.randomBytes(16).toString("hex");
|
|
23334
23524
|
const now = Date.now();
|
|
23335
23525
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
23336
23526
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
@@ -25168,8 +25358,8 @@ ${examsText}
|
|
|
25168
25358
|
|
|
25169
25359
|
// server/routes/plugins.ts
|
|
25170
25360
|
var import_express2 = __toESM(require("express"), 1);
|
|
25171
|
-
var
|
|
25172
|
-
var
|
|
25361
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
25362
|
+
var import_fs13 = __toESM(require("fs"), 1);
|
|
25173
25363
|
var import_semver3 = __toESM(require("semver"), 1);
|
|
25174
25364
|
|
|
25175
25365
|
// server/services/version-fetcher.ts
|
|
@@ -25327,7 +25517,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
25327
25517
|
}
|
|
25328
25518
|
|
|
25329
25519
|
// server/routes/plugin-api-gateway.ts
|
|
25330
|
-
var
|
|
25520
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
25331
25521
|
init_http_router();
|
|
25332
25522
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set(["config", "toggle", "contributions", "check-update", "one-click-update"]);
|
|
25333
25523
|
var FORBIDDEN_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -25461,7 +25651,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
25461
25651
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
25462
25652
|
return;
|
|
25463
25653
|
}
|
|
25464
|
-
const normalizedSubPath =
|
|
25654
|
+
const normalizedSubPath = import_node_path4.default.posix.normalize("/" + rawSubPath);
|
|
25465
25655
|
if (normalizedSubPath.includes("..")) {
|
|
25466
25656
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
25467
25657
|
return;
|
|
@@ -25681,16 +25871,16 @@ data: ${JSON.stringify({ error: msg })}
|
|
|
25681
25871
|
|
|
25682
25872
|
// server/routes/plugins.ts
|
|
25683
25873
|
function findLocalPluginSource(manifestId) {
|
|
25684
|
-
const root =
|
|
25685
|
-
if (!
|
|
25874
|
+
const root = import_path14.default.resolve(process.cwd(), "v2_plugins");
|
|
25875
|
+
if (!import_fs13.default.existsSync(root)) return null;
|
|
25686
25876
|
let best = null;
|
|
25687
25877
|
try {
|
|
25688
|
-
for (const entry of
|
|
25878
|
+
for (const entry of import_fs13.default.readdirSync(root, { withFileTypes: true })) {
|
|
25689
25879
|
if (!entry.isDirectory()) continue;
|
|
25690
|
-
const manifestPath =
|
|
25691
|
-
if (!
|
|
25880
|
+
const manifestPath = import_path14.default.join(root, entry.name, "manifest.json");
|
|
25881
|
+
if (!import_fs13.default.existsSync(manifestPath)) continue;
|
|
25692
25882
|
try {
|
|
25693
|
-
const m = JSON.parse(
|
|
25883
|
+
const m = JSON.parse(import_fs13.default.readFileSync(manifestPath, "utf-8"));
|
|
25694
25884
|
if (m?.id !== manifestId || !m?.version) continue;
|
|
25695
25885
|
if (!best || import_semver3.default.gt(m.version, best.version)) best = { dir: entry.name, version: m.version };
|
|
25696
25886
|
} catch {
|
|
@@ -25726,11 +25916,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25726
25916
|
const { app, buildOpenAITools: buildOpenAITools2, executeAgentToolCall: executeAgentToolCall2, buildOpenAIChatUrl: buildOpenAIChatUrl2, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2 } = ctx;
|
|
25727
25917
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
25728
25918
|
try {
|
|
25729
|
-
const docPath =
|
|
25730
|
-
if (!
|
|
25919
|
+
const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
25920
|
+
if (!import_fs13.default.existsSync(docPath)) {
|
|
25731
25921
|
return res.status(404).json({ success: false, error: "Document not found" });
|
|
25732
25922
|
}
|
|
25733
|
-
const content =
|
|
25923
|
+
const content = import_fs13.default.readFileSync(docPath, "utf-8");
|
|
25734
25924
|
res.json({ success: true, content });
|
|
25735
25925
|
} catch (e) {
|
|
25736
25926
|
sendSafeError(res, e);
|
|
@@ -25741,11 +25931,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25741
25931
|
const limit = parseInt(req.query.limit) || 200;
|
|
25742
25932
|
const component = req.query.component;
|
|
25743
25933
|
const levelFilter = req.query.level;
|
|
25744
|
-
const logFile2 =
|
|
25745
|
-
if (!
|
|
25934
|
+
const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
|
|
25935
|
+
if (!import_fs13.default.existsSync(logFile2)) {
|
|
25746
25936
|
return res.json({ success: true, logs: [] });
|
|
25747
25937
|
}
|
|
25748
|
-
const content =
|
|
25938
|
+
const content = import_fs13.default.readFileSync(logFile2, "utf-8");
|
|
25749
25939
|
const lines = content.split("\n").filter(Boolean);
|
|
25750
25940
|
let parsedLogs = lines.map((line) => {
|
|
25751
25941
|
try {
|
|
@@ -26307,7 +26497,7 @@ async function startServer() {
|
|
|
26307
26497
|
config: { port: Number(process.env.PORT) || 9e3 }
|
|
26308
26498
|
});
|
|
26309
26499
|
try {
|
|
26310
|
-
const migrationsDir =
|
|
26500
|
+
const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
|
|
26311
26501
|
const migrations = loadMigrationsFromDirectory(migrationsDir);
|
|
26312
26502
|
if (migrations.length > 0) {
|
|
26313
26503
|
runMigrations(kernelContainer.db, migrations);
|
|
@@ -26371,8 +26561,8 @@ async function startServer() {
|
|
|
26371
26561
|
});
|
|
26372
26562
|
app.use(import_express3.default.json({ limit: "10mb" }));
|
|
26373
26563
|
app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
|
|
26374
|
-
app.use("/uploads", import_express3.default.static(
|
|
26375
|
-
app.use("/plugins", import_express3.default.static(
|
|
26564
|
+
app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
|
|
26565
|
+
app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
|
|
26376
26566
|
const configuredOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
26377
26567
|
const isOriginAllowed = (origin, hostHeader) => {
|
|
26378
26568
|
if (!origin) return true;
|
|
@@ -26545,10 +26735,10 @@ async function startServer() {
|
|
|
26545
26735
|
});
|
|
26546
26736
|
app.use(vite.middlewares);
|
|
26547
26737
|
} else {
|
|
26548
|
-
const distPath = __dirname ||
|
|
26738
|
+
const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
|
|
26549
26739
|
app.use(import_express3.default.static(distPath));
|
|
26550
26740
|
app.get("*", (req, res) => {
|
|
26551
|
-
res.sendFile(
|
|
26741
|
+
res.sendFile(import_path15.default.join(distPath, "index.html"));
|
|
26552
26742
|
});
|
|
26553
26743
|
}
|
|
26554
26744
|
const HOST = process.env.HOST || "0.0.0.0";
|